Mybatis源码解析(一):MyBatis 是如何执行的
Mybatis源码解析(二):Mybatis 插件原理
Mybatis源码解析(三):Spring 整合 Mybatis 原理
MyBatis 框架允许用户通过自定义拦截器的方式改变 SQL 的执行行为,例如在 SQL 执行时追加 SQL 分页语法,从而达到简化分页查询的目的。用户自定义的拦截器也被称为 MyBatis 插件,本文就来分析一下 MyBatis 插件的实现原理以及如何开发一个慢SQL日志记录插件。
1 原理:Mybatis 插件源码分析
1.1 Configuration 的工厂方法
MyBatis 的插件实际上就是一个拦截器,Configuration 类中维护了一个 InterceptorChain 的实例,用于存放所有注册的拦截器,用户自定义的插件只能对 MyBatis 中的4 种组件的方法进行拦截,这 4 种组件及方法如下:
- Executor: update, query, flushStaterments, commit, rollback, getTransaction, close, isClosed
- ParameterHandler: getParameterObject, setParameters
- ResultSetHandler: handleResultSets, handleOutputParameters
- StatementHandler: prepare, parameterize, batch, update, query
在 Mybatis源码解析(一):MyBatis 是如何执行的 这篇文章中,提到 Configuration 组件有 3 个作用:
- 用于描述 MyBatis 配置信息,项目启动时,MyBatis 的所有配置信息都被转换为
Configuration对象; - 作为中介者简化 MyBatis 各个组件之间的交互,解决了各个组件错综复杂的调用关系;
- 作为
Executor、ParameterHandler、ResultSetHandler、StatementHandler组件的工厂创建这些组件的实例。
MyBatis 使用工厂方法创建 Executor、ParameterHandler、ResultSetHandler、StatementHandler 组件的实例,在工厂方法中执行拦截逻辑:
public class Configuration {
// 拦截器链(用来支持插件的插入)
protected final InterceptorChain interceptorChain = new InterceptorChain();
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
...
// 为执行器增加拦截器(插件),以启用各个拦截器的功能
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
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);
// 将 ResultHandler 处理器交给拦截器链进行替换,以便拦截器链中的拦截器能注入行为
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;
}
}
在 Configuration 类的 newParameterHandler()、newResutSetHandler()、newStatementHandler()、newExecutor() 这些工厂方法中,都调用了 InterceptorChain 对象的 pluginAll() 方法,返回 ParameterHandler、ResultSetHandler、StatementHandler 、Executor 对象的代理对象,拦截逻辑都是在代理对象中完成的。
1.2 拦截器链 InterceptorChain
看下 InterceptorChain 类的实现:
public class InterceptorChain {
// 拦截器链
private final List<Interceptor> interceptors = new ArrayList<>();
/**
* target是支持拦截的几个类的实例。该方法依次向所有拦截器插入这几个类的实例
* 如果某个插件真的需要发挥作用,则返回一个代理对象即可。如果不需要发挥作用,则返回原对象即可
* 向所有的拦截器链提供目标对象,由拦截器链给出替换目标对象的对象
*
* @param target 目标对象,是MyBatis中支持拦截的几个类(ParameterHandler、ResultSetHandler、StatementHandler、Executor)的实例
* @return 用来替换目标对象的对象
*/
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 类中通过一个 List 对象维护所有的拦截器实例,在 InterceptorChain 的 pluginAll() 方法中,会调用所有拦截器实例的 plugin() 方法,该方法返回一个目标对象的代理对象。
1.3 MyBatis 插件公共接口 Interceptor
MyBatis 中所有用户自定义的插件都必须实现 Interceptor 接口:
public interface Interceptor {
/**
* 该方法内是拦截器拦截到目标方法时的操作
* @param invocation 拦截到的目标方法的信息
* @return 经过拦截器处理后的返回结果
* @throws Throwable
*/
Object intercept(Invocation invocation) throws Throwable;
/**
* 用返回值替代入参对象,代理其行为
* 通常情况下,可以调用Plugin的warp方法来完成,因为warp方法能判断目标对象是否需要拦截,并根据判断结果返回相应的对象来替换目标对象
* @param target MyBatis 传入的支持拦截的几个类(ParameterHandler、ResultSetHandler、StatementHandler、Executor)的实例
* @return 如果当前拦截器要拦截该实例,则返回该实例的代理;如果不需要拦截该实例,则直接返回该实例本身
*/
default Object plugin(Object target) {
return Plugin.wrap(target, this);
}
/**
* 设置拦截器的属性
* @param properties 要给拦截器设置的属性
*/
default void setProperties(Properties properties) {}
Interceptor 接口中定义了3个方法:
- intercept() 方法用于定义拦截逻辑,该方法会在目标方法调用时执行;
- plugin() 方法用于创建
Executor、ParameterHandler、ResultSetHandler、StatementHandler的代理对象; - setProperties() 方法用于设置插件的属性值。
1.4 Invocation 类封装目标对象与方法
其中,interceptor() 接收一个 Invocation 对象作为参数,Invocation 对象中封装了目标对象的方法及参数信息,Invocation 类的实现代码如下:
// 代表了一个调用的详细信息
public class Invocation {
// 目标对象
private final Object target;
// 目标方法
private final Method method;
// 方法参数
private final Object[] args;
// 用于执行目标方法的逻辑
public Object proceed() throws InvocationTargetException, IllegalAccessException {
return method.invoke(target, args);
}
}
1.5 代理工具类 Plugin
为了便于用户创建 Executor、ParameterHandler、ResultSetHandler 、StatementHandler 实例的代理对象,MyBatis 中提供了一个 Plugin 工具类:
public class Plugin implements InvocationHandler {
// 被代理对象
private final Object target;
// 拦截器
private final Interceptor interceptor;
// 拦截器要拦截的所有的类,以及类中的方法
private final Map<Class<?>, Set<Method>> signatureMap;
/**
* 根据拦截器的配置来生成一个对象用来替换被代理对象,该方法用于简化动态代理对象的创建
* @param target 被代理对象
* @param interceptor 拦截器
* @return 用来替换被代理对象的对象
*/
public static Object wrap(Object target, Interceptor interceptor) {
// 得到拦截器interceptor要拦截的类型与方法
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
// 被代理对象的类型
Class<?> type = target.getClass();
// 逐级寻找被代理对象类型的父类,将父类中需要被拦截的全部找出
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
// 只要父类中有一个需要拦截,说明被代理对象是需要拦截的
if (interfaces.length > 0) {
// 创建并返回一个代理对象,是Plugin类的实例
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
// 直接返回原有被代理对象,这意味着被代理对象的方法不需要被拦截
return target;
}
/**
* 代理对象的拦截方法,当被代理对象中方法被触发时会进入这里
* @param proxy 代理类
* @param method 被触发的方法
* @param args 被触发的方法的参数
* @return 被触发的方法的返回结果
* @throws Throwable
*/
@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);
}
}
}
Plugin 类实现了 InvocationHandler 接口,即采用 JDK 内置的动态代理方式创建代理对象。Plugin 类中维护了 Executor、ParameterHandler、ResultSetHandler、StatementHandler 类的实例,以及用户自定义的拦截器实例和拦截器中通过 @Intercepts 注解指定的拦截方法。Plugin 类的 invoke() 方法会在调用目标对象的方法时执行,在 invoke() 方法中首先判断该方法是否被 @Intercepts 注解指定为被拦截的方法,如果是,则调用用户自定义拦截器的 intercept() 方法,并把目标方法信息封裝成 Invocation 对象作为 intercept() 方法的参数。
Plugin 类中还提供了一个静态的 wrap() 方法,该方法用于简化动态代理对象的创建。
@Intercepts 注解用于修饰拦截器类,告诉拦截器要对哪些组件的方法进行拦截,下面是 @Intercepts 注解的一个使用案例 (拦截 Executor 组件的 query() 方法):
@Intercepts({
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class})
})
接下来我们就来了解一下 Plugin 类的 getSignatureMap() 方法解析 @Intercepts 注解的过程,代码如下:
private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
// 获取拦截器的 Intercepts 注解信息
Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
// issue #251
if (interceptsAnnotation == null) {
throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());
}
// 将Intercepts注解的value信息取出来,是一个 Signature 数组
Signature[] sigs = interceptsAnnotation.value();
// 将Signature数组数组放入一个Map中,键为Signature注解的type类型,值为该类型下的方法集合
Map<Class<?>, Set<Method>> signatureMap = new HashMap<>();
// 对所有Signature注解进行遍历,把Signature注解指定拦截的组件及方法添加到Map中
for (Signature sig : sigs) {
Set<Method> methods = signatureMap.computeIfAbsent(sig.type(), k -> new HashSet<>());
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;
}
在 Plugin 类的 getSignatureMap() 方法中,首先获取 @Intercepts 注解,然后获取 @Intercepts 注解中配置的所有 Signature 注解,接着对所有的 Signature 注解信息进行遍历,将 Signature 注解中指定要拦截的组件及方法添加到 Map 对象中,其中 Key 为 Executor、ParameterHandler、ResutSetHandler、StatementHandler 对应的 Class 对象,Value 为拦截的所有方法对应的 Method 对象数组。
1.6 自定义 MyBatis 插件一般流程
当我们需要自定义一个 MyBatis 插件时,只需要实现 Interceptor 接口,在 intercept() 方法中编写拦截逻辑,通过 plugin() 方法返回一个动态代理对象:
@Intercepts({
@Signature(type = xxx.class, method = "xxx", args = {xxx.class, xxx.class, ...})
})
@Component
public class MybatisInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// TODO 自定义拦截逻辑
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
}
1.7 插件代理流程总结
以 Executor 为例,介绍该组件的代理流程。
SqlSession 是 MyBatis 中提供的面向用户的操作数据库的接口,而真正执行 SQL 操作的是 Executor 组件,MyBatis 通过工厂模式创建 Executor 实例,Configuration 类中提供了一个 newExecutor() 工厂方法,该方法返回的实际上是一个 Executor 的动态代理对象,其代理流程如下:
SqlSession中会调用Configuration类提供的newExecutor()工厂方法创建Executor对象;Configuration类中通过一个InterceptorChain对象维护了用户自定义的拦截器链。newExecutor()工厂方法中调用InterceptorChain对象的pluginAll()方法;InterceptorChain对象的pluginAll()方法中会调用自定义拦截器的plugin()方法;- 自定义拦截器的
plugin()方法是由我们来编写的,通常会调用Plugin类的wrap()静态方法创建一个代理对象。
2 案例:慢 SQL 日志记录插件开发
2.1 注册自定义插件
mybatis-config.xml 配置文件中注册实现的插件:
<plugins>
<plugin interceptor="com.example.learn.mybatis.plugin.SlowSqlInterceptor">
<property name="limitSecond" value="2"/>
</plugin>
</plugins>
2.1 实现自定义插件
com.example.learn.mybatis.plugin.SlowSqlInterceptor.java
@Slf4j
@Intercepts({
@Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}),
@Signature(type = StatementHandler.class, method = "update", args = {Statement.class}),
@Signature(type = StatementHandler.class, method = "batch", args = {Statement.class})
})
public class SlowSqlInterceptor implements Interceptor {
// 执行时间阈值,超过该时间,记录慢 SQL
private Integer limitSecond;
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object target = invocation.getTarget();
long begin = System.currentTimeMillis();
StatementHandler statementHandler = (StatementHandler) target;
try {
// 执行原有逻辑
return invocation.proceed();
} finally {
long end = System.currentTimeMillis();
// 判断超时
if ((end - begin) > limitSecond * 1000) {
// 使用反射工具包装,方便取对象属性 delegate.mappedStatement
MetaObject metaObject = MetaObject.forObject(statementHandler, SystemMetaObject.DEFAULT_OBJECT_FACTORY, SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY, new DefaultReflectorFactory());
// MappedStatement 是 MyBatis 中描述 Statement 的对象
MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
// 方法名称
String methodName = mappedStatement.getId();
// 类型
String sqlType = mappedStatement.getSqlCommandType().toString();
// MyBatis 中描述 SQL 文本的对象
BoundSql boundSql = statementHandler.getBoundSql();
String sql = boundSql.getSql();
// 参数map
Object parameterObject = boundSql.getParameterObject();
// 参数列表
List<ParameterMapping> parameterMappingList = boundSql.getParameterMappings();
// 格式化sql语句,去除换行符,替换参数
sql = formatSQL(sql, parameterObject, parameterMappingList);
// 控制台打印日志
log.error("执行 SQL:[ {} ], 执行耗时[ {} ms ]", sql, (end - begin));
}
}
}
private String formatSQL(String sql, Object parameterObject, List<ParameterMapping> parameterMappingList) {
if (sql == null || sql.length() == 0) {
return "";
}
// 去除换行符
sql = sql.replaceAll("[\\s\n ]+", " ");
// 替换参数
Map<String, Object> params = (Map<String, Object>) parameterObject;
for (ParameterMapping pm : parameterMappingList) {
if (pm.getMode().name().equals("IN")) {
sql = sql.replaceFirst("\\?", params.get(pm.getProperty()).toString());
}
}
return sql;
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
String limitSecond = (String) properties.get("limitSecond");
this.limitSecond = Integer.parseInt(limitSecond);
}
}