MyBatis3源码深度解析(二十一)动态SQL实现原理(二)动态SQL解析过程、#{}和${}的区别

news2024/9/20 18:49:00

文章目录

    • 前言
    • 8.5 动态SQL解析过程
      • 8.5.1 SQL配置转换为SqlSource对象
      • 8.5.2 SqlSource转换为静态SQL语句
    • 8.6 #{}和${}的区别
    • 8.7 小结

前言

在【MyBatis3源码深度解析(二十)动态SQL实现原理(一)动态SQL的核心组件】中研究了MyBatis动态SQL相关的组件,如SqlSource用于描述通过XML文件或Java注解配置的SQL信息,SqlNode用于描述动态SQL中的<if>、<where>等标签信息,LanguageDriver用于对SQL配置进行解析,将SQL配置转换为SqlSource对象。

研究了MyBatis动态SQL相关的组件,下面来研究一下动态SQL的解析过程。本文使用如下案例进行调试:

<!--UserMapper.xml-->
<select id="selectByCons" parameterType="User" resultType="User">
    select * from user where id = '${id}'
    <if test="name != null and name != ''">
        and name = #{name}
    </if>
    <if test="age != null">
        and age = #{age}
    </if>
</select>
// ......
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User user = new User();
user.setId(1);
user.setAge(18);
userMapper.selectByCons(user);

8.5 动态SQL解析过程

8.5.1 SQL配置转换为SqlSource对象

LanguageDriver用于对SQL配置进行解析,它其中一个实现类XMLLanguageDriver的createSqlSource()方法就用于解析XML配置文件中的SQL配置。

源码1org.apache.ibatis.scripting.xmltags.XMLLanguageDriver

@Override
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
    XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
    return builder.parseScriptNode();
}

由 源码1 可知,在XMLLanguageDriver的createSqlSource()方法中,XML配置文件中的SQL配置的解析实际上是委托给XMLScriptBuilder类来完成的,调用XMLScriptBuilder类的parseScriptNode()方法来完成解析工作。

该方法的参数值,script参数即<select>标签对应的XNode对象:

源码2org.apache.ibatis.scripting.xmltags.XMLScriptBuilder

private final XNode context;
private boolean isDynamic;
private final Class<?> parameterType;
  
public SqlSource parseScriptNode() {
    // 将SQL信息对应的XNode对象转换为SqlNode对象
    MixedSqlNode rootSqlNode = parseDynamicTags(context);
    SqlSource sqlSource;
    if (isDynamic) {
        // 如果是动态SQL,则返回DynamicSqlSource实例
        sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {
        // 如果不是动态SQL,则返回RawSqlSource实例
        sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    return sqlSource;
}

由 源码2 可知,在XMLScriptBuilder类的parseScriptNode()方法中,首先调用parseDynamicTags()方法将SQL信息对应的XNode对象转换为SqlNode对象,然后判定是否为动态SQL,如果是则返回DynamicSqlSource实例,否则返回RawSqlSource实例。

源码3org.apache.ibatis.scripting.xmltags.XMLScriptBuilder

protected MixedSqlNode parseDynamicTags(XNode node) {
    List<SqlNode> contents = new ArrayList<>();
    // 获取子节点
    NodeList children = node.getNode().getChildNodes();
    // 遍历子节点
    for (int i = 0; i < children.getLength(); i++) {
        XNode child = node.newXNode(children.item(i));
        // 如果子节点为文本节点,即非<if>、<where>等标签
        // 则使用TextSqlNode描述子节点
        if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
            String data = child.getStringBody("");
            TextSqlNode textSqlNode = new TextSqlNode(data);
            if (textSqlNode.isDynamic()) {
                // 如果是动态SQL,则将isDynamic属性设置为true
                contents.add(textSqlNode);
                isDynamic = true;
            } else {
                // 非动态SQL,只用StaticTextSqlNode描述子节点
                contents.add(new StaticTextSqlNode(data));
            }
        } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) {
            // 如果子节点为元素节点,即<if>、<where>等标签
            // 则使用对应的NodeHandler进行处理
            String nodeName = child.getNode().getNodeName();
            NodeHandler handler = nodeHandlerMap.get(nodeName);
            if (handler == null) {
                throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
            }
            // 调用NodeHandler的handleNode()方法进行处理
            handler.handleNode(child, contents);
            // 元素节点一定是动态SQL
            isDynamic = true;
        }
    }
    return new MixedSqlNode(contents);
}

