参考文章
https://blog.csdn.net/weixin_40757920/article/details/105400943
https://zhuanlan.zhihu.com/p/265641500
MyBatis插件的使用
首先我们先来通过一个例子来看看如何使用插件。
1、首先建立一个MyPlugin实现接口Interceptor,然后重写其中的三个方法(注意,这里必须要实现Interceptor接口,否则无法被拦截)。
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import java.util.Properties;
@Intercepts({@Signature(type = Executor.class,method = "query",args = {MappedStatement.class,Object.class, RowBounds.class, ResultHandler.class})})
public class MyPlugin implements Interceptor {
/**
* 这个方法会直接覆盖原有方法
* @param invocation
* @return
* @throws Throwable
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("成功拦截了Executor的query方法,在这里我可以做点什么");
return invocation.proceed();//调用原方法
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target,this);//把被拦截对象生成一个代理对象
}
@Override
public void setProperties(Properties properties) {//可以自定义一些属性
System.out.println("自定义属性:userName->" + properties.getProperty("userName"));
}
}
注解说明
@Intercepts是声明当前类是一个拦截器
@Signature是标识需要拦截的方法签名,通过以下三个参数来确定
- type:被拦截的类名。指定拦截器类型(ParameterHandler ,StatementHandler,ResultSetHandler )
- method:被拦截的方法名,是拦截器类型中的方法,不是自己写的方法
- args:是method中方法的入参类型
2、我们还需要在mybatis-config中配置好插件。
<plugins>
<plugin interceptor="com.lonelyWolf.mybatis.plugin.MyPlugin">
<property name="userName" value="张三"/>
</plugin>
</plugins>
这里如果配置了property属性,那么我们可以在setProperties获取到。
完成以上两步,我们就完成了一个插件的配置了,接下来我们运行一下:
可以看到,setProperties方法在加载配置文件阶段就会被执行了。
sql是怎么执行的
在MyBatis中插件是通过拦截器来实现的,那么既然是通过拦截器来实现的,就会有一个问题,哪些对象才允许被拦截呢?
真正执行Sql的是四大对象:
-
Executor : 执行器,由它调度StatementHandler、ParameterHandler和ResultSetHandler等来执行对应的SQL。
-
StatementHandler,使用数据库的Statement(PreparedStatement)执行操作,它是四大对象的核心,起到承上启下的作用,许多重要的插件都是通过拦截它来实现的。
-
ParameterHandler,用来处理SQL参数。
-
ResultSetHandler。进行数据集(ResultSet)的封装返回处理的。
需要注意的是,虽然我们可以拦截这四大对象,但是并不是这四大对象中的所有方法都能被拦截,下面就是官网提供的可拦截的对象和方法汇总:
插件的加载
我们进入XMLConfigBuilder类看看
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
String interceptor = child.getStringAttribute("interceptor"); // 获取拦截
Properties properties = child.getChildrenAsProperties(); // 获取自定义properties属性
// 根据配置文件中配置的插件类的全限定类名进行反射初始化
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
interceptorInstance.setProperties(properties);
//添加到配置类的InterceptorChain属性,InterceptorChain类维护了一个List<Interceptor>
configuration.addInterceptor(interceptorInstance);
}
}
}
解析出来之后会将插件存入InterceptorChain对象的list属性。
public class InterceptorChain {
// 存取插件
private final List<Interceptor> interceptors = new ArrayList();
public Object pluginAll(Object target) {
Interceptor interceptor;
// 循环解析插件
for(Iterator var2 = this.interceptors.iterator();
var2.hasNext();
// 获取代理对象
target = interceptor.plugin(target)) {
interceptor = (Interceptor)var2.next();
}
return target;
}
.....
}
看到InterceptorChain我们是不是可以联想到,MyBatis的插件就是通过责任链模式实现的。
1、Executot——执行器
1.1 执行器介绍
Executor是一个执行器,SqlSession是一个门面,真正干活的是执行器,它是一个真正执行Java与数据库交互的对象。
mybatis中有3种执行器,可以在mybaits配置文件中的defaultExecutorType属性进行选择,对于spring boot项目则在application配置文件中配置mybatis.configuration.default-executor-type属性。
1.2 执行器分类
- SIMPLE——简易执行器,没有什么特别的,默认执行器。
- REUSE——一种能够执行重用预处理语句的执行器。
- BATCH——执行器重用语句和批量更新,批量专用的执行器。
1.3、执行器的加载
在Configuration类中有如下方法,该方法根据配置的执行器类型去确定创建哪一种Executor。
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
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) {
//缓存用CachingExecutor进行包装Executor
executor = new CachingExecutor(executor);
}
//在运用插件时,拦截Executor
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
上述代码中的 interceptorChain.pluginAll(executor) ;是运行插件的关键,它拦截Executor,并构建一层层的动态代理对象,可以修改在调度真实的Executor方法之前执行配置插件的代码,这个就是插件的原理。
public Object pluginAll(Object target) {
Interceptor interceptor;
// 循环解析插件
for(Iterator var2 = this.interceptors.iterator();
var2.hasNext();
// 获取代理对象
target = interceptor.plugin(target)) {
interceptor = (Interceptor)var2.next();
}
return target;
}
点进去
default Object plugin(Object target) {
return Plugin.wrap(target, this);
}
到这里我们是不是发现很熟悉,没错,这就是我们上面示例中重写的方法,而plugin方法是接口中的一个默认方法。
这个方法是关键,我们进去看看:
public static Object wrap(Object target, Interceptor interceptor) {
// 获取到我们加注解的类
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
// 被代理对象,如 Executor,StatementHandler ……
Class<?> type = target.getClass();
// 被代理对象接口
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
if (interfaces.length > 0) {
// 使用JDK 动态代理,而这个对象必须要有接口,所以自定义插件一定要实现 Interceptor 接口
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
// 返回被代理的对象
return target;
}
// 而最终执行的intercept方法,就是我们上面示例中重写的方法。
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// 被拦截的方法签名
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
//如何当前方法属于被拦截方法,则执行代理对象的方法 intercept
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);
}
}
可以看到这个方法的逻辑也很简单,但是需要注意的是MyBatis插件是通过JDK动态代理来实现的,而JDK动态代理的条件就是被代理对象必须要有接口,这一点和Spring中不太一样,Spring中是如果有接口就采用JDK动态代理,没有接口就是用CGLIB动态代理。
正因为MyBatis的插件只使用了JDK动态代理,所以我们上面才强调了一定要实现Interceptor接口。
而其他三个也是这样进行拦截处理的,同样是 Configuration 类中进行处理
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) {
executorType = executorType == null ? defaultExecutorType : 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);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
1.4、执行器是如何执行SQL的?
在映射器Mapper的动态代理中,我们知道最后就是通过SqlSession对象去运行对象的SQL。通过跟踪代码,在Mapper的代理对象(即MapperProxy的实例)中的invoke()方法中,最后是执行了mapperMethod.execute(sqlSession, args);,而在execute()方法中,是采用了命令模式跳转到需要的方法中,在上文中,我们看了executeForMany()方法,其中有下面这段代码:
if (method.hasRowBounds()) {
RowBounds rowBounds = method.extractRowBounds(args);
result = sqlSession.selectList(command.getName(), param, rowBounds);
} else {
result = sqlSession.selectList(command.getName(), param);
}
可以看到,最终就是通过SqlSession对象去运行对象的SQL,在单线程环境下,SqlSession的实现类是DefaultSqlSession,它的selectList()方法如下:
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
这里用到执行器Executor的query()方法,定位到Executor的实现类BaseExecutor,代码如下:
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
queryStack++;
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
上面代码中的doQuery(ms, parameter, rowBounds, resultHandler, boundSql);是由Executor的三种类型来实现的,默认是SIMPLE,即SimpleExecutor,如下:
@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
//从该映射器节点MappedStatement中获取配置信息Configuration
Configuration configuration = ms.getConfiguration();
//根据Configuration构建StatementHandler
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
//对SQL编译和参数进行初始化
stmt = prepareStatement(handler, ms.getStatementLog());
//使用StatementHandler的query()方法,把ResultHandler传递进去
return handler.query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
Connection connection = getConnection(statementLog);
//调用StatementHandler的prepare()进行了预编译和基础的设置
stmt = handler.prepare(connection, transaction.getTimeout());
//通过StatementHandler的parameterize()来设置参数
handler.parameterize(stmt);
return stmt;
}
根据上面代码,可以看到最终的操作落到StatementHandler上。
2、StatementHandler——数据库会话器
2.1、StatementHandler介绍
数据库会话器就是专门处理数据库会话的,在Configuration中,mybatis通过如下方式生成StatementHandler:
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;
}
//对象适配器,它的作用是给3个接口对象的使用提供一个统一且简易的适配器
private final StatementHandler delegate;
public RoutingStatementHandler(Executor executor,
MappedStatement ms,
Object parameter,
RowBounds rowBounds,
ResultHandler resultHandler,
BoundSql boundSql) {
switch (ms.getStatementType()) {
case STATEMENT:
delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
case PREPARED:
delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
case CALLABLE:
delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
default:
throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
}
}
很显然,mybatis是通过RoutingStatementHandler的对象来创建真实对象的,RoutingStatementHandler不是真实的服务对象,它是通过适配器模式来找到对应的StatementHandler来执行的。
在mybaits中,RoutingStatementHandler分为3种:
SimpleStatementHandler、PreparedStatementHandler、CallableStatementHandler,他们分别对应的是JDBC的Statement、PreparedStatement(预编译处理)和CallableStatement(存储过程处理)。
2.2、StatementHandler的执行过程(以PreparedStatementHandler为例)
我们在Executor部分说到,Executor在执行查询时(如doQuery()方法)会执行StatementHandler的prepare、parameterize和query方法。其中PreparedStatementHandler的prepare方法如下:
public abstract class BaseStatementHandler implements StatementHandler {
//......
@Override
public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
ErrorContext.instance().sql(boundSql.getSql());
Statement statement = null;
try {
//对SQL进行预编译,做一些基础配置,由不同的StatementHandler的实现
statement = instantiateStatement(connection);
setStatementTimeout(statement, transactionTimeout);
setFetchSize(statement);
return statement;
} catch (SQLException e) {
closeStatement(statement);
throw e;
} catch (Exception e) {
closeStatement(statement);
throw new ExecutorException("Error preparing statement. Cause: " + e, e);
}
}
//...
}
接着,通过parameterize()方法设置参数,如下:
@Override
public void parameterize(Statement statement) throws SQLException {
//显然使用了ParameterHandler
parameterHandler.setParameters((PreparedStatement) statement);
}
最后执行query()查询方法——执行SQL返回结果,如下:
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
//java.sql.PreparedStatement 与数据库交互
PreparedStatement ps = (PreparedStatement) statement;
ps.execute();
//包装结果集ResultSet
return resultSetHandler.handleResultSets(ps);
}
一条查询SQL的执行过程?
Executor先调用StatementHandler的prepare()方法预编译SQL,同时设置一些基本运行的参数。
然后用parameterize()方法启用ParameterHandler设置参数,完成预编译,执行查询,update()也是这样的。
如果是查询,mybatis会使用ResultSetHandler封装结果返回给调用者。
3、ParameterHandler——参数处理器
- mybatis通过ParameterHandler对预编译语句进行参数设置,它的作用是完成对预编译参数的设置。
- mybatis为ParameterHandler提供了一个实现类DefaultParameterHandler。
- 设置参数过程中,是从parameterObject对象中取到参数,然后使用TypeHandler转换参数,如果有设置,那么它会根据签名注册的TypeHandler对参数进行处理。
- TypeHandler是在mybatis初始化时,注册在Configutation里面的,需要时就可以直接拿来用了。
4、ResultSetHandler——结果处理器
- ResultSetHandler是组装结果集返回的。
- mybatis为ResultSetHandler提供了一个实现类DefaultResultSetHandler,在默认情况下都是通过这个类进行处理的。
- 它涉及使用JAVASSIST(或者CGLIB)作为延迟加载。
- 然后通过TypeHandler和ObjectFactory进行组装结果再返回。
插件执行流程
时序图
假如一个对象被代理很多次
一个对象是否可以被多个代理对象进行代理?也就是说同一个对象的同一个方法是否可以被多个拦截器进行拦截?
答案是肯定的,因为被代理对象是被加入到list,所以我们配置在最前面的拦截器最先被代理,但是执行的时候却是最外层的先执行。
具体点:
假如依次定义了三个插件:插件A,插件B和插件C。
那么List中就会按顺序存储:插件A,插件B和插件C,而解析的时候是遍历list,所以解析的时候也是按照:插件A,插件B和插件C的顺序,但是执行的时候就要反过来了,执行的时候是按照:插件C,插件B和插件A的顺序进行执行。
PageHelper插件原理
我们上面提到,要实现插件必须要实现MyBatis提供的Interceptor接口,所以我们去找一下,发现PageHeler实现了Interceptor:
经过上面的介绍这个类应该一眼就能看懂,我们关键要看看SqlUtil的intercept方法做了什么:
这个方法的逻辑比较多,因为要考虑到不同的数据库方言的问题,所以会有很多判断,我们主要是关注PageHelper在哪里改写了sql语句,上图中的红框就是改写了sql语句的地方:
这里面会获取到一个Page对象,然后在爱写sql的时候也会将一些分页参数设置到Page对象,我们看看Page对象是从哪里获取的:
我们看到对象是从LOCAL_PAGE对象中获取的,这个又是什么呢?
这是一个本地线程池变量,那么这里面的Page又是什么时候存进去的呢?
这就要回到我们的示例上了,分页的开始必须要调用:
PageHelper.startPage(0,10);
这里就会构建一个Page对象,并设置到ThreadLocal内。
为什么PageHelper只对startPage后的第一条select语句有效
这个其实也很简单哈,但是可能会有人有这个以为,我们还是要回到上面的intercept方法:
在finally内把ThreadLocal中的分页数据给清除掉了,所以只要执行一次查询语句就会清除分页信息,故而后面的select语句自然就无效了。
不通过插件能否改变MyBatis的核心行为
上面我们介绍了通过插件来改变MyBatis的核心行为,那么不通过插件是否也可以实现呢?
答案是肯定的,官网中提到,我们可以通过覆盖配置类来实现改变MyBatis核心行为,也就是我们自己写一个类继承Configuration类,然后实现其中的方法,最后构建SqlSessionFactory对象的时候传入自定义的Configuration方法:
SqlSessionFactory build(MyConfiguration)
当然,这种方法是非常不建议使用的,因为这种方式就相当于在建房子的时候把地基抽出来重新建了,稍有不慎,房子就要塌了。