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

文章目录

    • 前言
    • 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.XMLScriptBuilderprivate 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.XMLScriptBuilderprotected 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属性设置为truecontents.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);// 元素节点一定是动态SQLisDynamic = 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.TextSqlNodepublic 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.XMLScriptBuilderprivate 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.XMLScriptBuilderprivate class IfHandler implements NodeHandler {public IfHandler() {// Prevent Synthetic Access}@Overridepublic 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.XMLScriptBuilderpublic 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.MappedStatementprivate 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.DynamicSqlSourcepublic class DynamicSqlSource implements SqlSource {private final Configuration configuration;private final SqlNode rootSqlNode;public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {this.configuration = configuration;this.rootSqlNode = rootSqlNode;}@Overridepublic 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.SqlSourceBuilderpublic 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.GenericTokenParserpublic 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) {// 没有找到结束字符,则把后面的字符全加入到builderbuilder.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.SqlSourceBuilderprivate static class ParameterMappingTokenHandler extends BaseBuilder implements TokenHandler {@Overridepublic 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.TextSqlNodeprivate static class BindingTokenParser implements TokenHandler {@Overridepublic 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.mzph.cn/news/770102.shtml

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

相关文章

AAC相关知识

一、AAC音频格式种类有哪些 AAC音频格式是一种由MPEG-4标准定义的有损音频压缩格式。AAC包含两种封装格式 ADIF&#xff08;Audio Data Interchange Format音频数据交换格式&#xff09;和ADTS&#xff08;Audio Data transport Stream音频数据传输流&#xff09;。 ADIF 特点…

promethus的安装使用

1、# 软件下载地址 https://prometheus.io/download/ https://grafana.com/grafana/download https://prometheus.io/download/ Prometheus是一套开源的监控&报警&时间序列数据库的组合,起始是由SoundCloud公司开发的。 Prometheus 的优点 1、非常少的外部依赖,安装…

OceanBase4.2.2.1 单机集群在ArmX86安装(自测记录)

OceanBase OceanBase就不必多加介绍了&#xff0c;本次主要是分享对于它的安装使用&#xff0c;先说说背景&#xff0c;首先接触是因为信创国产化的要求&#xff0c;为满足支持国产化&#xff0c;安装了Arm架构下版本4.0.0&#xff0c;满足支持通过。后来项目实际使用&#xff…

由浅到深认识Java语言(20):包装类

该文章Github地址&#xff1a;https://github.com/AntonyCheng/java-notes 在此介绍一下作者开源的SpringBoot项目初始化模板&#xff08;Github仓库地址&#xff1a;https://github.com/AntonyCheng/spring-boot-init-template & CSDN文章地址&#xff1a;https://blog.c…

leetcode刷题日记-外观数组

题目描述 解题思路 初始化字符串 init 为 “1”&#xff0c;作为外观数列的第一项。 通过循环迭代生成外观数列的下一项&#xff0c;循环次数为 n-1&#xff0c;因为已经初始化了第一项。 在每次迭代中&#xff0c;通过两个指针 pos 和 start 来遍历当前项 init&#xff0c;po…

C++中的枚举类型

C中的enum&#xff08;枚举&#xff09;类型是一种用户定义的类型&#xff0c;用于表示一组整数值&#xff0c;每个值都有对应的名称&#xff0c;增强了代码的可读性和可维护性。 1. 基本枚举类型 基本的枚举类型定义了一组命名的整数常量。 enum Color {RED,GREEN,BLUE };C…

创业之路:探索如何在Facebook上创业成功

引言 在当今数字化时代&#xff0c;社交媒体已成为创业者们开展业务、建立品牌和推广产品的重要平台之一。作为全球最大的社交媒体平台之一&#xff0c;Facebook为创业者提供了丰富的机会和资源。本文将探讨如何在Facebook上创业成功的关键因素和实践方法&#xff0c;帮助创业…

第九届蓝桥杯大赛个人赛省赛(软件类)真题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的数…

计算机程序的编译和链接

c语言中的小小白-CSDN博客c语言中的小小白关注算法,c,c语言,贪心算法,链表,mysql,动态规划,后端,线性回归,数据结构,排序算法领域.https://blog.csdn.net/bhbcdxb123?spm1001.2014.3001.5343 给大家分享一句我很喜欢我话&#xff1a; 知不足而奋进&#xff0c;望远山而前行&am…

下载网页上的在线视频 网络视频 视频插件下载

