Mybatis-插件源码分析

插件作用:在不修改mybatis代码和api调用方式的前提下,对调用进行拦截,增加一些特殊的需求

插件可以拦截的对象:

  • Executor
  • StatementHandler
  • ParameterHandler
  • ResultSetHandler

插件的实现原理是动态代理,使用代理解决插件的拦截需要解决四个问题:

1、 代理类什么时候创建

2、 代理类怎么被创建

3、 被代理之后调用的流程是怎样的

4、 多个插件时执行顺序是怎样的

1、自定义一个插件

在分析原理之前我们要先清楚怎么使用,那我们怎么去自定义一个插件呢?

只需要做两件事:

  1. 编写自定义的插件类
  2. 在全局文件中将该插件配置起来
1.1 编写插件类:

插件类需要实现Interceptor接口,该接口有三个方法:

1
2
3
4
5
6
7
8
9
10
public interface Interceptor {
Object intercept(Invocation invocation) throws Throwable;

default Object plugin(Object target) {
return Plugin.wrap(target, this);
}
default void setProperties(Properties properties) {
// NOP
}
}

我们在编写自定义插件时,重点需要实现的方法是intercept(Invocation invocation)方法,该方法就是插件的核心逻辑,我们需要增加的逻辑或者流程是在这个方法中进行实现的。

plugin方法主要负责生成代理类对象,mybatis已经给出了默认的实现,没有特殊的情况一般不需要重写

setProperties方法负责设置插件的属性,有些在配置的时候会设置一些属性(在全局配置文件中),在解析配置文件的时候,需要使用该方法将xml中配置的插件属性设置到插件对象中。该方法默认是没有逻辑的,如果自定义的插件有对外可配置的属性,那么需要重写这个方法。

1.2 添加注解

插件类需要添加@Intercepts注解,注解中需要指定该插件拦截的对象类型,拦截哪些方法,以分页插件为例:

1
2
3
4
@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}),
})

该分页插件拦截将拦截 Executor 类的 query 方法,由于在该类存在重载的query方法,所以这边需要提供两个@Signature。

1.3 配置插件

在全局配置文件中进行以下配置:

1
2
3
4
5
6
7
<plugins>
<plugin interceptor="com.pd.interceptor.SQLInterceptor">
<property name="panda" value="betterme" />
</plugin>
<plugin interceptor="com.pd.interceptor.MyPageInterceptor">
</plugin>
</plugins>

2、插件运行原理分析

2.1 插件配置的解析

插件配置在全局配置文件中,根据之前的源码分析我们已经清楚整个解析阶段的流程,这里我直接定位到解析逻辑,即XMLConfigBuilder类的parseConfiguration方法:

1
2
3
4
5
6
7
8
private void parseConfiguration(XNode root) {
try {
...
// 插件
pluginElement(root.evalNode("plugins"));
...
}
}

跟进:

1
2
3
4
5
6
7
8
9
10
11
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).getDeclaredConstructor().newInstance();
interceptorInstance.setProperties(properties); // 设置插件属性
configuration.addInterceptor(interceptorInstance);
}
}
}

可以看到根据interceptor标签解析插件类型以及属性,实例化之后调用setProperties方法设置属性,最后将插件add到Configuration对象中,我们继续跟进,看配置对象是怎么存放插件实例的:

1
2
3
4
5
//Configuration
protected final InterceptorChain interceptorChain = new InterceptorChain();
public void addInterceptor(Interceptor interceptor) {
interceptorChain.addInterceptor(interceptor);
}

Configuration将插件实例放到了interceptorChain成员属性中,这个叫拦截器链的成员,内部封装了一个list用于存放插件实例。看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class InterceptorChain {
private final List<Interceptor> interceptors = new ArrayList<>();
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);
}
}

注意pluginAll方法在接下来会经常看到,它主要是通过一个for循环调用所有插件实例的plugin方法,对目标对象进行层层代理。

2.2 代理对象的创建和执行

这里我还是以分页插件为例进行分析,分页插件主要是对Excutor对象的query方法进行拦截。在创建Excutor之后就要对其进行拦截生成代理对象实现加入插件的逻辑,所以先想想Excutor对象是什么时候创建的?

之前的源码分析也分析了Excutor对象的创建是在openSession方法调用时,定位代码:

1
2
3
4
5
6
7
8
9
10
11
//DefaultSqlSessionFactory
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
...
// 根据事务工厂和默认的执行器类型,创建执行器 >>
final Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
}
...
}
1
2
3
4
5
6
7
8
9
10
11
12
//Configuration
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
...
executor = new SimpleExecutor(this, transaction);
// 二级缓存开关,settings 中的 cacheEnabled 默认是 true
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
// 植入插件的逻辑,至此,四大对象已经全部拦截完毕
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}