由 源码3 可知,在XMLScriptBuilder类的parseDynamicTags()方法中,会对SQL配置对应的XNode对象的所有子节点进行遍历。

如果子节点为文本节点(即非标签元素),则使用TextSqlNode描述子节点,然后继续判断该SQL文本是否是动态SQL,若是则将isDynamic属性设置为true(源码2 中会根据该属性返回不同类型的SqlSource对象),若不是则转为使用StaticTextSqlNode描述子节点。

TextSqlNode对象的isDynamic()方法用于判断SQL文本是否是动态SQL:

源码4org.apache.ibatis.scripting.xmltags.TextSqlNode

public boolean isDynamic() {
    DynamicCheckerTokenParser checker = new DynamicCheckerTokenParser();
    GenericTokenParser parser = createParser(checker);
    parser.parse(text);
    return checker.isDynamic();
}

private GenericTokenParser createParser(TokenHandler handler) {
    return new GenericTokenParser("${", "}", handler);
}

由 源码4 可知,判断SQL文本是否是动态SQL需要借助GenericTokenParser类来完成。在createParser()方法中,创建了一个GenericTokenParser对象并传递了"${""}"两个参数。

由此可以猜想,判断SQL文本是否是动态SQL的依据是SQL文本中是否包含"${""}"符号。(通过后文分析会发现这里猜想是对的)

借助Debug工具,可以查看到案例中TextSqlNode对象封装了select * from user where id = '${id}',并且它是动态SQL:

回到 源码3, 如果子节点为元素节点,即、等标签节点,则使用对应的NodeHandler进行处理。

在XMLScriptBuilder类内部,定义了一个私有的接口NodeHandler,并为每种动态SQL标签提供了一个NodeHandler接口的实现类,用于处理对应的动态SQL标签,转换为对应的SqlNode对象。

源码5org.apache.ibatis.scripting.xmltags.XMLScriptBuilder

private interface NodeHandler {
    void handleNode(XNode nodeToHandle, List<SqlNode> targetContents);
}

由 源码5 可知,NodeHandler接口只有一个handleNode()方法,它接收两个参数:nodeToHandle参数是动态SQL标签对应的XNode对象,targetContents参数是存放SqlNode对象的List集合。该方法对XML标签进行解析后,把生成的SqlNode对象存放到该List集合中。

借助Debug,可以列出NodeHandler接口的8个实现类:

由图可知,每个实现类用于处理对应的动态SQL标签,例如IfHandler用于处理动态SQL配置中的<if>标签,将<if>标签的内容转换为IfSqlNode对象。

源码6org.apache.ibatis.scripting.xmltags.XMLScriptBuilder

private class IfHandler implements NodeHandler {
    public IfHandler() {
        // Prevent Synthetic Access
    }
    
    @Override
    public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
        // 继续调用parseDynamicTags()方法处理<if>标签下的子节点
        MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
        // 获取<if>标签的test属性
        String test = nodeToHandle.getStringAttribute("test");
        // 创建IfSqlNode对象并添加到List集合中
        IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);
        targetContents.add(ifSqlNode);
    }
}

由 源码6 可知,在IfHandler的handleNode()方法中,会继续调用XMLScriptBuilder类的parseDynamicTags()方法完成<if>标签的子节点的解析,将子节点转换为MixedSqlNode对象,然后获取<if>标签的test属性,接着创建IfSqlNode对象并添加到List集合中。

这里使用了“递归”来完成动态SQL标签的解析。parseDynamicTags()方法会获取当前节点的所有子节点,如果子节点为标签节点,则继续调用对应的NodeHandler进行处理,这就“递归”地完成了所有动态SQL标签的解析。

需要注意的是,在XMLScriptBuilder类的构造方法中,会调用initNodeHandlerMap()方法将所有的NodeHandler实例注册到Map集合中:

源码7org.apache.ibatis.scripting.xmltags.XMLScriptBuilder

