0
点赞
收藏
分享

微信扫一扫

Mybatis 四大组件及自定义插件原理解析

杨沐涵 2022-02-18 阅读 72


一、四大组件:

1.Executor​:Mybatis的SQL执行器,Mybatis中对数据库所有增删改查操作都由其完成,他的实现类如下:

Mybatis 四大组件及自定义插件原理解析_apache

BaseExecutor中定义执行流程及通用的处理逻辑,具体方法由子类实现,是典型的模板方法模式的应用。SimpleExecutor是基础的Executor,能够完成基本的增删改查,ResueExecutor对JDBC中的Statement对象做了缓存,当执行相高的sql语句时,直接从缓存中取出Statement对象进行复用,避免了频繁创建和销毁Satement对象,从而提升系统性能,这是享元思想的应用。BatchExecutor则会对调用同一个Mappr执行的update、insert、delete操作,调用Statement对象的批量操作功能。另外,我们知道MyBatis支持一级缓存和二级缓存,当MyBatis开启了二级缓存功能时,会使用 CachingExecutor 对 SimpleExecutor、BatchExecutor、ReuseExecutor进行装饰,为查询操作增加二级缓存功能,这是装饰器模式的应用。

2.StatementHandler​:封装了对JDBC Statement对象的操作,比如为Statement对象设置参数,调用Statement接口提供的方法与数据库交互等,他的实现类如下:

Mybatis 四大组件及自定义插件原理解析_sql_02

BaseStatementHandler 的三个子类基本上可以顾名思义,不多讲,RoutingStatementHandler 会根据Mapper中配置的StatementType的值创建对应的StatementHandler实现,这个值默认PREPARED,因为我没有做任何设置,断点走到了PREPARED逻辑,如下:

Mybatis 四大组件及自定义插件原理解析_sql_03

3.ResultSetHandler​:封装了对JDBC中的ResultSet对象操作,当执行SQL类型为SELECT语句时,ResultSetHandler用于将查询结果转换成Java对象。

4.ParameterHandler​:当Mybatis框架使用的Statement类型为CallableStatement和PreparedStatement时,ParameterHandler用于为Statement对象参数占位符设置值。

二、自定义插件

Mybatis允许用户通过自定义拦截器的方式改变sql的执行行为,例如在sql执行时追加sql分页语法,达到分页查询目的。用户自定义的插件只能对上文中所说的四大对象的方法进行拦截,这些方法分别是(包含方法重载):

Executor​:update、query、flushStatements、commit、rollback、getTransaction、close、isClosed

StatementHandler​: prepare、parameterize、batch、update、query

ParameterHandler​: getParameterObject、setParameters

ResultSetHandler​:handleResultSets、handleOutputParameters

Mybatis的插件实际上是一个拦截器,Configuration中维护了InterceptorChain对象,下面代码分别体现了InterceptorChain 与四大对象的关系:

public class Configuration {

protected final InterceptorChain interceptorChain = new InterceptorChain();

/**
* @since 3.2.2
*/
public List<Interceptor> getInterceptors() {
return interceptorChain.getInterceptors();
}

public void addInterceptor(Interceptor interceptor) {
interceptorChain.addInterceptor(interceptor);
}

public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
// 创建ParameterHandler代理对象
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 = (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 = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler;
}

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) {
executor = new CachingExecutor(executor);
}
// 创建Executor代理对象
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
}

下面来看看InterceptorChain 的代码结构:

public class InterceptorChain {

private final List<Interceptor> interceptors = new ArrayList<>();

// 调用所有拦截器对象的plugin()执行拦截逻辑
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);
}

}

InterceptorChain :拦截器链,用到了Interceptor,再来看看Interceptor的代码结构:

public interface Interceptor {

Object intercept(Invocation invocation) throws Throwable;

// 这种接口中写默认方法(还可以写静态方法),要得益于JDK8,8以前是不支持的
default Object plugin(Object target) {
// 这里会返回一个代理对象
return Plugin.wrap(target, this);
}

default void setProperties(Properties properties) {
// NOP
}

}

这个Plugin实现了 InvocationHandler 接口,懂JDK动态代理的对这个InvocationHandler应该不陌生。。。

public class Plugin implements InvocationHandler {

private final Object target;
private final Interceptor interceptor;
private final Map<Class<?>, Set<Method>> signatureMap;

private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
this.target = target;
this.interceptor = interceptor;
this.signatureMap = signatureMap;
}

public static Object wrap(Object target, Interceptor interceptor) {
// 调用getSignatureMap()获取自定义插件,通过Interceptors注解指定的方法
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
if (interfaces.length > 0) {
// 这一段逻辑就是JDK动态代理创建代理对象的逻辑,JDK动态代理要求被代理的对象实现接口(这一点与CGLIB代理区别很大)
// 第一个参数:类加载器,第二个参数:目标类实现的接口,第三个参数:实现了InvocationHandler的对象
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}

@Override
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);
}
}

// ......

}

Mybatis 默认给了一个自定义插件的示例:

@Intercepts({})
public class ExamplePlugin implements Interceptor {
private Properties properties;

@Override
public Object intercept(Invocation invocation) throws Throwable {
return invocation.proceed();
}

@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}

@Override
public void setProperties(Properties properties) {
this.properties = properties;
}

public Properties getProperties() {
return properties;
}
}

下面,我们根据示例来自定义一个打印慢查询sql插件,代码示例如下:

