热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Mybatis占位符#和$的区别?源码解读

原标题:Mybatis占位符#和$的区别?源码解读Mybatis作为国内开发中常用到的半自动orm框架,相信大家都很熟悉,它提供了简单灵活的xml映射

原标题:Mybatis占位符#和$的区别?源码解读

Mybatis 作为国内开发中常用到的半自动 orm 框架,相信大家都很熟悉,它提供了简单灵活的xml映射配置,方便开发人员编写简单、复杂SQL,在国内互联网公司使用众多。


本文针对笔者日常开发中对 Mybatis 占位符 #{}${} 使用时机结合源码,思考总结而来




  • Mybatis 版本 3.5.11


  • Spring boot 版本 3.0.2


  • mybatis-spring 版本 3.0.1

  • github地址:https://github.com/wayn111, 欢迎大家关注,点个star


一. 启动时,mybatis-spring 解析xml文件流程图

Spring 项目启动时,mybatis-spring 自动初始化解析xml文件核心流程
流程图 (2).jpg
MybatisbuildSqlSessionFactory() 会遍历所有 mapperLocations(xml文件) 调用 xmlMapperBuilder.parse()解析,源码如下
image.png
在 parse() 方法中, Mybatis 通过 configu文章来源站点https://www.yii666.com/rationElement(parser.evalNode("/mapper")) 方法解析xml文件中的各个标签

public class XMLMapperBuilder extends BaseBuilder {
...
private final MapperBuilderAssistant builderAssistant;
private final Map sqlFragments;
...

public void parse() {
if (!configuration.isResourceLoaded(resource)) {
// xml文件解析逻辑
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
private void configurationElement(XNode context) {
try {
// 解析xml文件内的namespace、cache-ref、cache、parameterMap、resultMap、sql、select、insert、update、delete等各种标签
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.isEmpty()) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
resultMapElements(context.evalNodes("/mapper/resultMap"));
sqlElement(context.evalNodes("/mapper/sql"));
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
}
}
}

最后会把 namespace、cache-ref、cache、parameterMap、resultMap、select、insert、update、delete等标签内容解析结果放到 builderAssistant 对象中,将sql标签解析结果放到sqlFragments对象中,其中 由于 builderAssistant 对象会保存select、insert、update、delete标签内容解析结果我们对 builderAssistant 对象进行深入了解

public class MapperBuilderAssistant extends BaseBuilder {
...
}
public abstract class BaseBuilder {
protected final Configuration configuration;
...
}
public class Configuration {
...
protected final Map mappedStatements = new StrictMap("Mapped Statements collection")
.conflictMessageProducer((savedValue, targetValue) ->
". please check " + savedValue.getResource() + " and " + targetValue.getResource());
protected final Map caches = new StrictMap<>("Caches collection");
protected final Map resultMaps = new StrictMap<>("Result Maps collection");
protected final Map parameterMaps = new StrictMap<>("Parameter Maps collection");
protected final Map keyGenerators = new StrictMap<>("Key Generators collection");
protected final Set loadedResources = new HashSet<>();
protected final Map sqlFragments = new StrictMap<>("XML fragments parsed from previous mappers");
...
}

builderAssistant 对象继承至 BaseBuilder,BaseBuilder 类中包含一个 configuration 对象属性, configuration 对象中会保存xml文件标签解析结果至自身对应属性mappedStatements、caches、resultMaps、sqlFragments

这里有个问题上面提到的sql标签结果会放到 XMLMapperBuilder 类的 sqlFragments 对象中,为什么 Configuration 类中也有个 sqlFragments 属性?

这里回看上文 buildSqlSessionFactory() 方法最后
image.png
原来 XMLMapperBuilder 类中的 sqlFragments 属性就来自Configuration类?

回到主题,在 buildStatementFromContext(context.evalNodes("select|insert|update|delete")) 方法中会通过如下调用

buildStatementFromContext(List list, String requiredDatabaseId)
-> parseStatementNode()
-> createSqlSource(Configuration configuration, XNode script, Class parameterType)
-> parseScriptNode()
-> parseDynamicTags(context)

最后通过parseDynamicTags(context) 方法解析 select、insert、update、delete 标签内容将结果保存在 MixedSqlNode 对象中的 SqlNode 集合中

public class MixedSqlNode implements SqlNode {
private final List contents;
public MixedSqlNode(List contents) {
this.cOntents= contents;
}
@Override
public boolean apply(DynamicContext context) {
contents.forEach(node -> node.awww.yii666.compply(context));
return true;
}
}

SqlNode 是一个接口,有10个实现类如下
image.png
可以看出我们的 select、insert、update、delete 标签中包含的各个文本(包含占位符 #{} 和 ${})、子标签都有对应的 SqlNode 实现类,后续运行中, Mybatis 对于 select、insert、update、delete 标签的 sql 语句处理都与这里的 SqlNode 各个实现类相关。自此我们 mybatis-spring 初始化流程中相关的重要代码都过了一遍。


二. 运行中,sql语句占位符 #{}${} 的处理

这里直接给出xml文件查询方法标签内容


运行时 Mybatis 动态代理 MapperProxy 对象的调用流程,如下:

-> newBeeMallOrderMapper.findNewBeeMallOrderList(pageUtil);
-> MapperProxy.invoke(Object proxy, Method method, Object[] args)
-> MapperProxy.invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession)
-> MapperMethod.execute(SqlSession sqlSession, Object[] args)
-> MapperMethod.executeForMany(SqlSession sqlSession, Object[] args)
-> SqlSessionTemplate.selectList(String statement, Object parameter)
-> SqlSessionInterceptor.invoke(Object proxy, Method method, Object[] args)
-> DefaultSqlSession.selectList(String statement, Object parameter)
-> DefaultSqlSession.selectList(String statement, Object parameter, RowBounds rowBounds)
-> DefaultSqlSession.selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler)
-> CachingExecutor.query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler)
-> MappedStatement.getBoundSql(Object parameterObject)
-> DynamicSqlSource.getBoundSql(Object parameterObject)
-> MixedSqlNode.apply(DynamicContext context) // ${} 占位符处理
-> SqlSourceBuilder.parse(String originalSql, Class parameterType, Map additionalParameters) // #{} 占位符处理