public XMLScriptBuilder(Configuration configuration, XNode context, Class<?> parameterType) {
    super(configuration);
    this.context = context;
    this.parameterType = parameterType;
    initNodeHandlerMap();
}

private void initNodeHandlerMap() {
    nodeHandlerMap.put("trim", new TrimHandler());
    nodeHandlerMap.put("where", new WhereHandler());
    nodeHandlerMap.put("set", new SetHandler());
    nodeHandlerMap.put("foreach", new ForEachHandler());
    nodeHandlerMap.put("if", new IfHandler());
    nodeHandlerMap.put("choose", new ChooseHandler());
    nodeHandlerMap.put("when", new IfHandler());
    nodeHandlerMap.put("otherwise", new OtherwiseHandler());
    nodeHandlerMap.put("bind", new BindHandler());
}

由 源码7 可知,提前注册好NodeHandler实例,在需要解析动态SQL标签时,只需要根据标签名获取对应的NodeHandler对象进行处理即可,而不需要每次都创建对应的NodeHandler实例,这是享元思想的应用。

经过以上逻辑,SQL配置已转换为对应的SqlSource对象。 案例中的SqlSource对象内容如下:

由图可知,由于是动态SQL,因此该SqlSource对象的类型是DynamicSqlSource。SQL配置被解析后,转换为5个SqlNode对象,包括1个TextSqlNode对象和2个IfSqlNode对象,剩余2个StaticSqlNode对象是换行符和空格。

8.5.2 SqlSource转换为静态SQL语句

SQL配置转换为SqlSource对象后,存放在MappedStatement对象的sqlSource属性中。

在【MyBatis3源码深度解析(十六)SqlSession的创建与执行(三)Mapper方法的调用过程】中提到,SELECT类型的Mapper方法的调用过程中,会调用BaseExecutor类的query()方法,内部再转调MappedStatement对象的getBoundSql()方法获取一个BoundSql对象。

源码8org.apache.ibatis.mapping.MappedStatement

private SqlSource sqlSource;

public BoundSql getBoundSql(Object parameterObject) {
    // 调用SqlSource对象的getBoundSql()方法获取BoundSql对象
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    // ......
    return boundSql;
}

由 源码8 可知,在MappedStatement对象的getBoundSql()方法中,会调用SqlSource对象的getBoundSql()方法获取一个BoundSql对象,而BoundSql对象内部封装了SQL语句及参数信息,这就完成了SqlSource对象到静态SQL语句的转换。

源码9org.apache.ibatis.scripting.xmltags.DynamicSqlSource

public class DynamicSqlSource implements SqlSource {

    private final Configuration configuration;
    private final SqlNode rootSqlNode;

    public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
        this.configuration = configuration;
        this.rootSqlNode = rootSqlNode;
    }

    @Override
    public BoundSql getBoundSql(Object parameterObject) {
        // 通过参数对象构建动态SQL上下文对象
        DynamicContext context = new DynamicContext(configuration, parameterObject);
        // 调用SqlNode对象的```apply()```方法
        rootSqlNode.apply(context);
        // 创建SqlSourceBuilder对象
        SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
        Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
        // 调用SqlSourceBuilder对象的parse()方法对SQL内容做进一步的处理,返回一个StaticSqlSource对象
        SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
        // 调用StaticSqlSource对象的getBoundSql()方法获取BoundSql对象
        BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
        // 将<bind>标签绑定的参数添加到BoundSql对象中
        context.getBindings().forEach(boundSql::setAdditionalParameter);
        return boundSql;
    }
}

由 源码9 可知,在DynamicSqlSource的getBoundSql()方法中,会调用SqlNode对象的apply()方法对动态SQL进行解析(解析过程详见【MyBatis3源码深度解析(二十)动态SQL实现原理(一)动态SQL的核心组件 8.4 SqlNode组件】)。

动态SQL解析完成后,接着调用SqlSourceBuilder对象的parse()方法对SQL内容做进一步的处理,返回一个StaticSqlSource对象,StaticSqlSource对象用于描述动态SQL解析后的静态SQL信息。

最后,调用StaticSqlSource对象的getBoundSql()方法获取BoundSql对象并返回。

源码10org.apache.ibatis.builder.SqlSourceBuilder