package org.apache.ibatis.submitted.use_actual_param_name;
import org.apache.ibatis.binding.MapperMethod;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import java.sql.Statement;
import java.util.*;

/**
* @Author: francis
* @Description: 自定义一个慢查询统计插件,可以定义多个@Signature注解,因为@Intercepts支持数组,也就是说可以同时拦截多个方法
* @Date: 2020/4/14 20:04
*/
@Intercepts({@Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class})})
public class FrancisInterceptor implements Interceptor {

// 时长限制,超过这个时间则认为查询较慢
private long limitTime;

@Override
public Object intercept(Invocation invocation) throws Throwable {
long startTime = System.currentTimeMillis();
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
Object proceed = invocation.proceed();
long endTime = System.currentTimeMillis();
long costTime = endTime - startTime;
// 假设查询时间大于10毫秒,则认为查询较慢,打印日志
if (costTime > limitTime) {
BoundSql boundSql = statementHandler.getBoundSql();
printFormattedSql(boundSql, costTime);
}
return proceed;
}

@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}

@Override
public void setProperties(Properties properties) {
limitTime = Long.parseLong((String) properties.get("limitTime"));
}

/**
* 格式化打印sql
* @param boundSql
* @param millis
*/
private void printFormattedSql(BoundSql boundSql, long millis) {
StringBuilder sb = new StringBuilder();
sb.append("执行较慢的sql为:").append(boundSql.getSql()).append("\n参数值分别为:");
final Set<Map.Entry<String, String>> entrySet = ((MapperMethod.ParamMap) boundSql.getParameterObject()).entrySet();
Object[] objects = entrySet.toArray();
for (int i = 0; i < objects.length; i++) {
final String str = objects[i].toString();
// 过滤掉 param1=xxx 这种
if (str.contains("param")) {
continue;
}
sb.append(str).append(", ");
}

sb.append("\n耗时为:").append(millis).append("ms");
System.out.println(sb.toString());
}
}

这个插件拦截的是 StatementHandler 的 query(),那么怎么确定@Signature注解中args参数呢?直接打开 StatementHandler.java,找到对应方法,查看有哪些参数即可,如下:

Mybatis 四大组件及自定义插件原理解析_sql_04

编写完FrancisInterceptor代码后,在mybatis-config.xml中配置拦截器,这个拦截器最好放置在紧挨着​​<configuration>​​的位置,否则可能编译器会提示错误,如下:

Mybatis 四大组件及自定义插件原理解析_拦截器_05

<plugins>
<plugin interceptor="org.apache.ibatis.submitted.use_actual_param_name.FrancisInterceptor">
<!-- 设置一个时长 -->
<property name="limitTime" value="10"/>
</plugin>
</plugins>

这个拦截器是什么时候添加到Configuration中的呢?这个操作是在org.apache.ibatis.builder.xml.XMLConfigBuilder#pluginElement方法中做的,如下

Mybatis 四大组件及自定义插件原理解析_sql_06

然后执行单测(直接改造的mybatis源码的单测org.apache.ibatis.submitted.use_actual_param_name.UseActualParamNameTest),查看控制台打印的结果,如下:

@Test
void tests() {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
Student student = mapper.getStudent("1001", "francis");
assertNotNull(student);
}
}

Mybatis 四大组件及自定义插件原理解析_拦截器_07

下面总结一下动态代理对象的创建过程(依据本例而来,拦截不同的组件效果有点不一样,但是流程大致一样):


  1. 执行sql时会一直调用到org.apache.ibatis.executor.SimpleExecutor#doQuery方法,这时会尝试创建一个StatementHandler对象。
  2. Configuration内部维护了一个InterceptorChain,在newStatementHandler()方法中会调用InterceptorChain的pluginAll()方法。
  3. pluginAll()方法会调用拦截器(或者说自定义拦截器)的plugin()方法。
  4. plugin()方法又会调用Plugin.wrap()方法,这个方法实则使用JDK动态代理创建并返回代理对象。

代码大致流程:

org.apache.ibatis.submitted.use_actual_param_name.UseActualParamNameTest#tests  ->
org.apache.ibatis.executor.SimpleExecutor#doQuery ->
org.apache.ibatis.session.Configuration#newStatementHandler ->
org.apache.ibatis.submitted.use_actual_param_name.FrancisInterceptor#plugin ->
org.apache.ibatis.plugin.Plugin#wrap

然后再总结一下插件拦截逻辑的执行流程:


  1. 执行sql时会调用Executor的query()方法,一直调到org.apache.ibatis.executor.SimpleExecutor#doQuery方法,
  2. doQuery方法中会创建一个StatementHandler的代理对象,并调用Plugin的invoke()方法。
  3. Plugin的invoke()方法会调用自定义拦截器的intercept()方法执行拦截逻辑。
  4. 执行完所有操作后,将结果返回给SqlSession 。

代码大致流程:

org.apache.ibatis.submitted.use_actual_param_name.UseActualParamNameTest#tests  -> 
org.apache.ibatis.executor.BaseExecutor#query() ->
org.apache.ibatis.executor.SimpleExecutor#doQuery ->
org.apache.ibatis.plugin.Plugin#invoke ->
org.apache.ibatis.submitted.use_actual_param_name.FrancisInterceptor#intercept ->
org.apache.ibatis.executor.BaseExecutor#query()



举报

相关推荐

0 条评论