阅读完需:约 32 分钟
SqlSession下的四大对象介绍
通过前面的分析,我们应该知道在Mybatis中的,首先是通过SqlSessionFactoryBuilder
加载全局配置文件(包括SQL映射器),这些配置都会封装在Configuration
中,其中每一条SQL语句的信息都会封装在MappedStatement
中。然后创建SqlSession
,这时还会初始化Executor
执行器。最后通过调用sqlSession.getMapper()
来动态代理执行Mapper中对应的SQL语句。而当一个动态代理对象进入到了MapperMethod的execute()
方法后,它经过简单地判断就进入了SqlSession的delete、update、insert、select等方法,这里是真正执行SQL语句的地方。那么这些方法是如何执行呢?
答:实际上SqlSession
的执行过程是通过Executor、StatementHandler、ParameterHandler和ResultSetHandler
来完成数据库操作和结果返回的,它们简称为四大对象:
-
Executor
:代表执行器,由它调度StatementHandler
、ParameterHandler
、ResultSetHandler
等来执行对应的SQL,其中StatementHandler
是最重要的。 -
StatementHandler
:作用是使用数据库的Statement(PreparedStatement
)执行操作,它是四大对象的核心,起到承上启下的作用,许多重要的插件都是通过拦截它来实现的。 -
ParameterHandler
:是用来处理SQL参数的。 -
ResultSetHandler
:是进行数据集(ResultSet)的封装返回处理的,它非常的复杂,好在不常用。
这四个对象都是通过Mybatis的插件来完成的,在实例化Executor、StatementHandler、ParameterHandler、ResultSetHandler四大接口对象的时候都是调用interceptorChain.pluginAll()
方法插入进去的。
四大对象的生成和运作原理
这里来分析一条查询SQL的执行过程:首先肯定是要构建SqlSessionFactory
、SqlSession
和动态代理Mapper对象的,前面已经介绍过了,所以不多说,主要来看具体是怎么样执行的。当动态代理对象获取MapperMethod
对象后,通过其内部的execute()
方法调用sqlSession.selectList()
方法来真正执行SQL,所以继续从这里来跟踪代码:
SqlSession的实现类为DefaultSqlSession
,所以去DefaultSqlSession中查看selectList()方法:
可以看到这里获取了MappedStatement
对象,并且调用了executor对象的query()方法来执行SQL。所以我们来看看Executor类。
Executor对象
Executor
表示执行器,它是真正执行Java和数据库交互的对象,所以它十分重要,每一个SqlSession都会拥有一个Executor对象,这个对象负责增删改查的具体操作,我们可以简单的将它理解为JDBC中Statement的封装版。基本功能:改、查,没有增删的原因是所有的增删操作都可以归结到改。
Executor的关系图如下:
-
BaseExecutor
:是一个抽象类,采用模板方法的设计模式。它实现了Executor接口,实现了执行器的基本功能。 -
SimpleExecutor
:最简单的执行器,根据对应的SQL直接执行即可,不会做一些额外的操作;拼接完SQL之后,直接交给StatementHandler
去执行。 -
BatchExecutor
:批处理执行器,用于将多个SQL一次性输出到数据库,通过批量操作来优化性能。通常需要注意的是批量更新操作,由于内部有缓存的实现,使用完成后记得调用flushStatements
来清除缓存。 -
ReuseExecutor
:可重用的执行器,重用的对象是Statement,也就是说该执行器会缓存同一个sql的Statement,省去Statement的重新创建,优化性能。内部的实现是通过一个HashMap来维护Statement
对象的。由于当前Map只在该session中有效,所以使用完成后记得调用flushStatements来清除Map。调用实现的四个抽象方法时会调用prepareStatement()
-
CachingExecutor
:启用于二级缓存时的执行器;采用静态代理;代理一个 Executor 对象。执行 update 方法前判断是否清空二级缓存;执行 query 方法前先在二级缓存中查询,命中失败再通过被代理类查询。
我们来看看Mybatis是如何创建Executor
的,其实在前面已经介绍过了,它是在Configuration
类中完成的,这里不看可以跳过:
Executor对象会在MyBatis加载全局配置文件时初始化,它会根据配置的类型去确定需要创建哪一种Executor,我们可以在全局配置文件settings元素中配置Executor类型,setting属性中有个defaultExecutorType
,可以配置如下3个参数:
-
SIMPLE
: 简易执行器,它没有什么特别的,默认执行器 -
REUSE
:是一种能够执行重用预处理语句的执行器 -
BATCH
:执行器重用语句和批量更新,批量专用的执行器
默认使用SimpleExecutor
。而如果开启了二级缓存,则用CachingExecutor
进行包装,SqlSession
会调用CachingExecutor
执行器的query()
方法,先从二级缓存获取数据,当无法从二级缓存获取数据时,则委托给BaseExecutor
的子类进行操作,CachingExecutor
执行过程代码如下:
如果没有使用二级缓存并且没有配置其它的执行器,那么MyBatis默认使用SimpleExecutor
,调用父类BaseExecutor的query()
方法:
注意:query方法有两种形式,一种是直接查询;一种是从缓存中查询,下面来看一下源码:
当有一个查询请求访问的时候,如果开启了二级缓存,首先会经过Executor
的实现类CachingExecutor
。
先从二级缓存中查询SQL是否是第一次执行,如果是第一次执行的话,那么就直接执行SQL语句,并创建缓存,如果第二次访问相同的SQL语句的话,那么就会直接从缓存中提取。
如果没有开启二级缓存,是第一次执行,直接执行SQL语句,并创建缓存,再次执行则从一级缓存中获取数据,如下源代码所示:
而如果一级缓存也没有数据,则调用queryFromDatabase()
从数据库中获取数据:
在queryFromDatabase()
方法中调用SimpleExecutor
的 doQuery()
方法(注意:这里说是调用了SimpleExecutor
的方法,但是还在BaseExecutor
类中是因为SimpleExecutor
继承了它,所以SimpleExecutor
对象中也有这个方法,而doQuery()
方法在子类SimpleExecutor
实现的,所以说是调用SimpleExecutor的 doQuery() 方法。其方法代码如下:
这里显然是根据Configuration
对象来构建StatementHandler
,然后使用prepareStatement()
方法对SQL编译和参数进行初始化。
实现过程是:它调用了StatementHandler
的prepare()
进行了预编译和基础的设置,然后通过StatementHandler的parameterize()
来设置参数,这个parameterize()
方法实际是通过ParameterHandler
来对参数进行设置。最后使用 StatementHandler的query()
方法,把ResultHandler
传递进去,执行查询后再通过ResultSetHandler
封装结果并将结果返回给调用者来完成一次查询,这样焦点又转移到了 StatementHandler
对象上。所以通过以上流程发现,MyBatis核心工作实际上是由Executor、StatementHandler、ParameterHandler和ResultSetHandler四个接口完成的,掌握这四个接口的工作原理,对理解MyBatis底层工作原理有很大帮助。
StatementHandler对象
StatementHandler
是数据库会话器,顾名思义,数据库会话器就是专门处理数据库会话的,相当于JDBC中的Statement(PreparedStatement)。StatementHandler的关系图如下:
StatementHandler
接口设计采用了适配器模式,其实现类RoutingStatementHandler
根据上下文来选择适配器生成相应的StatementHandler
。三个适配器分别是SimpleStatementHandler
、PreparedStatementHandler
和CallableStatementHandler
。
-
BaseStatementHandler
: 是一个抽象类,它实现了StatementHandler
接口,用于简化StatementHandler
接口实现的难度,采用适配器设计模式,它主要有三个实现类SimpleStatementHandler、PreparedStatementHandler和CallableStatementHandler。
-
SimpleStatementHandler
: 最简单的StatementHandler,处理不带参数运行的SQL,对应JDBC的Statement -
PreparedStatementHandler
: 预处理Statement的handler,处理带参数允许的SQL, 对应JDBC的PreparedStatement(预编译处理) -
CallableStatementHandler
:存储过程的Statement的handler,处理存储过程SQL,对应JDBC的CallableStatement(存储过程处理) -
RoutingStatementHandler
:RoutingStatementHandler
根据上下文来选择适配器生成相应的StatementHandler
。三个适配器分别是SimpleStatementHandler、PreparedStatementHandler和CallableStatementHandler。
StatementHandler
接口中的方法如下:
StatementHandler
的初始化过程如下(它也是在Configuration
对象中完成的):
当调用到doQuery()
方法时内部会通过configuration.newStatementHandler()
方法来创建StatementHandler
对象。
可以发现MyBatis生成StatementHandler
代码中,创建的真实对象是一个RoutingStatementHandler
的对象,而不是其它三个对象中的。但是RoutingStatementHandler并不是真实的服务对象,它是通过适配器模式来找到对应的StatementHandler
来执行的。
在初始化RoutingStatementHandler
对象时,它会根据上下文环境来决定创建哪个具体的StatementHandler。RoutingStatementHandler 的构造方法如下:
它内部定义了一个对象的适配器delegate
,它是一个StatementHandler
接口对象,然后构造方法根据配置来配置对应的StatementHandler
对象。它的作用是给3个接口对象的使用提供一个统一且简单的适配器。此为对象的配适,可以使用对象配适器来尽可能地使用己有的类对外提供服务,可以根据需要对外屏蔽或者提供服务,甚至是加入新的服务。我们以常用的 PreparedStatementHandler
为例,看看Mybatis是怎么执行查询的。
继续跟踪到SimpleExecutor对象中的prepareStatement()方法:
可以发现Executor 执行查询时会执行 StatementHandler
的 prepare()
和 parameterize()
方法来对SQL进行预编译和参数的设置, 其中 PreparedStatementHandler
的 prepare
方法如下:
注意:这个 prepare
方法是先调用到 StatementHandler
的实现类 RoutingStatementHandler
,再由RoutingStatementHandler
调用 BaseStatementHandler
中的 prepare
方法。
通过prepare()方法,可知其中最重要的方法就是 instantiateStatement()
方法了,因为它要完成对SQL的预编译。在得到 Statement 对象的时候,会去调用 instantiateStatement()
方法,这个方法位于 BaseStatementHandler
中,是一个抽象方法由子类去实现,实际执行的是三种 StatementHandler
中的一种,我们以 PreparedStatementHandler
中的为例:
从上面的代码我们可以看到,instantiateStatement() 方法最终返回的也是Statement对象,所以经过一系列的调用会把创建好的 Statement 对象返回到 SimpleExecutor 简单执行器中,为后面设置参数的 parametersize 方法所用。也就是说,prepare 方法负责生成 Statement 实例对象,而 parameterize 方法用于处理 Statement 实例对应的参数。所以我们来看看parameterize 方法:
可以看到这里通过调用了ParameterHandler
对象来设置参数,所以下面我们来介绍一下ParameterHandler
对象。
ParameterHandler对象
ParameterHandler
是参数处理器,它的作用是完成对预编译的参数的设置,也就是负责为 PreparedStatement
的 SQL 语句参数动态赋值。ParameterHandler
相比于其他的组件就简单很多了,这个接口很简单只有两个方法:
这两个方法的含义为:
- getParameterObject: 用于获取参数对象。
- setParameters:用于设置预编译SQL的参数
ParameterHandler
对象的创建,ParameterHandler
参数处理器对象是在创建 StatementHandler
对象的同时被创建的,同样也是由 Configuration
对象负责创建:
可以发现在创建 ParameterHandler
对象时,传入了三个参数分别是:mappedStatement、parameterObject、boundSql
。
mappedStatement
保存了一个映射器节点<select|update|delete|insert>
中的内容,包括我们配置的SQL、SQL的id、缓存信息、resultMap、ParameterType、resultType、resultMap等重要配置内容等。
parameterObject
表示读取的参数。
boundSql
表示要实际执行的SQL语句,它是通过SqlSource 对象来生成的,就是根据传入的参数对象,动态计算这个 BoundSql, 也就是 Mapper 文件中节点的计算,是由 SqlSource 完成的,SqlSource
最常用的实现类是 DynamicSqlSource
。
然后就我们进入newParameterHandler()
方法中:
上面是在 Configuration
对象创建 ParameterHandler
的过程,它实际上是交由 LanguageDriver
来创建具体的参数处理器,LanguageDriver
默认的实现类是 XMLLanguageDriver
,由它调用 DefaultParameterHandler
中的构造方法完成 ParameterHandler
的创建工作:
上面创建完成之后,该进行具体的解析工作,那么 ParameterHandler
如何解析SQL中的参数呢?
ParameterHandler
由实现类DefaultParameterHandler
执行,使用TypeHandler
将参数对象类型转换成jdbcType
,完成预编译SQL的参数设置,这是在setParameters()
方法中完成的,setParameters()
方法的实现如下:
至此,我们的参数就处理完成了。一切都准备就绪之后就肯定可以执行了呗!在SimpleExecutor
的 doQuery()
方法中最后会调用query()
方法来执行SQL语句。并且把创建好的Statement和结果处理器以参数传入进去,我们进入query()方法:
可以看到这里执行了我们的SQL语句,然后对执行的结果进行处理,这里用到的是MyBatis 四大对象的最后一个神器也就是 ResultSetHandler,所以下面我们继续来介绍ResultSetHandler对象。
ResultSetHandler对象
ResultSetHandler
是结果处理器,它是用来组装结果集的。ResultSetHandler
接口的定义也挺简单的,只有三个方法:
ResultSetHandler
对象的创建,ResultSetHandler
对象是在处理查询请求时创建 StatementHandler
对象同时被创建的,同样也是由 Configuration
对象负责创建,示例如下:
Configuration
对象中的newResultSetHandler()
方法:
ResultSetHandler 创建好之后就可以处理结果映射了。还记得在前面Executor的doQuery()方法中,我们最后是通过调用handler.query()
方法来完成结果集的处理,如下:
进入query()方法,它是在PreparedStatementHandler实现的:
ResultSetHandler
接口只有一个默认的实现类是DefaultResultSetHandler
,我们通过SELECT语句执行得到的结果集由其 handleResultSets()
方法处理,方法如下:
上面涉及的主要对象有:
ResultSetWrapper
:结果集的包装器,主要针对结果集进行的一层包装。这个类中的主要属性有:
-
ResultSet
: Java JDBC ResultSet接口表示数据库查询的结果。 有关查询的文本显示了如何将查询结果作为java.sql.ResultSet返回。 然后迭代此ResultSet以检查结果。 -
TypeHandlerRegistry
: 类型注册器,TypeHandlerRegistry
在初始化的时候会把所有的 Java类型和类型转换器进行注册。 -
ColumnNames
: 字段的名称,也就是查询操作需要返回的字段名称 -
ClassNames
: 字段的类型名称,也就是ColumnNames
每个字段名称的类型 -
JdbcTypes
: JDBC 的类型,也就是java.sql.Types 类型
ResultMap
:负责处理更复杂的映射关系
multipleResults
:用于封装处理好的结果集。其中的主要方法是 handleResultSet
:
可以看到,handleResultSet()
方法中又分为:嵌套和不嵌套处理这两种方法,这里我们只管理不嵌套的处理,嵌套的虽然会比不嵌套复杂一点,但总体类似,差别并不大。
最后 handleResultSets()
方法返回的是 collapseSingleResultList(multipleResults)
,我们来看一下:
它是判断的 multipleResults
的数量,如果数量是 1 ,就直接取位置为0的元素,如果不是1,那就返回 multipleResults
的真实数量。
以上在 DefaultResultSetHandler
中处理完结果映射,并把上述得到的结果返回给调用的客户端,从而执行完成一条完整的SQL语句。结果集的处理就看到这里了,因为ResultSetHandler
的实现非常复杂,它涉及了CGLIB
或者JAVASSIST
作为延迟加载,然后通过typeHandler
和ObjectFactory
解析组装结果在返回,由于实际工作需要改变它的几率不高加上他比较复杂,所以这里就不在论述了。
小结
一条SQL语句在Mybatis中的执行过程小结:
首先是创建Mapper的动态代理对象MapperProxy,然后将Mappe接口中的方法封装至MapperMethod对象,通过MapperMethod对象中的execute()方法来执行SQL,其本质是通过SqlSession下的方法来实现的,SQL语句的具体的执行则是通过SqlSession下的四大对象来完成。
Executor先调用StatementHandler的prepare()方法预编译SQL,然后用parameterize()方法启用ParameterHandler设置参数,完成预编译,执行查询,update()也是这样的。
如果是查询,MyBatis则会使用ResultSetHandler来封装结果并返回给调用者,从而完成查询。其执行的完整流程图如下:
详细解析图
MyBatis插件原理
MyBatis提供了一种插件(plugin)的功能,虽然叫做插件,但其实这是拦截器功能。那么拦截器拦截MyBatis中的哪些内容呢?MyBatis 允许你在已映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:
-
Executor
(update, query, flushStatements, commit, rollback, getTransaction, close, isClosed) -
ParameterHandler
(getParameterObject, setParameters) -
ResultSetHandler
(handleResultSets, handleOutputParameters) -
StatementHandler
(prepare, parameterize, batch, update, query)
我们看到了可以拦截Executor接口的部分方法,比如update,query,commit,rollback等方法,还有其他接口的一些方法等。总体概括为:
- 拦截执行器的方法
- 拦截参数的处理
- 拦截结果集的处理
- 拦截Sql语法构建的处理
插件的使用
拦截器介绍及配置
首先我们看下MyBatis拦截器的接口定义:
public interface Interceptor {
Object intercept(Invocation invocation) throws Throwable;
Object plugin(Object target);
void setProperties(Properties properties);
}
比较简单,只有3个方法。 MyBatis默认没有一个拦截器接口的实现类,开发者们可以实现符合自己需求的拦截器。下面的MyBatis官网的一个拦截器实例:
@Intercepts({@Signature(
type= Executor.class,
method = "update",
args = {MappedStatement.class,Object.class})})
public class ExamplePlugin implements Interceptor {
public Object intercept(Invocation invocation) throws Throwable {
return invocation.proceed();
}
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
public void setProperties(Properties properties) {
}
}
全局xml配置:
<plugins>
<plugin interceptor="org.format.mybatis.cache.interceptor.ExamplePlugin">
</plugin>
</plugins>
这个拦截器拦截Executor接口的update方法(其实也就是SqlSession的新增,删除,修改操作),所有执行executor的update方法都会被该拦截器拦截到。
使拦截器生效的方式:
1、直接给自定义拦截器添加一个 @Component注解,使得拦截器成为一个Spring Bean。
2、SqlSessionFactory
直接通过SqlSessionFactory
来注册插件也是一个非常通用的做法
这里也有两种方式——第一种是通过setPlugins()
方式
@Bean
public SqlSessionFactory
sqlSessionFactory(DataSource dataSource,
MybatisPlusProperties mybatisProperties) throws Exception {
MybatisSqlSessionFactoryBean sessionFactory = new MybatisSqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource);
// 关键是这个 setPlugins()
sessionFactory.setPlugins(new TranslatedToIntercept());
sessionFactory.setConfiguration(mybatisProperties.getConfiguration());
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
// 需要在这里指定xml文件的位置,不然自定义的sql会报 Invalid bound statement 异常
sessionFactory.setMapperLocations(resolver.getResources("classpath*:mapper/*.xml"));
return sessionFactory.getObject();
}
第二张是通过addInterceptor()
添加拦截器
public TranslatedToIntercept translated(){
return new TranslatedToIntercept();
}
@Bean
public SqlSessionFactory
sqlSessionFactory(DataSource dataSource,
MybatisPlusProperties mybatisProperties) throws Exception {
MybatisSqlSessionFactoryBean sessionFactory = new MybatisSqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource);
// sessionFactory.setPlugins(new TranslatedToIntercept());
sessionFactory.setConfiguration(mybatisProperties.getConfiguration());
try {
//关键在 这里,先获取配置再添加拦截器
final TranslatedToIntercept interceptor = translated();
sessionFactory.getConfiguration().addInterceptor(interceptor);
} catch (Exception ignored) {
}
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
// 需要在这里指定xml文件的位置,不然自定义的sql会报 Invalid bound statement 异常
sessionFactory.setMapperLocations(resolver.getResources("classpath*:mapper/*.xml"));
return sessionFactory.getObject();
}
3、XML配置——通过Mybatis-config.xml
全局配置
<plugins>
<plugin interceptor="org.format.mybatis.cache.interceptor.ExamplePlugin">
</plugin>
</plugins>
源码分析
下面我们分析一下这段代码背后的源码。首先从源头->配置文件开始分析:XMLConfigBuilder解析MyBatis全局配置文件的pluginElement私有方法:
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
String interceptor = child.getStringAttribute("interceptor");
Properties properties = child.getChildrenAsProperties();
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
interceptorInstance.setProperties(properties);
configuration.addInterceptor(interceptorInstance);
}
}
}
具体的解析代码其实比较简单,就不贴了,主要就是通过反射实例化plugin节点中的interceptor属性表示的类。然后调用全局配置类Configuration的addInterceptor
方法。
public void addInterceptor(Interceptor interceptor) {
interceptorChain.addInterceptor(interceptor);
}
这个interceptorChain是Configuration的内部属性,类型为InterceptorChain
,也就是一个拦截器链,我们来看下它的定义:
public class InterceptorChain {
private final List<Interceptor> interceptors = new ArrayList<Interceptor>();
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
public void addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
}
public List<Interceptor> getInterceptors() {
return Collections.unmodifiableList(interceptors);
}
}
现在我们理解了拦截器配置的解析以及拦截器的归属,现在我们回过头看下为何拦截器会拦截这些方法(Executor,ParameterHandler,ResultSetHandler,StatementHandler的部分方法):
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
return parameterHandler;
}
public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,
ResultHandler resultHandler, BoundSql boundSql) {
ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
return resultSetHandler;
}
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler;
}
public Executor newExecutor(Transaction transaction, ExecutorType executorType, boolean autoCommit) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
if (cacheEnabled) {
executor = new CachingExecutor(executor, autoCommit);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
以上4个方法都是Configuration
的方法。这些方法在MyBatis的一个操作(新增,删除,修改,查询)中都会被执行到,执行的先后顺序是Executor,ParameterHandler,ResultSetHandler,StatementHandler
(其中ParameterHandler
和ResultSetHandler
的创建是在创建StatementHandler
[3个可用的实现类CallableStatementHandler,PreparedStatementHandler,SimpleStatementHandler]的时候,其构造函数调用的[这3个实现类的构造函数其实都调用了父类BaseStatementHandler的构造函数])。这4个方法实例化了对应的对象之后,都会调用interceptorChain的pluginAll方法,InterceptorChain的pluginAll刚才已经介绍过了,就是遍历所有的拦截器,然后调用各个拦截器的plugin方法。
注意:拦截器的plugin方法的返回值会直接被赋值给原先的对象由于可以拦截StatementHandler
,这个接口主要处理sql语法的构建,因此比如分页的功能,可以用拦截器实现,只需要在拦截器的plugin方法中处理StatementHandler接口实现类中的sql即可,可使用反射实现。
MyBatis还提供了 @Intercepts
和 @Signature
关于拦截器的注解。官网的例子就是使用了这2个注解,还包括了Plugin类的使用:
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
下面我们就分析这3个 “新组合” 的源码,首先先看Plugin类的wrap方法:
public static Object wrap(Object target, Interceptor interceptor) {
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
if (interfaces.length > 0) {
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}
Plugin类实现了InvocationHandler
接口,很明显,我们看到这里返回了一个JDK自身提供的动态代理类。我们解剖一下这个方法调用的其他方法:
getSignatureMap
方法:
private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
if (interceptsAnnotation == null) { // issue #251
throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());
}
Signature[] sigs = interceptsAnnotation.value();
Map<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>();
for (Signature sig : sigs) {
Set<Method> methods = signatureMap.get(sig.type());
if (methods == null) {
methods = new HashSet<Method>();
signatureMap.put(sig.type(), methods);
}
try {
Method method = sig.type().getMethod(sig.method(), sig.args());
methods.add(method);
} catch (NoSuchMethodException e) {
throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);
}
}
return signatureMap;
}
getSignatureMap方法解释:首先会拿到拦截器这个类的 @Interceptors
注解,然后拿到这个注解的属性 @Signature
注解集合,然后遍历这个集合,遍历的时候拿出 @Signature注解的type属性(Class类型),然后根据这个type得到带有method属性和args属性的Method。由于 @Interceptors注解的 @Signature属性是一个属性,所以最终会返回一个以type为key,value为Set的Map。
@Intercepts({@Signature(
type= Executor.class,
method = "update",
args = {MappedStatement.class,Object.class})})
比如这个 @Interceptors注解会返回一个key为Executor,value为集合(这个集合只有一个元素,也就是Method实例,这个Method实例就是Executor接口的update方法,且这个方法带有MappedStatement和Object类型的参数)。这个Method实例是根据 @Signature的method和args属性得到的。如果args参数跟type类型的method方法对应不上,那么将会抛出异常。
getAllInterfaces
方法:
private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
Set<Class<?>> interfaces = new HashSet<Class<?>>();
while (type != null) {
for (Class<?> c : type.getInterfaces()) {
if (signatureMap.containsKey(c)) {
interfaces.add(c);
}
}
type = type.getSuperclass();
}
return interfaces.toArray(new Class<?>[interfaces.size()]);
}
getAllInterfaces
方法解释:根据目标实例target(这个target就是之前所说的MyBatis拦截器可以拦截的类,Executor,ParameterHandler,ResultSetHandler,StatementHandler
)和它的父类们,返回signatureMap中含有target实现的接口数组。所以Plugin
这个类的作用就是根据 @Interceptors
注解,得到这个注解的属性 @Signature
数组,然后根据每个 @Signature
注解的type,method,args
属性使用反射找到对应的Method
。最终根据调用的target对象实现的接口决定是否返回一个代理对象替代原先的target对象。
比如MyBatis官网的例子,当Configuration调用newExecutor方法的时候,由于Executor接口的update(MappedStatement ms, Object parameter)方法被拦截器被截获。因此最终返回的是一个代理类Plugin,而不是Executor。
这样调用方法的时候,如果是个代理类,那么会执行:
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
if (methods != null && methods.contains(method)) {
return interceptor.intercept(new Invocation(target, method, args));
}
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
没错,如果找到对应的方法被代理之后,那么会执行Interceptor
接口的interceptor
方法。
这个Invocation
类如下:
public class Invocation {
private Object target;
private Method method;
private Object[] args;
public Invocation(Object target, Method method, Object[] args) {
this.target = target;
this.method = method;
this.args = args;
}
public Object getTarget() {
return target;
}
public Method getMethod() {
return method;
}
public Object[] getArgs() {
return args;
}
public Object proceed() throws InvocationTargetException, IllegalAccessException {
return method.invoke(target, args);
}
}
它的proceed方法也就是调用原先方法(不走代理)。
总结
MyBatis拦截器接口提供的3个方法中,plugin方法用于某些处理器(Handler)的构建过程。interceptor方法用于处理代理类的执行。setProperties方法用于拦截器属性的设置。
其实MyBatis官网提供的使用 @Interceptors和 @Signature注解以及Plugin类这样处理拦截器的方法,我们不一定要直接这样使用。我们也可以抛弃这3个类,直接在plugin方法内部根据target实例的类型做相应的操作。总体来说MyBatis拦截器还是很简单的,拦截器本身不需要太多的知识点,但是学习拦截器需要对MyBatis中的各个接口很熟悉,因为拦截器涉及到了各个接口的知识点。
MyBatis插件过程
MyBatis-Plus插件接口
在MyBatis中想要做一个插件最好是实现Interceptor
接口添加注解@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
,而在Plus中可以去实现Plus里面的提供的接口InnerInterceptor
,这个接口就是Mybatis那4个核心类的集合体。
当我们将插件完成后可以注册添加到MyBatisPlus的内部,那么SQL就会被我们的类所拦截。
@Bean
MybatisPlusInterceptor interceptor() {
final MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
PaginationInnerInterceptor pageInterceptor = new PaginationInnerInterceptor();
pageInterceptor.setOptimizeJoin(false);
interceptor.addInnerInterceptor(pageInterceptor);
return interceptor;
}
我们的类需要像MyBtaisPlus自己提供的分页插件一样,添加到MybatisPlusInterceptor
类中,然后将MybatisPlusInterceptor
注册为Spring的Bean
。
JSqlParser——SQL语法解释器
在我们编写插件拦截SQL语句的时候往往都要自定义的拼接SQL,那么JSqlParser
就是一个拼接SQL很好的工具。
支持常见数据库包含Oracle、SqlServer、MySQL、PostgreSQL, 可以直接操作由语句生成的对象并生成新的sql,也可以使用访问者模式访问sql转换而来的模型。mybatis-plus权限隔离组件就是用的这个工具包来动态修改sql。
https://github.com/JSQLParser/JSqlParser
案例
String sqlStr = "SELECT\n" +
"\tsu.dept_id `deptId`,\n" +
"\tsu.user_id,\n" +
"\tsr.role_id,\n" +
"\tsu.user_name,\n" +
"\tsd.dept_name,\n" +
"\tsr.role_name\n" +
"FROM\n" +
"\tsys_user AS su\n" +
"JOIN sys_dept sd ON su.dept_id = sd.dept_id\n" +
"JOIN sys_user_role sur ON sur.user_id = su.user_id\n" +
"JOIN sys_role sr ON sur.role_id = sr.role_id\n" +
"WHERE\n" +
"\tsd.dept_name = '研发部门'\n" +
"\tand su.user_name = 'admin'\n" +
"\tand su.dept_id = 103\n" +
"\tor sr.role_name = '超级管理员'\n" +
"ORDER BY\n" +
"\tsd.create_time DESC";
Select querySql = (Select)CCJSqlParserUtil.parse(sqlStr);
querySql.getSelectBody().accept(new SelectVisitorAdapter () {
@Override
public void visit(PlainSelect plainSelect) {
log.info("--------------查询列名----------------------------------------");
plainSelect.getSelectItems().stream().forEach(selectItem -> {
selectItem.accept(new SelectItemVisitorAdapter() {
@Override
public void visit(SelectExpressionItem selectExpressionItem) {
log.info(selectExpressionItem.getExpression().toString());
if (selectExpressionItem.getAlias()!=null) {
log.info("列别名 {}",selectExpressionItem.getAlias().getName());
}
}
});
});
log.info("--------------From Table Info----------------------------------------");
log.info(plainSelect.getFromItem().toString());
if (plainSelect.getFromItem().getAlias()!=null) {
log.info("表别名"+plainSelect.getFromItem().getAlias().getName());
}
log.info("--------------Join Table Info----------------------------------------");
plainSelect.getJoins().stream().forEach(join -> {
log.info(join.toString());
log.info("关联表:{} ",join.getRightItem());
if (join.getRightItem().getAlias()!=null) {
log.info("关联表别名:{}",join.getRightItem().getAlias().getName());
}
log.info("关联条件:{}",join.getOnExpression().toString());
});
log.info("--------------Where Info----------------------------------------");
plainSelect.getWhere().accept(new ExpressionVisitorAdapter() {
@Override
public void visitBinaryExpression(BinaryExpression expr) {
log.info("表达式:{}",expr.toString());
log.info("表达式左侧:{}",expr.getLeftExpression().toString());
log.info("表达式右侧:{}",expr.getRightExpression().toString());
}
});
log.info("--------------增加查询条件----------------------------------------");
try {
plainSelect.setWhere(new AndExpression(CCJSqlParserUtil.parseCondExpression("1=1"),plainSelect.getWhere()));
} catch (JSQLParserException e) {
throw new RuntimeException(e);
}
}
});
log.info("语句:{}",querySql.toString());
- 使用net.sf.jsqlparser.parser.CCJSqlParserUtil将输入转为Statement
- Statement为所有sql模型的父类, 使用访问者接口xxxAdapter或类型强转为具体的子类
- CCJSqlParserUtil.parseCondExpression可将字符串转换为条件模型 修改
- 模型之后使用toString()可获得最终语句
动态拼接SQL条件
当需要手动构造SQL的时候需要填充参数,这个时候就需要有一些工具来填充
比如Druid
提供的SQL工具,或者hutool提供的工具
@Test
public void CCJSqlParserUtil() throws JSQLParserException {
String sql = "select count(1) from vehicle.parking_lot where park_id = ? and name = ? and id != ?;";
List<Object> list=new ArrayList<>();
list.add("park");
list.add("name");
list.add(111);
System.out.println(SQLUtils.format(sql, "postgresql", list));
}
拼接结果
SELECT count(1)
FROM vehicle.parking_lot
WHERE park_id = 'park'
AND name = 'name'
AND id != 111;