public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
    // 创建一个参数映射处理器,对SQL中的"#{}"参数占位符进行解析
    ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType,
        additionalParameters);
    // 创建一个GenericTokenParser对象,对SQL中的"#{}"参数占位符进行解析
    GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
    String sql;
    if (configuration.isShrinkWhitespacesInSql()) {
        sql = parser.parse(removeExtraWhitespaces(originalSql));
    } else {
        sql = parser.parse(originalSql);
    }
    // 返回StaticSqlSource对象
    return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}

由 源码10 可知,在SqlSourceBuilder对象的parse()方法中,创建了两个处理器:ParameterMappingTokenHandler对象是参数映射处理器,负责将SQL语句中的"#{}"参数占位符进行转换;GenericTokenParser对象负责对SQL中的"#{}"参数占位符进行解析。

借助Debug工具,可以获取此时originalSql参数的值为:select * from user where id = '1' and age = #{age}(去掉了一些不必要的空格和换行符)。

注意,到这一步会发现${id}已经被替换成具体的参数值,是怎么替换的放到下文再解析。

下面就来分析一下GenericTokenParser对象的parse()方法的原理:

源码11org.apache.ibatis.parsing.GenericTokenParser

public class GenericTokenParser {

    private final String openToken;
    private final String closeToken;
    private final TokenHandler handler;

    public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) {
        // 被初始化为 "#{"
        this.openToken = openToken;
        // 被初始化为 "}"
        this.closeToken = closeToken;
        this.handler = handler;
    }
    
    public String parse(String text) {
        if (text == null || text.isEmpty()) {
            return "";
        }
        // 获取第1个"#{"符号在SQL文本中的位置索引
        int start = text.indexOf(openToken);
        if (start == -1) {
            // 位置索引为-1,说明不存在"#{"符号
            return text;
        }
        // 将SQL文本转换为字符数组
        char[] src = text.toCharArray();
        // 用于记录已处理字符的偏移量
        int offset = 0;
        // 记录已确定的SQL文本
        final StringBuilder builder = new StringBuilder();
        // 记录"#{}"符号内的内容
        StringBuilder expression = null;
        do {
            if (start > 0 && src[start - 1] == '\\') {
                // 如果"#{"符号前面是'\\'符号,说明这个符号后的内容已经被注释掉了
                // 则记录'\\'符号前面的内容,并记录此时的偏移量
                builder.append(src, offset, start - offset - 1).append(openToken);
                offset = start + openToken.length();
            } else {
                // 找到了"#{"符号
                if (expression == null) {
                    expression = new StringBuilder();
                } else {
                    expression.setLength(0);
                }
                // "#{"符号前面的内容已经是确定的
                builder.append(src, offset, start - offset);
                // 偏移量移动到"#{"符号后
                offset = start + openToken.length();
                // 寻找结束字符"}"的位置索引
                int end = text.indexOf(closeToken, offset);
                while (end > -1) {
                    // 找到了结束字符"}"的位置索引
                    if ((end <= offset) || (src[end - 1] != '\\')) {
                        // 结束字符"}"的位置索引比当前偏移量大,且结束字符之前没有'\\'符号
                        // 说明确实是真的结束字符
                        // 将"#{}"符号内的内容记录到expression中
                        expression.append(src, offset, end - offset);
                        break;
                    }
                    // 结束字符之前有'\\'符号,说明已经注释掉了,则手动加一个结束字符
                    expression.append(src, offset, end - offset - 1).append(closeToken);
                    // 记录偏移量和结束字符的位置
                    offset = end + closeToken.length();
                    end = text.indexOf(closeToken, offset);
                }
                if (end == -1) {
                    // 没有找到结束字符,则把后面的字符全加入到builder
                    builder.append(src, start, src.length - start);
                    offset = src.length;
                } else {
                    // 有结束字符,则调用TokenHandler的handleToken()方法处理"#{}"符号内的内容
                    builder.append(handler.handleToken(expression.toString()));
                    offset = end + closeToken.length();
                }
            }
            // 将开始索引移动到新的位置
            start = text.indexOf(openToken, offset);
        } while (start > -1);
        if (offset < src.length) {
            builder.append(src, offset, src.length - offset);
        }
        return builder.toString();
    }
}