只需要在浏览器上安装一个插件&#xff0c;就可以下载大部分的视频文件&#xff0c;几秒到一两个小时的视频&#xff0c;基本都不是问题。详细解决如下&#xff1a; 0、因为工作需要&#xff0c;需要获取某网站上的宣传视频&#xff0c;我像往常一样&#xff0c;查看视频的url…

Schemdraw小白从入门到放弃---原理工具书

文章目录 序版本最简单的例子一、总体思路二、元件2.1 color习题 2.2 label2.3 length 三、元件的连接3.1 延续性习题 3.2 方向习题 3.3 接线点习题3.3.1 默认激活anchor与沉默anchor3.3.2 切换鼠标焦点机制3.3.2.1 at函数规定元件的start接在哪个anchor上3.3.2.2 to函数规定元…

重磅!一起做个淘宝的简易布局!(超详细)

你好&#xff0c;我是云桃桃。 一个希望帮助更多朋友快速入门 WEB 前端程序媛。 因为之前的学习内容&#xff0c;今天&#xff0c;我们可以来综合运用一下标签和 CSS 样式&#xff0c;做一个简易的淘宝网页大体布局了&#xff0c;如图。 咱们今天要做成这样子&#xff01; 里面…

4.1 RK3399项目开发实录-案例开发之MIPI 摄像头开发(wulianjishu666)

嵌入式从零到项目开发全套例程资料 链接&#xff1a;https://pan.baidu.com/s/1ksCQN__jD8ZrJhw8sWzhwQ?pwdvvfz 3.2. MIPI 摄像头 带有 MIPI CSI 接口的 RK3399 板子都添加了双 MIPI 摄像头 OV13850 的支持&#xff0c;应用中也添加了摄像头的例子。下面介绍一下相关配置。…

蓝桥杯(2):python基础算法【上】

时间复杂度、枚举、模拟、递归、进制转换、前缀和、差分、离散化 1 时间复杂度 重要是看循环&#xff0c;一共运行了几次 1.1 简单代码看循环 #时间复杂度1 n int(input()) for i in range(1,n1):for j in range(0,i):pass ###时间复杂度&#xff1a;123....nn(1n)/2 所以…

2024 ccfcsp认证打卡 2023 09 02 坐标变换(其二)

202309-2 坐标变换&#xff08;其二&#xff09; 题解1题解2区别第一种算法&#xff08;使用ArrayList存储操作序列&#xff09;&#xff1a;数据结构&#xff1a;操作序列处理&#xff1a; 第二种算法&#xff08;使用两个数组存储累积结果&#xff09;&#xff1a;数据结构&a…

Unable to authenticate, need: BASIC realm=“Sonatype Nexus Repository Manager“

问题 使用公司的私有源&#xff0c;执行 npm i 的时候突然报错了&#xff1a; 解决 执行命令 npm config list找到 .npmrc 去掉对应的这一行即可&#xff0c;或者使用 nrm 工具执行 nrm del xxx 删掉私有源&#xff0c;然后在添加私有源也可。可以参考我这篇&#xff1a;使…

(一)基于IDEA的JAVA基础8

使用多重if选择结构 多个if条件进行判断: 语法: if(条件1){ 执行语句1&#xff1b; }else if(条件2){ 执行语句2&#xff1b; }else if(条件3){ 执行语句3&#xff1b; }else if (条件4)…… 流程图: 我们来写个好玩的&#xff0c;对暗号: public class Test01 { …

五分钟快速搭建个人游戏网站(1Panel)

五分钟快速搭建个人游戏网站&#xff08;1Panel&#xff09; 环境要求&#xff1a;主流 Linux 发行版本&#xff08;基于 Debian / RedHat&#xff0c;包括国产操作系统&#xff09;&#xff1b; 如果是Windows OS的可以通过WSL来实现安装。 1 介绍 1Panel 是一个基于 Web 的 L…

结构体讲解

目录 一.结构体类型的声明 (1)结构体的声明 (2)结构体的创建和初始化 (3)匿名结构体 (4)结构体的自引用 二.结构体内存对齐 (1)对齐规则 (2)为什么存在内存对齐&#xff1f; (3)结构体传参 三.结构体实现位段 (1)什么是位段 (2)位段的内存分配 (3)位段的跨平…

电脑桌面便签,怎么在电脑桌面上设置便签

在数字化时代&#xff0c;电脑已成为我们日常生活不可或缺的一部分。在我们使用电脑进行各种工作和学习的过程中&#xff0c;经常会遇到需要记录临时信息或提醒自己的情况。这时&#xff0c;设置便签在电脑桌面上就成为了一种非常便捷的方法。那么有一个问题&#xff0c;电脑桌…