启动项目时,与SQL相关的逻辑
启动项目后,执行CURD方法时,与SQL相关的逻辑
六问Mybatis插件
其中,key
为{全类名}.{方法名}
,如:com.aspire.ssm.mapper.SqlTestMapper.selectAll,value
为该方法对应的MappedStatement对象
。
注:其实同一个方法,同一个value,会存两次,一个长key(如上),一个短key(短key,只有方法名)。
注:对应源码可详见:org.apache.ibatis.builder.annotation.MapperAnnotationBuilder#parseStatement
。
先解析出xml中的SQL对应的MappedStatement实例对象
,可详见源码:org.apache.ibatis.builder.xml.XMLStatementBuilder#parseStatementNode
。"main@1" prio=5 tid=0x1 nid=NA runnable
java.lang.Thread.State: RUNNABLE
at org.apache.ibatis.session.Configuration.addMappedStatement(Configuration.java:686)
at org.apache.ibatis.builder.MapperBuilderAssistant.addMappedStatement(MapperBuilderAssistant.java:296)
at org.apache.ibatis.builder.xml.XMLStatementBuilder.parseStatementNode(XMLStatementBuilder.java:110)
at org.apache.ibatis.builder.xml.XMLMapperBuilder.buildStatementFromContext(XMLMapperBuilder.java:137)
at org.apache.ibatis.builder.xml.XMLMapperBuilder.buildStatementFromContext(XMLMapperBuilder.java:130)
at org.apache.ibatis.builder.xml.XMLMapperBuilder.configurationElement(XMLMapperBuilder.java:120)
at org.apache.ibatis.builder.xml.XMLMapperBuilder.parse(XMLMapperBuilder.java:94)
at org.apache.ibatis.builder.annotation.MapperAnnotationBuilder.loadXmlResource(MapperAnnotationBuilder.java:182)
at org.apache.ibatis.builder.annotation.MapperAnnotationBuilder.parse(MapperAnnotationBuilder.java:129)
at org.apache.ibatis.binding.MapperRegistry.addMapper(MapperRegistry.java:72)
at org.apache.ibatis.session.Configuration.addMapper(Configuration.java:759)
at org.mybatis.spring.mapper.MapperFactoryBean.checkDaoConfig(MapperFactoryBean.java:80)
at org.springframework.dao.support.DaoSupport.afterPropertiesSet(DaoSupport.java:44)
后解析出注解
(@Select、@Delete、@Update、@Insert、@SelectProvider、@DeleteProvider、@UpdateProvider、@InsertProvider)中的SQL对应的MappedStatement实例对象
,可详见源码:org.apache.ibatis.builder.annotation.MapperAnnotationBuilder.parseStatement
。具体的调用栈为:"main@1" prio=5 tid=0x1 nid=NA runnable
java.lang.Thread.State: RUNNABLE
at org.apache.ibatis.session.Configuration.addMappedStatement(Configuration.java:686)
at org.apache.ibatis.builder.MapperBuilderAssistant.addMappedStatement(MapperBuilderAssistant.java:296)
at org.apache.ibatis.builder.annotation.MapperAnnotationBuilder.parseStatement(MapperAnnotationBuilder.java:356)
at org.apache.ibatis.builder.annotation.MapperAnnotationBuilder.parse(MapperAnnotationBuilder.java:139)
at org.apache.ibatis.binding.MapperRegistry.addMapper(MapperRegistry.java:72)
at org.apache.ibatis.session.Configuration.addMapper(Configuration.java:759)
at org.mybatis.spring.mapper.MapperFactoryBean.checkDaoConfig(MapperFactoryBean.java:80)
at org.springframework.dao.support.DaoSupport.afterPropertiesSet(DaoSupport.java:44)
1.4.1、 RawSqlSource:内部持有了一个SqlSource,该SqlSource是StaticSqlSource实例。因为RawSqlSource在启动时就会计算出mapping(即:sql的最终的样子),所以其性能优于DynamicSqlSource。
普通的SQL,会被封装为RawSqlSource或者DynamicSqlSource
:
1.4.2、 DynamicSqlSource:动态SQL处理器,会处理${}、#{}等一系列SQL,最终处理完毕后,会以最终的SQL信息等为参数,new一个StaticSqlSource来作为最终查询时用的SqlSource。
除了${}占位的普通SQL外,动态SQL全都会被封装为DynamicSqlSource
:
1.4.3、 ProviderSqlSource:处理通过注解@InsertProvider、@DeleteProvider、@UpdateProvider、@SelectProvider写的SQL;ProviderSqlSource会转换为DynamicSqlSource或RawSqlSource,最终都会与StaticSqlSource关系起来。
1.4.4、 ProviderSqlSource:StaticSqlSource:静态的SqlSource,无论是RawSqlSource、DynamicSqlSource还是ProviderSqlSource,最终都会与StaticSqlSource关系起来。都会“转换”成StaticSqlSource。确切的说:RowSqlSource持有StaticSqlSource实例;DynamicSqlSource执行查询时,处理完动态SQL后会创建StaticSqlSource实例;ProviderSqlSource执行查询时,会转换为RowSqlSource或DynamicSqlSource。所以,MappedStatement#getBoundSql方法里面的sqlSource.getBoundSql(parameterObject),最终其实还是StaticSqlSource#getBoundSql
。
注:RowSqlSource与DynamicSqlSource的区别是:SQL的结构会不会因为程序或参数值而变动。RowSqlSource:不会。DynamicSqlSource:会。
注:在项目启动后,执行CURD时,ProviderSqlSource会被转换为RowSqlSource或DynamicSqlSource。
如:主动调用List selectAll_xml();方法进行查询。
即com.sun.proxy.$Proxy86.selectAll_xml
,实际上是由org.apache.ibatis.binding.MapperProxy.invoke
进行调用的。
首先,org.apache.ibatis.mapping.MappedStatement#getBoundSql:
注:MappedStatement#getBoundSql方法中,BoundSql boundSql = sqlSource.getBoundSql(parameterObject)返回的BoundSql 对象里面的SQL,是没有占位符#{xxx}的,原SQL中的#{xxx}会被?代替;MappedStatement#getBoundSql方法返回的BoundSql对象里面的SQL,也是没有占位符#{xxx}的,原SQL中的#{xxx}会被?代替。
注:MappedStatement#getBoundSql方法中,BoundSql boundSql = sqlSource.getBoundSql(parameterObject)返回的BoundSql 对象里面的SQL,是没有占位符${xxx}的,原SQL中的${xxx}会直接被具体的参数值代替MappedStatement#getBoundSql方法返回的BoundSql对象里面的SQL,也是没有占位符${xxx}的,原SQL中的${xxx}会直接被具体的参数值代替。
其次,我们知道在启动时,SQL信息被封装进的SqlSource实现只有RawSqlSource或DynamicSqlSource或ProviderSqlSource这三种,下面一次对他们进行分析。
2.6.1、 RawSqlSource#getBoundSql:
RawSqlSource#getBoundSql的调用方法栈为:
2.6.2、 DynamicSqlSource#getBoundSql:
2.6.2.1、 其中,org.apache.ibatis.scripting.xmltags.SqlNode#apply实现了动态SQL:
SqlNode接口有很多实现:
从这些类的名字就可看出,这些类的功能了。他们实现了对xml中if标签、choose标签、foreach等标签的动态判断处理。
2.6.2.3、 以IfSqlNode为例,讲解是如何实现动态SQL的:
"main@1" prio=5 tid=0x1 nid=NA runnable
java.lang.Thread.State: RUNNABLE
// 如果进一步跟踪, 会发现就是: 如果满足条件的话,就使用StringBuilder#append拼接SQL
at org.apache.ibatis.scripting.xmltags.DynamicContext.appendSql(DynamicContext.java:66)
at org.apache.ibatis.scripting.xmltags.StaticTextSqlNode.apply(StaticTextSqlNode.java:30)
at org.apache.ibatis.scripting.xmltags.MixedSqlNode.lambda$apply$0(MixedSqlNode.java:32)
at org.apache.ibatis.scripting.xmltags.MixedSqlNode$$Lambda$389.584643821.accept(Unknown Source:-1)
at java.util.ArrayList.forEach(ArrayList.java:1257)
// 这里实现了动态SQL
at org.apache.ibatis.scripting.xmltags.MixedSqlNode.apply(MixedSqlNode.java:32)
at org.apache.ibatis.scripting.xmltags.DynamicSqlSource.getBoundSql(DynamicSqlSource.java:39)
at org.apache.ibatis.mapping.MappedStatement.getBoundSql(MappedStatement.java:297)
at com.github.pagehelper.PageInterceptor.intercept(PageInterceptor.java:82)
at org.apache.ibatis.plugin.Plugin.invoke(Plugin.java:61)
at com.sun.proxy.$Proxy99.query(Unknown Source:-1)
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:147)
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:140)
at sun.reflect.NativeMethodAccessorImpl.invoke0(NativeMethodAccessorImpl.java:-1)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:433)
at com.sun.proxy.$Proxy81.selectList(Unknown Source:-1)
2.6.3、 ProviderSqlSource#getBoundSql:
其中invokeProviderMethod方法长这样:
所以,不论是RawSqlSource#getBoundSql还是DynamicSqlSource#getBoundSql还是ProviderSqlSource#getBoundSql,最终获得的都是StaticSqlSource#getBoundSql的结果。
注:本文的重点不是介绍如何自定义插件的,所以这里就简单介绍了。
3.2.1、 方式一: 自定义插件,然后只需要将插件注册进入Spring容器即可,MybatisAutoConfiguration的会自动感知到容器中的插件,让后将其记录进org.apache.ibatis.session.Configuration#interceptorChain。
3.2.1.2、 然后,MybatisAutoConfiguration的构造器会自动感知到容器中的插件:
提示:org.mybatis.spring.boot:mybatis-spring-boot-autoconfigure依赖下的spring.factories文件中,指定了org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration,这就意味着程序启动时,会(考虑)自动注册MybatisAutoConfiguration类。
3.2.1.3、 然后,在后面的逻辑中,会将感知到的插件记录进org.apache.ibatis.session.Configuration#interceptorChain。
3.2.2、 方式二: 获取到容器中所有的SqlSessionFactory,然后通过SqlSessionFactory实例获取到Configuration实例,然后调用Configuration#addInterceptor添加自定义的拦截器。
3.2.3、 方式n: …
提示一: 所有的Mybatis插件都会在Mybatis启动时,记录进org.apache.ibatis.session.Configuration#interceptorChain。
提示二: Mybatis插件是SqlSession级别的,所以在执行SQL时,才会在SqlSession中真正应用给四大拦截对象(Executor或StatementHandler或ParameterHandler或ResultSetHandler)
。
3.3.1、 启动Mybatis时,记录所有插件:org.apache.ibatis.session.Configuration的interceptorChain属性实例中,维护了一个ArrayList;Mybatis所有的插件,都会在启动时记录进这个ArrayList中。
3.3.2、 启动Mybatis后,执行SQL方法时,绑定插件给四大对象:
3.3.2.1、 插件应用给Executor的时机:org.mybatis.spring.SqlSessionTemplate.SqlSessionInterceptor#invoke中getSqlSession时。更准确的说,是【时机是】Configuration#newExecutor时,可以看一下相关方法调用栈细节:
"main@1" prio=5 tid=0x1 nid=NA runnable
java.lang.Thread.State: RUNNABLE
at org.apache.ibatis.plugin.InterceptorChain.pluginAll(InterceptorChain.java:30) // 初始化插件(将插件与Executor关联起来)
at org.apache.ibatis.session.Configuration.newExecutor(Configuration.java:599)
at org.apache.ibatis.session.defaults.DefaultSqlSessionFactory.openSessionFromDataSource(DefaultSqlSessionFactory.java:96)
at org.apache.ibatis.session.defaults.DefaultSqlSessionFactory.openSession(DefaultSqlSessionFactory.java:57)
at org.mybatis.spring.SqlSessionUtils.getSqlSession(SqlSessionUtils.java:98) // 获取SqlSession时
at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:428)
at com.sun.proxy.$Proxy81.selectList(Unknown Source:-1)
at org.mybatis.spring.SqlSessionTemplate.selectList(SqlSessionTemplate.java:230)
at org.apache.ibatis.binding.MapperMethod.executeForMany(MapperMethod.java:147)
at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:80)
at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:58) // 代理对象调用方法
at com.sun.proxy.$Proxy86.selectAll_xml(Unknown Source:-1) // 代理对象调用方法
at com.aspire.ssm.SsmApplicationTests.testOne(SsmApplicationTests.java:24) // 触发查询
3.3.2.2、 插件应用给ParameterHandler、ResultSetHandler、StatementHandler的时机:org.mybatis.spring.SqlSessionTemplate.SqlSessionInterceptor#invoke中,getSqlSession后,查询动作完成之前。更准确的说,【时机分别是】Configuration#newParameterHandler时、Configuration#newParameterHandler时、Configuration#newParameterHandler时。可以看一下相关方法调用栈细节:
"main@1" prio=5 tid=0x1 nid=NA runnable
java.lang.Thread.State: RUNNABLE
at org.apache.ibatis.plugin.InterceptorChain.pluginAll(InterceptorChain.java:30) // 应用插件(将插件与StatementHandler关联起来)
/*
* ************************************************************
* 在这之间会(按顺序)完成【应用插件(将插件与ResultSetHandler关联起来)】、【应用插件(将插件与ParameterHandler关联起来)】
* 注: 可详见源码org.apache.ibatis.session.Configuration.newStatementHandler
*/
at org.apache.ibatis.plugin.InterceptorChain.pluginAll(InterceptorChain.java:30)
at org.apache.ibatis.session.Configuration.newResultSetHandler(Configuration.java:571) // 应用插件(将插件与ResultSetHandler关联起来)
at org.apache.ibatis.executor.statement.BaseStatementHandler.<init>(BaseStatementHandler.java:70)
at org.apache.ibatis.session.Configuration.newParameterHandler(Configuration.java:564) // 应用插件(将插件与ParameterHandler关联起来)
at org.apache.ibatis.executor.statement.BaseStatementHandler.<init>(BaseStatementHandler.java:69)
at org.apache.ibatis.executor.statement.PreparedStatementHandler.<init>(PreparedStatementHandler.java:41)
at org.apache.ibatis.executor.statement.RoutingStatementHandler.<init>(RoutingStatementHandler.java:46)
// ************************************************************
at org.apache.ibatis.session.Configuration.newStatementHandler(Configuration.java:577)
at org.apache.ibatis.executor.SimpleExecutor.doQuery(SimpleExecutor.java:61) // 触发查询方法
at org.apache.ibatis.executor.BaseExecutor.queryFromDatabase(BaseExecutor.java:324)
at org.apache.ibatis.executor.BaseExecutor.query(BaseExecutor.java:156)
at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:109)
at com.github.pagehelper.PageInterceptor.intercept(PageInterceptor.java:108)
at org.apache.ibatis.plugin.Plugin.invoke(Plugin.java:61)
at com.sun.proxy.$Proxy99.query(Unknown Source:-1)
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:147)
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:140)
at sun.reflect.NativeMethodAccessorImpl.invoke0(NativeMethodAccessorImpl.java:-1)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498) // 获取SqlSession后, 触发查询方法
at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:433)
at com.sun.proxy.$Proxy81.selectList(Unknown Source:-1)
at org.mybatis.spring.SqlSessionTemplate.selectList(SqlSessionTemplate.java:230)
at org.apache.ibatis.binding.MapperMethod.executeForMany(MapperMethod.java:147)
at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:80)
at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:58) // 代理对象调用方法
at com.sun.proxy.$Proxy86.selectAll_xml(Unknown Source:-1) // 代理对象调用方法
at com.aspire.ssm.SsmApplicationTests.testOne(SsmApplicationTests.java:24) // 触发查询
3.3.2.3、 将上面两个分支中涉及到的org.apache.ibatis.plugin.InterceptorChain#pluginAll(Object target)单独拿出来,继续钻细节:
InterceptorChain#pluginAll的关键调用栈:
"main@1" prio=5 tid=0x1 nid=NA runnable
java.lang.Thread.State: RUNNABLE
at org.apache.ibatis.plugin.Plugin.wrap(Plugin.java:46)
at com.aspire.ssm.plugins.MyExecutorPlugin.plugin(MyExecutorPlugin.java:46)
at org.apache.ibatis.plugin.InterceptorChain.pluginAll(InterceptorChain.java:31)
3.3.2.3.2.1、 A处的作用是:获取到当前插件对象上@Intercepts注解里,@Signature的信息,即:获取到下图自定义插件中这个位置里面的信息:
注:源码可详见org.apache.ibatis.plugin.Plugin#getSignatureMap。
3.3.2.3.2.3、 B处的作用是:决定是否把当前interceptor插件,绑定到当前target对象上。若返回的interfaces长度大于0,则需要绑定,否者不需要绑定。
getAllInterfaces方法返回interfaces的逻辑是这样的:
逻辑一:获取target对象的所有接口,使知道target代表了四大对象中的谁(可以只代表一个、也可以同时代表多个)。
问:为什么是获取target的所有接口?
答:我们知道,target实际上是四大对象的子类实现。四大对象分别是Executor、ParameterHandler、ResultSetHandler、StatementHandler,他们都是接口。所以,这里获取到target实现的所有接口后,就知道target是属于四大对象中的哪个(或哪些)对象了。简单的讲,就是让程序知道target代表了四大对象中的谁(可以只代表一个、也可以同时代表多个)。
逻辑二:通过A处得到的signatureMap,进一步请Class交集,若存在,则添加至集合interfaces中,并返回。
问:signatureMap在getAllInterfaces方法中发挥的作用是什么?
答:首先,signatureMap是我们在前面的A中获得的,这里面的信息代表了:当前Interceptor的绑定方向(或者说处理能力)。即:当前Interceptor实例只能用于,在这个signatureMap的keys中存在的类。因为我们在自定义插件时,@Signature指定的type为四大对象,所以这里signatureMap中的keys也只可能是四大对象。
这样一来,对这两者求交集,就能知道:是否应该把当前Interceptor插件绑定到当前target实例上了
3.3.2.3.2.4、 如果需要绑定的话,就将当前interceptor插件,绑定到当前target对象上;并返回,作为新的target,继续遍历下一个插件。
绑定当前插件至此target,同时生成新的target并返回:
for循环下一个插件:
注:这里可以看到【装饰者模式】,插件对原target进行层层装饰,返回装饰后的新的target。
在target对象(即:四大对象)执行@Signature指定的方法时
。
3.4.1、 分析:
3.4.1.2、 那么,当target对象属于四大对象中的Executor,且当其执行Executor#query(MappedStatement, Object, RowBounds, ResultHandler)或Executor#query(MappedStatement, Object, RowBounds, ResultHandler, CacheKey, BoundSql)方法时:
根据当前对象执行的方法,判断是否触发当前插件的逻辑。可详见源码org.apache.ibatis.plugin.Plugin#invoke:
注:在【第三问】中,我们知道了插件是何时绑定到对象上的。但是,程序运行毕竟是方法上的,光知道插件与对象的对应关系,还不行;还得知道插件和该对象的方法的对应关系。此处就是知道插件与对象的方法的对应关系的,如果对应,则执行插件的逻辑;否者不执行。
3.4.1.3.1、 参数Invocation说明:
方法Interceptor#intercept的参数Invocation,是下图这里传的:
可以看到,一个Invocation对象里,包含了:
target:真正要调用的对象
。
method:要调用的方法
。
args:方法的参数
。
3.4.1.3.2、 A处是此插件的前处理逻辑。
3.4.1.3.3、 B处invocation.proceed,表示:让程序继续往下执行:
提示一:这一步背后的逻辑,用到了【责任链模式】,也可以理解为逆向打开【装饰者模式】。
提示二:可以参考Filter的机制进行理解。
举例说明,这一步背后的逻辑
:
假设:
同时有3个插件针对当前对象(注:当前对象属于四大对象)的当前方法生效。那么
,原target对象被这三个插件装饰(包裹)后的新的target是这样的:
此处背后的逻辑(方法栈)为:
3.4.1.3.4、 C处是此插件的后处理逻辑。
3.4.2、 结论:
当执行方法的对象是插件的@Signature注解里指定的对象,且执行的方法是@Signature注解里指定的对象的指定方法时;会先执行插件的前处理逻辑,然后再继续执行目标方法,然后再执行插件的后处理逻辑。当该对象的该方法同时被多个插件拦截时,会像链条一样(责任链),先按顺序执行所有插件的前处理逻辑,然后再继续执行目标方法,然后再按顺序执行所有插件的后处理逻辑。
注:前处理逻辑、后处理逻辑的位置如图所示:
多个插件时,执行顺序如图所示:
提示:哪个插件在外层,哪个插件在里层,可详见【第五问】。
3.5.1、 分析:
在【插件绑定到四大对象】时,会调用org.apache.ibatis.plugin.InterceptorChain#pluginAll给target(注:当前target对象,为四大对象之一)应用上插件。
因为是通过foreach循环的有序列表ArrayList中的所有的插件。那么,ArrayList中,index越小的插件,就越先应用给target对象。
3.5.2、 结论:
答:不会。因为Mybatis插件实际上是代理模式的一种实现。内部调用,是不会走代理的,所以如左图1中所示,当外部调用了BaseExecutor的4个参数的query方法时,虽然4个参数的query方法内部调用了6个参数的query方法,但是Mybatis插件(即:代理)的逻辑还是只走一遍。
Mybatis之一个SQL的运行过程,梳理完毕 !
^_^ 如有不当之处,欢迎指正
^_^ 参考资料
《Mybatis源码》
^_^ 测试代码托管链接
https://github.com/JustryDeng…Mybatis…
^_^ 本文已经被收录进《程序员成长笔记(七)》,笔者JustryDeng