Mybatis 通过 DynamicSqlSource.getBoundSql(Object parameterObject) 方法对 select、insert、www.yii666.comupdate、delete 标签内容做 sql 转换处理,代码如下:

@Override
public BoundSql getBoundSql(Object parameterObject) {
DynamicContext cOntext= new DynamicContext(configuration, parameterObject);
rootSqlNode.apply(context);
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
context.getBindings().forEach(boundSql::setAdditionalParameter);
return boundSql;
}

2.1 ${} 占位符处理

rootSqlNode.apply(context) -> MixedSqlNode.apply(DynamicContext context) 中会将 SqlNode 集合拼接成实际要执行的 sql 语句
保存在 DynamicContext 对象中。这里给出 SqlNode 集合的调试截图
image.png
可以看出我们的 ${} 占位符文本的 SqlNode 实现类为 TextSqlNode,apply方法相关操作如下

public class TextSqlNode implements SqlNode {
...
@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);
}
// 划重点,${}占位符替换逻辑在就handleToken(String content)方法中
@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);
}
Object value = OgnlCache.getValue(content, context.getBindings());
String srtValue = value == null ? "" : String.valueOf(value); // issue #274 return "" instead of "null"
checkInjection(srtValue);
return srtValue;
}
}
public class GenericTokenParser {
public String parse(String text) {
...
do {
...
if (end == -1) {
...
} else {
builder.append(handler.handleToken(expression.toString()));
offset = end + closeToken.length();
}
}
...
} while (start > -1);
...
return builder.toString();
}
}

划重点,${} 占位符处理如下

handleToken(String content) 方法中, Mybatis 会通过 ognl 表达式将 ${} 的结果直接拼接在 sql 语句中,由此我们得知 ${} 占位符拼接的字段就是我们传入的原样字段,有着 Sql 注入风险


