Mybatis 作为国内开发中常用到的半自动 orm 框架,相信大家都很熟悉,它提供了简单灵活的xml映射配置,方便开发人员编写简单、复杂SQL,在国内互联网公司使用众多。
本文针对笔者日常开发中对 Mybatis 占位符 #{} 和 ${} 使用时机结合源码,思考总结而来
Mybatis 版本 3.5.11Spring boot 版本 3.0.2mybatis-spring 版本 3.0.1mybatis-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<String, XNode> 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<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection")
.conflictMessageProducer((savedValue, targetValue) ->
". please check " + savedValue.getResource() + " and " + targetValue.getResource());
protected final Map<String, Cache> caches = new StrictMap<>("Caches collection");
protected final Map<String, ResultMap> resultMaps = new StrictMap<>("Result Maps collection");
protected final Map<String, ParameterMap> parameterMaps = new StrictMap<>("Parameter Maps collection");
protected final Map<String, KeyGenerator> keyGenerators = new StrictMap<>("Key Generators collection");
protected final Set<String> loadedResources = new HashSet<>();
protected final Map<String, XNode> 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<XNode> 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<SqlNode> contents;
public MixedSqlNode(List<SqlNode> 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文件查询方法标签内容
<select id="findNewBeeMallOrderList" parameterType="Map" resultMap="BaseResultMap">
select
<include refid="Base_Column_List"/>
from tb_newbee_mall_order
<where>
<if test="orderNo!=null and orderNo!=''">
and order_no = #{orderNo}
</if>
<if test="userId!=null and userId!=''">
and user_id = #{userId}
</if>
<if test="payType!=null and payType!=''">
and pay_type = #{payType}
</if>
<if test="orderStatus!=null and orderStatus!=''">
and order_status = #{orderStatus}
</if>
<if test="isDeleted!=null and isDeleted!=''">
and is_deleted = #{isDeleted}
</if>
<if test="startTime != null and startTime.trim() != ''">
and create_time > #{startTime}
</if>
<if test="endTime != null and endTime.trim() != ''">
and create_time < #{endTime}
</if>
</where>
<if test="sortField!=null and order!=null">
order by ${sortField} ${order}
</if>
<if test="start!=null and limit!=null">
limit #{start},#{limit}
</if>
</select>
运行时 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<String, Object> 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_order
order by create_time desc
limit #{start},#{limit}
Mybatis 会通过上面提到 getBoundSql(Object parameterObject) 方法中的
sqlSourceParser.parse() 方法完成 #{} 占位符的处理,代码如下:
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> 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
请帮助我理解范围运算符...和..之间的区别,作为Ruby中使用的“触发器”。这是PragmaticProgrammersguidetoRuby中的一个示例:a=(11..20).collect{|i|(i%4==0)..(i%3==0)?i:nil}返回:[nil,12,nil,nil,nil,16,17,18,nil,20]还有:a=(11..20).collect{|i|(i%4==0)...(i%3==0)?i:nil}返回:[nil,12,13,14,15,16,17,18,nil,20] 最佳答案 触发器(又名f/f)是
我正在检查一个Rails项目。在ERubyHTML模板页面上,我看到了这样几行:我不明白为什么不这样写:在这种情况下,||=和ifnil?有什么区别? 最佳答案 在这种特殊情况下没有区别,但可能是出于习惯。每当我看到nil?被使用时,它几乎总是使用不当。在Ruby中,很少有东西在逻辑上是假的,只有文字false和nil是。这意味着像if(!x.nil?)这样的代码几乎总是更好地表示为if(x)除非期望x可能是文字false。我会将其切换为||=false,因为它具有相同的结果,但这在很大程度上取决于偏好。唯一的缺点是赋值会在每次运行
我正在阅读一本关于Ruby的书,作者在编写类初始化定义时使用的形式与他在本书前几节中使用的形式略有不同。它看起来像这样:classTicketattr_accessor:venue,:datedefinitialize(venue,date)self.venue=venueself.date=dateendend在本书的前几节中,它的定义如下:classTicketattr_accessor:venue,:datedefinitialize(venue,date)@venue=venue@date=dateendend在第一个示例中使用setter方法与在第二个示例中使用实例变量之间是
一、引擎主循环UE版本:4.27一、引擎主循环的位置:Launch.cpp:GuardedMain函数二、、GuardedMain函数执行逻辑:1、EnginePreInit:加载大多数模块int32ErrorLevel=EnginePreInit(CmdLine);PreInit模块加载顺序:模块加载过程:(1)注册模块中定义的UObject,同时为每个类构造一个类默认对象(CDO,记录类的默认状态,作为模板用于子类实例创建)(2)调用模块的StartUpModule方法2、FEngineLoop::Init()1、检查Engine的配置文件找出使用了哪一个GameEngine类(UGame
转自:spring.profiles.active和spring.profiles.include的使用及区别说明下文笔者讲述spring.profiles.active和spring.profiles.include的区别简介说明,如下所示我们都知道,在日常开发中,开发|测试|生产环境都拥有不同的配置信息如:jdbc地址、ip、端口等此时为了避免每次都修改全部信息,我们则可以采用以上的属性处理此类异常spring.profiles.active属性例:配置文件,可使用以下方式定义application-${profile}.properties开发环境配置文件:application-dev
打印1:defsum(i)i=i+[2]end$x=[1]sum($x)print$x打印12:defsum(i)i.push(2)end$x=[1]sum($x)print$x后者是修改全局变量$x。为什么它在第二个例子中被修改而不是在第一个例子中?类Array的任何方法(不仅是push)都会发生这种情况吗? 最佳答案 变量范围在这里无关紧要。在第一段代码中,您仅使用赋值运算符=为变量i赋值,而在第二段代码中,您正在修改$x(也称为i)使用破坏性方法push。赋值从不修改任何对象。它只是提供一个名称来引用一个对象。方法要么是破坏性
Ruby中的Fixnum方法.next和.succ有什么区别?看起来它的工作原理是一样的:1.next=>21.succ=>2如果有什么不同,为什么有两种方法做同样的事情? 最佳答案 它们是等价的。Fixnum#succ只是Fixnum#next的同义词。他们甚至在thereferencemanual中共享同一block. 关于ruby-Ruby中.next和.succ的区别,我们在StackOverflow上找到一个类似的问题: https://stacko
我明白了defa(&block)block.call(self)end和defa()yieldselfend导致相同的结果,如果我假设有这样一个blocka{}。我的问题是-因为我偶然发现了一些这样的代码,它是否有任何区别或者是否有任何优势(如果我不使用变量/引用block):defa(&block)yieldselfend这是一个我不理解&block用法的具体案例:defrule(code,name,&block)@rules=[]if@rules.nil?@rules 最佳答案 我能想到的唯一优点就是自省(introspecti
由于匿名block和散列block看起来大致相同。我正在玩它。我做了一些严肃的观察,如下所示:{}.class#=>Hash好的,这很酷。空block被视为Hash。print{}.class#=>NilClassputs{}.class#=>NilClass为什么上面的代码和NilClass一样,下面的代码又显示了Hash?puts({}.class)#Hash#=>nilprint({}.class)#Hash=>nil谁能帮我理解上面发生了什么?我完全不同意@Lindydancer的观点你如何解释下面几行:print{}.class#NilClassprint[].class#A
在Ruby中,我试图理解to_enum和enum_for方法。在我提出问题之前,我提供了一些示例代码和两个示例来帮助理解上下文。示例代码:#replicatesgroup_bymethodonArrayclassclassArraydefgroup_by2(&input_block)returnself.enum_for(:group_by2)unlessblock_given?hash=Hash.new{|h,k|h[k]=[]}self.each{|e|hash[input_block.call(e)]示例#1:irb(main)>puts[1,2,3].group_by2.ins