由 源码11 可知,在GenericTokenParser对象的parse()方法中,主要是对SQL语句中的所有#{}参数占位符进行解析,获取参数占位符内的内容,并调用ParameterMappingTokenHandler对象的handleToken()方法对参数占位符内容进行替换。

#{}参数占位符被替换成了什么呢?

源码12org.apache.ibatis.builder.SqlSourceBuilder

private static class ParameterMappingTokenHandler extends BaseBuilder implements TokenHandler {
    @Override
    public String handleToken(String content) {
        parameterMappings.add(buildParameterMapping(content));
        return "?";
    }
}

由 源码12 可知,#{}参数占位符最终被替换成了一个“?”。

借助Debug工具,确实发现originalSql参数的值由原来的select * from user where id = '1' and age = #{age}转换为select * from user where id = '1' and age = ?

可想而知,#{}参数占位符被替换成了一个“?”之后,就可以调用PreparedStatement对象的setXXX()方法为参数占位符设置值了。

另外,在ParameterMappingTokenHandler对象的handleToken()方法中,还调用了buildParameterMapping()方法对占位符内容进行解析,即对javaType、jdbcType、property等参数进行解析。

至此,动态SQL的解析完成,最终获得的SQL语句是包含?号的,后续会通过调用PreparedStatement对象的setXXX()方法为?号设置值。

8.6 #{}和${}的区别

在案例中,#{age}最终被转换为?,而${id}直接被替换为具体的参数值。这两者的解析有什么区别呢?

前面已经详细研究了#{}参数占位符的解析,下面重点看看${}参数占位符的解析。

经过【8.5.1 SQL配置转换为SqlSource】的逻辑,SQL配置已转换为对应的SqlSource对象。 在该对象中,包括1个TextSqlNode对象,该对象描述的SQL文本是:select * from user where id = '${id}'

可见,对${}参数占位符的解析在TextSqlNode对象的apply()方法中完成。

源码13org.apache.ibatis.scripting.xmltags.TextSqlNode

@Override
public boolean apply(DynamicContext context) {
    GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
    context.appendSql(parser.parse(text));
    return true;
}

private GenericTokenParser createParser(TokenHandler handler) {
    return new GenericTokenParser("${", "}", handler);
}

由 源码13 可知,在TextSqlNode对象的apply()方法中,会转调GenericTokenParser对象的parse()方法,但GenericTokenParser对象的openToken属性值为${,closeToken属性值为},TokenHandler为BindingTokenParser对象。

parse()方法的逻辑与 源码11 的分析是一致的,不同的是openToken、closeToken属性值不一样,最终会调用BindingTokenParser对象的handleToken()方法对${}参数占位符进行处理。(这里也足以证明判断SQL文本是否是动态SQL的依据是SQL文本中是否包含"${""}"符号)

源码14org.apache.ibatis.scripting.xmltags.TextSqlNode

private static class BindingTokenParser implements TokenHandler {
    @Override
    public String handleToken(String content) {
        // 获取内置参数,该参数保存了所有参数信息
        Object parameter = context.getBindings().get("_parameter");
        if (parameter == null) {
            context.getBindings().put("value", null);
        } else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) {
            context.getBindings().put("value", parameter);
        }
        // 通过OGNL表达式获取参数值
        Object value = OgnlCache.getValue(content, context.getBindings());
        String srtValue = value == null ? "" : String.valueOf(value);
        checkInjection(srtValue);
        // 返回参数值
        return srtValue;
    }
}

由 源码14 可知,与ParameterMappingTokenHandler对象的handleToken()方法返回一个?号不同,BindingTokenParser对象的handleToken()方法返回了具体的参数值。这也就解释了${}参数占位符会直接被替换为具体的参数值。

总结一下,使用#{}参数占位符时,占位符内容会被替换成?,然后通过PreparedStatement对象的setXXX()方法为参数占位符设置值;而${}参数占位符内容会被直接替换为具体的参数值。

使用#{}参数占位符能够有效避免SQL注入问题,在实际开发中应优先考虑,当其无法满足要求时才考虑使用${}参数占位符。

8.7 小结

第八章到此就梳理完毕了,本章的主题是:动态SQL实现原理。回顾一下本章的梳理的内容:

(二十)SqlSource、BoundSql、LanguageDriver、SqlNode组件
(二十一)动态SQL解析过程、#{}和${}的区别