2.2 #{} 占位符处理

#{} 占位符文本的 SqlNode 实现类为 StaticTextSqlNode,查看源码

public class StaticTextSqlNode implements SqlNode {
private final String text;
public StaticTextSqlNode(String text) {
this.text = text;
}
@Overri文章来源地址193594.htmlde
public boolean apply(DynamicContext context) {
context.appendSql(text);
return true;
}
}

StaticTextSqlNode 会直接将节点内容拼接在 sql 语句中,也就是说在 rootSqlNode.apply(context) 方法执行完毕后,此时的 sql 语句如下

select order_id, order_no, user_id, total_price,
pay_status, pay_type, pay_time, order_status,
extra_info, user_name, user_phone, user_address,
is_deleted, create_time, update_time
from tb_newbee_mall_order
order by create_time desc
limit #{start},#{limit}

Mybatis 会通过上面提到 getBoundSql(Object parameterObject) 方法中的
image.png
sqlSourceParser.parse() 方法完成 #{} 占位符的处理,代码如下:

public SqlSource parse(String originalSql, Class parameterType, Map additionalParameters) {
ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
String sql;
if (configuration.isShrinkWhitespacesInSql()) {
sql = parser.parse(removeExtraWhitespaces(originalSql));
} else {
sql = parser.parse(originalSql);
}
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}

看到了熟悉的 #{ 占位符没有,哈哈?, Mybatis 对于 #{} 占位符的处理就在 GenericTokenParser类的 parse() 方法中,代码如下:

public class GenericTokenParser {
public String parse(String text) {
...
do {
...
if (end == -1) {
...
} else {
builder.append(handler.handleToken(expression.toString()));
offset = end + closeToken.length();
}
}
...
} while (start > -1);
...
return builder.toString();
}
}
public class SqlSourceBuilder extends BaseBuilder {
...
// 划重点,#{}占位符替换逻辑在就SqlSourceBuilder.handleToken(String content)方法中
@Override
public String handleToken(String content) {
parameterMappings.add(buildParameterMapping(content));
return "?";
}
}

划重点,#{} 占位符处理如下

handleToken(String content) 方法中, Mybatis 会直接将我们的传入参数转换成问号(就是 jdbc 规范中的问号),也就是说我们的 sql 语句是预处理的。能够避免 sql 注入问题


三. 总结

由上经过源码分析,我们知道 Mybatis#{} 占位符是直接转换成问号,拼接预处理 sql。 ${} 占位符是原样拼接处理,有sql注入风险,最好避免由客户端传入此参数。


源码阅读不易,欢迎大家点赞,转发,评论,加我的qq技术讨论群:862416125


来源于:Mybatis占位符#和$的区别?源码解读