这里创建出一个SimpleExecutor之后,先对其进行缓存装饰(如果需要的话),然后调用拦截器链的pluginAll方法生成代理对象。

根据以上的分析我们知道pluginAll方法是调用拦截器连中的所有插件实例的plugin方法,而该方法的默认实现是

1
2
3
default Object plugin(Object target) {
return Plugin.wrap(target, this); //>>
}

跟进warp方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
public static Object wrap(Object target, Interceptor interceptor) {
// 获取插件类上的Intercepts注解信息,生成一个signatureMap
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;
}

该方法首先获取插件类上的@Intercepts注解信息,生成一个signatureMap,mybatis从这个map中可以知道当前的这个插件需要拦截哪些对象的哪些方法。

然后这里使用JDK动态里来生成代理对象,JDK动态代理的三个参数已经接触过很多次了,最重要的是第三个参数触发管理类对象,这边可以看到触发管理类就是Plugin类,那么Plugin肯定实现了InvocationHandler接口,我们找到它实现的invoke方法:

1
2
3
4
5
6
7
8
9
10
11
12
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// 从map中获取被拦截对象的类型对应的拦截方法集合
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);
}
}

首先从map中获取被拦截对象的类型对应的拦截方法集合,如果取到方法结合且当前方法在集合中,也就是说当前执行的方法需要拦截,那么调用当前拦截器的intercept方法

判断不用进行拦截,直接通过反射执行当前方法。

intercept方法需要传入一个Invocation对象,这个对象是干嘛用的呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Invocation {

private final Object target;
private final Method method;
private final Object[] args;

public Invocation(Object target, Method method, Object[] args) {
this.target = target;
this.method = method;
this.args = args;
}
public Object proceed() throws InvocationTargetException, IllegalAccessException {
return method.invoke(target, args);
}
}

创建Invocation需要传入被代理的对象target、当前执行的方法method、和方法执行参数

所以我们应该能猜想到,插件逻辑执行完之后,还需要继续执行目标方法的逻辑,mybatis将目标方法的执行上下文封装到Invocation中传递给intercept方法,intercept在执行完插件逻辑之后需要调用Invocation.proceed()方法执行主线逻辑。

2.3 多插件如何代理

配置多插件时,代理对象的生成是一层套一层

多插件的执行顺序与配置顺序相反。

3、PageHelper原理

还是老规矩,先看怎么使用,在分析原理:

1
2
3
4
5
6
@GetMapping("/page-helper-test")
public List<Blog> queryBlogs(@RequestParam Integer pageNum){
PageHelper.startPage(pageNum,2);
List<Blog> res = mbService.queryBlogs();
return res;
}

只需要在调用查询服务之前,调用PageHelper.startPage()方法将分页信息传入,拦截器就能发挥作用,这是为啥呢?跟进这个方法一探究竟:

经过多次调用重载的PageHelper.startPage()方法,最终会进入一下逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {
Page<E> page = new Page<E>(pageNum, pageSize, count);
page.setReasonable(reasonable);
page.setPageSizeZero(pageSizeZero);
//当已经执行过orderBy的时候
Page<E> oldPage = getLocalPage();
if (oldPage != null && oldPage.isOrderByOnly()) {
page.setOrderBy(oldPage.getOrderBy());
}
setLocalPage(page); //>>
return page;
}

将controller传入的分页信息封装成一个Page对象,然后调用setLocalPage方法:

1
2
3
4
protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();
protected static void setLocalPage(Page page) {
LOCAL_PAGE.set(page);
}

原来分页信息存放到了一个ThreadLocal变量中,那么可以猜想,intercept方法肯定会去这个线程私有变量中去取出这个信息,再根据使用的数据库类型做相应的拦截处理,拼接sql语句。

4、应用场景分析

4.1 水平分表

场景描述:假如一张费用表按照月度拆分为12张表,当查询条件出现月份时,把select语句中的逻辑表明修改为对应月份的费用表名。

实现方式:对query、update方法进行拦截,修改sql语句

4.2 菜单权限控制

场景描述:不同角色的用户登录,查询菜单时获取到的菜单结果不同,在前段展示不同的菜单

实现方式:对query方法进行拦截,修改sql语句,加上权限过滤条件

4.3 数据脱敏

场景描述:对查询返回的敏感信息进行屏蔽,例如使用XXXX替换掉手机号的中间四位

实现方式:拦截ResultSet,对结果集脱敏