更多内容请查阅分类专栏:MyBatis3源码深度解析

第九章主要学习:MyBatis插件原理及应用。主要内容包括:

  • MyBatis插件实现原理;
  • 自定义一个分页插件;
  • 自定义慢SQL统计插件。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1544059.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

git在单分支(自己分支)上的操作流程

文章目录 一、git命令整体操作流程&#xff08;了解&#xff09;二、idea中git操作流程&#xff08;常用-图文&#xff09;1、add2、commit&#xff0c;提交代码3、pull 拉取最新代码4、push 推送代码到远程仓库5、最后就可以在远程仓库中看你提交的代码了。 平时在idea中&…

MySQL数据库的下载及安装教程

MySQL是一个数据库管理系统&#xff0c;允许您管理关系数据库。它是Oracle支持的开源软件&#xff0c;本文介绍如何下载、安装和启动&#xff0c;便于新手快速启动学习之旅&#xff0c;具体如下&#xff1a; 一、下载MySQL 1.打开MySQL官网&#xff08;https://www.mysql.com…

第九届蓝桥杯大赛个人赛省赛(软件类)真题C 语言 A 组-第几个幸运数字

幸运数字是可以被3,5,7任一整除的数字&#xff0c;列举小明号码内的所有可能组合并计数。注意别忘了把1占的一位减去。 #include<stdio.h> typedef long long ll; int main(){long long ans 0, n 59084709587505LL;for(ll i 1; i < n; i * 3){//计算小于等于n的数…

面试笔记——Redis(分布式锁的使用场景及实现原理)

分布式锁的使用场景 资源竞争控制&#xff1a;多个客户端同时访问共享资源时&#xff0c;可以使用分布式锁来控制资源的并发访问&#xff0c;防止多个客户端同时对同一资源进行修改造成数据不一致的问题。 避免重复操作&#xff1a;在分布式环境中&#xff0c;可能会出现多个客…

rmvb是什么文件格式?rmvb格式怎么改成mp4?

RMVB&#xff0c;全称RealMedia Variable Bitrate&#xff0c;是由RealNetworks公司开发的一种视频文件格式。其产生背景可追溯至上世纪90年代&#xff0c;为了解决传输和存储上的挑战&#xff0c;RealNetworks公司致力于推出一种更为高效的解决方案。于是&#xff0c;RMVB问世…

还在用传统知识库?AI知识库才是企业的最优选择

在数字化和信息化日趋严重的时代&#xff0c;企业不仅要处理海量的数据&#xff0c;同时还要有效地管理和利用它们。这就使得知识库&#xff0c;作为一种集中存储、管理和共享知识资源的工具&#xff0c;被越来越多的企业所重视。然而&#xff0c;随着技术的快速迭代&#xff0…

Django之Celery篇(一)

一、介绍 Celery是由Python开发、简单、灵活、可靠的分布式任务队列,是一个处理异步任务的框架,其本质是生产者消费者模型,生产者发送任务到消息队列,消费者负责处理任务。 Celery侧重于实时操作,但对调度支持也很好,其每天可以处理数以百万计的任务。特点: 简单:熟悉…

ElasticSearch8 - 基本操作

前言 本文记录 ES 的一些基本操作&#xff0c;就是对官方文档的一些整理&#xff0c;按自己的习惯重新排版&#xff0c;凑合着看。官方的更详细&#xff0c;建议看官方的。 下文以 books 为索引名举例。 新增 添加单个文档 (没有索引会自动创建) POST books/_doc {"n…

消息队列八股

RabbitMQ 确保消息不丢失 重复消费问题 延迟队列 消息堆积 高可用 很少使用 Kafka 如何保证消息不丢失 回调接口保证生产者发送到brocker消息不丢失 保证消息顺序性 高可用机制 数据清理机制 实现高性能的设计

mysql - 缓存

缓存 InnoDB存储引擎在处理客户端的请求时&#xff0c;当需要访问某个页的数据时&#xff0c;就会把完整的页的数据全部加载到内存中&#xff0c;也就是说即使我们只需要访问一个页的一条记录&#xff0c;那也需要先把整个页的数据加载到内存中。将整个页加载到内存中后就可以…