推荐阅读
  • 在springmvc框架中,前台ajax调用方法,对图片批量下载,如何弹出提示保存位置选框?Controller方法 ... [详细]
  • 使用Ubuntu中的Python获取浏览器历史记录原文: ... [详细]
  • 如何实现织梦DedeCms全站伪静态
    本文介绍了如何通过修改织梦DedeCms源代码来实现全站伪静态,以提高管理和SEO效果。全站伪静态可以避免重复URL的问题,同时通过使用mod_rewrite伪静态模块和.htaccess正则表达式,可以更好地适应搜索引擎的需求。文章还提到了一些相关的技术和工具,如Ubuntu、qt编程、tomcat端口、爬虫、php request根目录等。 ... [详细]
  • 本文介绍了在开发Android新闻App时,搭建本地服务器的步骤。通过使用XAMPP软件,可以一键式搭建起开发环境,包括Apache、MySQL、PHP、PERL。在本地服务器上新建数据库和表,并设置相应的属性。最后,给出了创建new表的SQL语句。这个教程适合初学者参考。 ... [详细]
  • 云原生边缘计算之KubeEdge简介及功能特点
    本文介绍了云原生边缘计算中的KubeEdge系统,该系统是一个开源系统,用于将容器化应用程序编排功能扩展到Edge的主机。它基于Kubernetes构建,并为网络应用程序提供基础架构支持。同时,KubeEdge具有离线模式、基于Kubernetes的节点、群集、应用程序和设备管理、资源优化等特点。此外,KubeEdge还支持跨平台工作,在私有、公共和混合云中都可以运行。同时,KubeEdge还提供数据管理和数据分析管道引擎的支持。最后,本文还介绍了KubeEdge系统生成证书的方法。 ... [详细]
  • 学习笔记(34):第三阶段4.2.6:SpringCloud Config配置中心的应用与原理第三阶段4.2.6SpringCloud Config配置中心的应用与原理
    立即学习:https:edu.csdn.netcourseplay29983432482?utm_sourceblogtoedu配置中心得核心逻辑springcloudconfi ... [详细]
  • 本文详细介绍了PHP中与URL处理相关的三个函数:http_build_query、parse_str和查询字符串的解析。通过示例和语法说明,讲解了这些函数的使用方法和作用,帮助读者更好地理解和应用。 ... [详细]
  • 如何用UE4制作2D游戏文档——计算篇
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了如何用UE4制作2D游戏文档——计算篇相关的知识,希望对你有一定的参考价值。 ... [详细]
  • 本文由编程笔记小编整理,介绍了PHP中的MySQL函数库及其常用函数,包括mysql_connect、mysql_error、mysql_select_db、mysql_query、mysql_affected_row、mysql_close等。希望对读者有一定的参考价值。 ... [详细]
  • 推荐系统遇上深度学习(十七)详解推荐系统中的常用评测指标
    原创:石晓文小小挖掘机2018-06-18笔者是一个痴迷于挖掘数据中的价值的学习人,希望在平日的工作学习中,挖掘数据的价值, ... [详细]
  • Oracle分析函数first_value()和last_value()的用法及原理
    本文介绍了Oracle分析函数first_value()和last_value()的用法和原理,以及在查询销售记录日期和部门中的应用。通过示例和解释,详细说明了first_value()和last_value()的功能和不同之处。同时,对于last_value()的结果出现不一样的情况进行了解释,并提供了理解last_value()默认统计范围的方法。该文对于使用Oracle分析函数的开发人员和数据库管理员具有参考价值。 ... [详细]
  • 【MicroServices】【Arduino】装修甲醛检测,ArduinoDart甲醛、PM2.5、温湿度、光照传感器等,数据记录于SD卡,Python数据显示,UI5前台,微服务后台……
    这篇文章介绍了一个基于Arduino的装修甲醛检测项目,使用了ArduinoDart甲醛、PM2.5、温湿度、光照传感器等硬件,并将数据记录于SD卡,使用Python进行数据显示,使用UI5进行前台设计,使用微服务进行后台开发。该项目还在不断更新中,有兴趣的可以关注作者的博客和GitHub。 ... [详细]
  • Java验证码——kaptcha的使用配置及样式
    本文介绍了如何使用kaptcha库来实现Java验证码的配置和样式设置,包括pom.xml的依赖配置和web.xml中servlet的配置。 ... [详细]
  • imx6ull开发板驱动MT7601U无线网卡的方法和步骤详解
    本文详细介绍了在imx6ull开发板上驱动MT7601U无线网卡的方法和步骤。首先介绍了开发环境和硬件平台,然后说明了MT7601U驱动已经集成在linux内核的linux-4.x.x/drivers/net/wireless/mediatek/mt7601u文件中。接着介绍了移植mt7601u驱动的过程,包括编译内核和配置设备驱动。最后,列举了关键词和相关信息供读者参考。 ... [详细]
  • 本文介绍了在多平台下进行条件编译的必要性,以及具体的实现方法。通过示例代码展示了如何使用条件编译来实现不同平台的功能。最后总结了只要接口相同,不同平台下的编译运行结果也会相同。 ... [详细]
author-avatar
快乐浪迹天涯海绵宝宝_243
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有