Mybatis占位符#{}和${}的区别?源码解读(二)
本文针对笔者日常开发中对Mybatis占位符 {}和${}使用时机结合源码,思考总结而来
Mybatis 作为国内开发中常用到的半自动 orm 框架,相信大家都很熟悉,它提供了简单灵活的xml映射配置,方便开发人员编写简单、复杂SQL,在国内互联网公司使用众多。
本文针对笔者日常开发中对 Mybatis
占位符 #{}
和 ${}
使用时机结合源码,思考总结而来
Mybatis
版本 3.5.11Spring boot
版本 3.0.2mybatis-spring
版本 3.0.1github地址:https://github.com/wayn111, 欢迎大家关注,点个starmybatis-spring
解析xml文件流程图Spring
项目启动时,mybatis-spring
自动初始化解析xml文件核心流程
【资料图】
Mybatis
在 buildSqlSessionFactory()
会遍历所有 mapperLocations(xml文件)
调用 xmlMapperBuilder.parse()
解析,源码如下
在 parse() 方法中, Mybatis
通过 configurationElement(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()
方法最后
原来 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.apply(context)); return true; }}
SqlNode 是一个接口,有10个实现类如下
可以看出我们的 select、insert、update、delete
标签中包含的各个文本(包含占位符 #{} 和 ${})、子标签都有对应的 SqlNode 实现类,后续运行中, Mybatis
对于 select、insert、update、delete
标签的 sql 语句处理都与这里的 SqlNode 各个实现类相关。自此我们 mybatis-spring
初始化流程中相关的重要代码都过了一遍。
#{}
和 ${}
的处理这里直接给出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、update、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; }
${}
占位符处理在 rootSqlNode.apply(context) -> MixedSqlNode.apply(DynamicContext context)
中会将 SqlNode 集合拼接成实际要执行的 sql 语句
保存在 DynamicContext 对象中。这里给出 SqlNode 集合的调试截图
可以看出我们的 ${}
占位符文本的 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 注入风险
#{}
占位符处理#{}
占位符文本的 SqlNode 实现类为 StaticTextSqlNode,查看源码
public class StaticTextSqlNode implements SqlNode { private final String text; public StaticTextSqlNode(String text) { this.text = text; } @Override 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_orderorder by create_time desclimit #{start},#{limit}
Mybatis
会通过上面提到 getBoundSql(Object parameterObject)
方法中的
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注入风险,最好避免由客户端传入此参数。
本文针对笔者日常开发中对Mybatis占位符 {}和${}使用时机结合源码,思考总结而来
关于最近【党的二十大的主题】事件在360热搜的热度非常高,关注要排名的很多网友同样也在关注【党的二十大的主题】这个事件,小编通过我们的百
2月10日,北京钓鱼台国宾馆。
幽默搞笑喝酒的句子优选32句1 成熟不是心变老,而是眼泪在眼里打转却还保持微笑。2 今年光棍节你还是一个人吗?不然是一条狗吗。3 双11已经到了
泥头车什么梗,泥头车也叫自卸车,这种车由于可以自卸,所以装的基本上是一些不易或不怕摔坏的东西,多用来装土方,弄的浑身上下都是泥。所以
天振股份近期接受机构调研时表示,“年产2500万平方米新型无机材料复合地板智能化生产线项目”(越南项目)基本完成并已投产
文 视频 华声在线全媒体记者叶竹“亲子游的卡还没正式开通,轮滑课程交了钱鞋子都没领,2家店现在都不开门营业了,也没给退
相信目前很多小伙伴对于蒙嘉慧都比较感兴趣,那么小搜今天在网上也是收集了一些与蒙嘉慧相关的信息来分享给大家,希望能够帮助到
烽火战国怎么点亮图标,烽火战国怎么点亮图标?点亮烽火战国的图标需要官阶等级达到官大夫!!而达到官大夫需要2000点声望!!所以,点亮图标就
前男友把送他的礼物送别人了,他把你当成礼物送给他的,他会想起你,这样他会更加开心,也会更加开心。你是一个好女孩,送给普通
烟组个词语,烟组词举例如下:1、烟雨:指像烟雾那样的细雨,如诗如梦。出处:宋代鲍照《观漏赋》:“聊弭志以高歌,顺烟雨而沉逸。”2、烟柳
女生节礼物选什么好,关于的女生节小礼品,礼物还挺多。就送他一个好看的礼物,他一定会满意。收小礼品的,看起来轻描淡写,背地
开心签名(一)谢谢那些明明知道我不好却依旧陪在我身边的人。幻想着你的出现,心脏迫不及待的又加快了跳动的频率。你一句我记好
1、你好!!!所谓区间测速是在同一路段上布设两个相邻的监控点,基于车辆通过前后两个监控点的时间来计算车辆在该路段上的平均
各路明星出名以后自己孩子也会同时受到关注,不过为了不必要的危险,为了保护自己孩子的隐私,一般明星都会选择对自己孩子的信息
金普烟花晚会男孩走散,警察叔叔“手拉手”帮忙找妈妈
品玩2月9日讯,据9to5google报道,谷歌AR和VR业务的负责人ClayBavor即将在下个月从谷歌离职。Bavor从2005年便已经开始在谷歌工作,2013年——Z
2月9日消息,据媒体报道,记者从接近小米人士处获悉,小米在ChatGPT领域有丰富落地场景,包括小爱对话、机器人(行情300024,诊股)等,其中小爱
1、气体的速度方向实际上与飞机的方向相同,这话没错。2、因为喷气式飞机飞行速度是800m s,气体相对于飞机是向后飞行。3、若飞机与气体的相对
金投白银网提供宣统三年大清银币价格(2023年02月09日),清代宣统三年银币最新消息(2023年02月09日)。