学习vue3第十一节(依赖注入:provide/inject)

本机介绍&#xff1a;provide/inject 注意&#xff1a;大家在看此小节时候&#xff0c;默认大家已经了解一些组件的使用方法 1、依赖注入的用途&#xff1a; 当嵌套层级多的时候&#xff0c;某个子组件需要较远层级的父组件数据时候&#xff0c;如果我们依然使用props 传递数…

virtualbox导入vdi

新建虚拟机 点击新建 输入新建属性 配置cpu和内存 虚拟硬盘 这里选择已有的vdi文件 摘要 这里点击完成 虚拟机添加成功 点击启动&#xff0c;启动虚拟机 注意 这个时候的ip&#xff0c;还是以前镜像的ip&#xff0c;如果两个镜像一起启动&#xff0c;则需要修 改ip地…

802.1X网络访问控制协议

802.1X是一种由IEEE&#xff08;电气和电子工程师协会&#xff09;制定的网络访问控制协议&#xff0c;主要用于以太网和无线局域网&#xff08;WLAN&#xff09;中基于端口的网络接入控制。802.1X协议通过认证和授权机制&#xff0c;确保只有合法的用户和设备才能够接入网络&a…

Java中static、final关键字【详解】

文章目录 一、static关键字1.1 成员变量1.1.1 静态变量及其访问1.1.2 实例变量及其访问 1.2 成员方法1.2.1 静态方法及其访问1.2.2 实例方法及其访问 1.3 小结1.4 static应用知识 二、final关键字2.1 修饰类&#xff1a;不能被继承2.2 修饰方法2.3 修饰变量-局部变量2.3.1 局部…

背景减除(1)--bgslibrary Windows编译和使用

入侵监控领域中&#xff0c;在固定场景下&#xff0c;需要检测和监控的入侵物体种类繁多&#xff0c;无法具体穷尽。传统的CV算法提取的特征应用场景有限&#xff0c;无法完成大量物体的监控&#xff1b;深度学习目标检测方法没法收集到无穷无尽的物体种类&#xff0c;因此监督…

水牛社五大赚钱栏目概览:轻松了解项目核心与赚钱原理

很多新用户首次访问水牛社官网时&#xff0c;可能会感到有些迷茫。由于软件介绍相对较长&#xff0c;部分朋友可能缺乏耐心细读。然而&#xff0c;若您真心希望在网络上找到赚钱的机会&#xff0c;深入了解我们的发展历程将大有裨益。简而言之&#xff0c;本文旨在快速带您领略…

大数据基础:Linux基础详解

课程介绍 本课程主要通过对linux基础课程的详细讲解&#xff0c;让大家熟练虚拟机的安装使用&#xff0c;Linux系统的安装配置&#xff0c;学习掌握linux系统常用命令的使用&#xff0c;常用的软件安装方法&#xff0c;制作快照&#xff0c;克隆&#xff0c;完成免密登录&…

深度学习图像处理02:Tensor数据类型

上一讲深度学习图像处理01&#xff1a;图像的本质&#xff0c;我们了解到图像处理的本质是对矩阵的操作。这一讲&#xff0c;我们讲介绍深度学习图像处理的基本数据类型&#xff1a;Tensor类型。 在深度学习领域&#xff0c;Tensor是一种核心的数据结构&#xff0c;用于表示和…

蓝桥杯单片机快速开发笔记——特训2 按键的长按与短按

一、题目要求 在CT107D单片机综合训练平台上&#xff0c;通过I/O模式编写代码&#xff0c;实现以下功能&#xff1a; 系统上电后&#xff0c;关闭蜂鸣器、继电器和全部指示灯&#xff0c;数码管显示初始值为28&#xff0c;仅显示数码管最右边两位。利用定时器0实现10ms间隔定…

代码随想录第20天| 654.最大二叉树 617.合并二叉树

654.最大二叉树 654. 最大二叉树 - 力扣&#xff08;LeetCode&#xff09; 代码随想录 (programmercarl.com) 又是构造二叉树&#xff0c;又有很多坑&#xff01;| LeetCode&#xff1a;654.最大二叉树_哔哩哔哩_bilibili 给定一个不重复的整数数组 nums 。 最大二叉树 可以…