插件功能也是Mybatis框架中的一个核心功能,Mybatis提供了自定义插件功能来帮我们扩展个性化业务需求。本篇文章我们将总结Mybatis的插件机制以及如何自定义一个插件。
我们在做查询操作的时候,如果数据量很大,不可能一次性返回所有数据,一般会采用分页查询的方式,那么在Mybatis的分页是如何做的呢?其实,要实现分页,就可以使用到Mybatis的插件功能,我们可以拦截到Mybatis将要执行的SQL语句,然后动态修改其中的参数,比如加入limit限制条数等。MyBatis的插件是通过动态代理来实现的,并且会形成一个interceptorChain插件链。
下面我们先通过一个简单的分页插件来详细分析Mybatis的插件机制。
在MyBatis中,我们只能对以下四种类型的对象进行拦截
这里我们要实现动态修改或者拼接SQL语句的limit条数限制,所以可以选择拦截Executor的query方法。大体步骤如下:
具体代码如下:
package com.wsh.mybatis.mybatisdemo;
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.Field;
import java.util.Properties;
/**
* Mybatis插件机制 - 自定义简单分页插件
* 说明:
* 1、使用@Intercepts和@Signature指定需要拦截的哪个类的哪个方法;
* 2、Mybatis的插件需要执行拦截哪个类的哪个方法,使用@Intercepts注解,里面是方法的签名信息,使用@Signature定义拦截方法信息的描述;
* 3、Mybatis的插件类需要实现Interceptor接口,重写其中的方法;
* 4、重写具体的拦截逻辑intercept()方法;
* 5、plugin()方法返回代理对象;
*/
@Intercepts({@Signature(type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
public class CustomPagePlugin implements Interceptor {
private static final Logger logger = LoggerFactory.getLogger(CustomPagePlugin.class);
/**
* 拦截目标对象的目标方法
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
RowBounds rowBounds = (RowBounds) args[2];
logger.info("拦截前rowBounds对象offset=" + rowBounds.getOffset() + ", limit = " + rowBounds.getLimit());
//修改RowBounds参数中的limit
if (rowBounds != null) {
if (rowBounds.getLimit() > 2) {
Field field = rowBounds.getClass().getDeclaredField("limit");
field.setAccessible(true);
field.set(rowBounds, 2);
}
} else {
rowBounds = new RowBounds(0, 2);
args[2] = rowBounds;
}
logger.info("拦截后rowBounds对象offset=" + rowBounds.getOffset() + ", limit = " + rowBounds.getLimit());
//执行目标方法,并返回执行后的结果
return invocation.proceed();
}
/**
* 包装目标对象,为目标对象返回一个代理对象
*/
@Override
public Object plugin(Object target) {
//target: 需要包装的对象
//this: 使用当前拦截器CustomPagePlugin进行拦截
//返回为当前target创建的代理对象
return Plugin.wrap(target, this);
}
/**
* 将插件注册的时候的属性信息设置进来
*/
@Override
public void setProperties(Properties properties) {
}
}
插件逻辑写完了,我们需要在配置文件中配置一下插件才能生效,在mybatis-config.xml中添加plugins标签,并且配置我们上面实现的插件的全限定类名:
<plugins>
<plugin interceptor="com.wsh.mybatis.mybatisdemo.CustomPagePlugin">
</plugin>
</plugins>
插件编写完了,主要实现intercept(Invocation invocation)方法。那么Mybatis的插件是在什么时候被加载的呢?
如果前面有了解过Mybatis的配置信息解析的话,这里不能猜到,插件肯定也是在一开始解析XML配置,就跟mapper接口、environment等信息的解析在一块,在XMLConfigBuilder的parse()解析的时候加载的。
//org.apache.ibatis.builder.xml.XMLConfigBuilder#parseConfiguration
private void parseConfiguration(XNode root) {
try {
//issue #117 read properties first
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
注意pluginElement(root.evalNode("plugins"))方法,这个就是具体解析插件的方法。
//org.apache.ibatis.builder.xml.XMLConfigBuilder#pluginElement
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
//遍历我们在mybatis-config.xml配置的所有插件
for (XNode child : parent.getChildren()) {
//获取到具体的插件全限定类名
String interceptor = child.getStringAttribute("interceptor");
//获取插件配置的属性信息
Properties properties = child.getChildrenAsProperties();
//通过classLoaderWrapper创建一个拦截器对象
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
//设置属性
interceptorInstance.setProperties(properties);
//将拦截器添加到configuration中
configuration.addInterceptor(interceptorInstance);
}
}
}
//org.apache.ibatis.session.Configuration#addInterceptor
public void addInterceptor(Interceptor interceptor) {
//Configuration的一个成员属性:InterceptorChain interceptorChain = new InterceptorChain();
interceptorChain.addInterceptor(interceptor);
}
下图是解析插件拦截器的过程:
下面我们来看一下InterceptorChain类,InterceptorChain 是一个interceptor集合,相当于是一层层包装,后一个插件就是对前一个插件的包装,并返回一个代理对象。
public class InterceptorChain {
private final List<Interceptor> interceptors = new ArrayList<>();
// 生成代理对象
public Object pluginAll(Object target) {
//循环遍历所有的插件,调用对应的plugin方法
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);
}
}
前面我们已经知道了Mybatis加载插件的流程,那么插件到底是在哪里发挥作用的。以Executor为例,在创建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) {
executor = new CachingExecutor(executor);
}
// 创建插件对象
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
插件其实就是在这里创建的,它会调用拦截器链的pluginAll方法,将自身对象executor传入进去,实际调用的是每个Interceptor的plugin()方法,plugin()就是对目标对象的一个代理,并且生成一个代理对象返回。我们来看看plugin()方法的源码:
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
//org.apache.ibatis.plugin.Plugin#wrap
public static Object wrap(Object target, Interceptor interceptor) {
// 获取拦截器需要拦截的方法签名信息,以拦截器对象为key,拦截方法为value
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
// 获取目标对象的class对象
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;
}
【a】编写mapper接口
List<User> getAllUser(@Param("offset") Integer offset, @Param("pageSize") Integer pageSize);
【b】编写mapper.xml
<select id="getAllUser" resultType="com.wsh.mybatis.mybatisdemo.entity.User">
select * from user
<if test="offset != null">
limit #{offset}, #{pageSize}
</if>
</select>
【c】启动单元测试
List<User> allUser = userMapper.getAllUser(null, null);
System.out.println("查询到的总记录数:" + allUser.size());
for (User user : allUser) {
System.out.println(user);
}
后台打印日志如下:
16:10:41.447 [main] INFO com.wsh.mybatis.mybatisdemo.CustomPagePlugin - 拦截前rowBounds对象offset=0, limit = 2147483647
16:10:41.448 [main] INFO com.wsh.mybatis.mybatisdemo.CustomPagePlugin - 拦截后rowBounds对象offset=0, limit = 2
Opening JDBC Connection
Created connection 1667689440.
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@6366ebe0]
==> Preparing: select * from user
==> Parameters:
<== Columns: id, username
<== Row: 1, 张三
<== Row: 2, 李四
查询到的总记录数:2
User{id='1', username='张三'}
User{id='2', username='李四'}
我们可以看到,拦截后的SQL语句的limit已经被修改为2了,通过返回结果也可以看到,只返回了两条数据回来。
再来看看插件运行过程中几个关键步骤的各个属性的值:
如下是在openSession开启会话的时候,创建Executor执行器对象时,executor = (Executor) interceptorChain.pluginAll(executor)调用拦截器链的pluginAll方法时。
如下是在具体生成代理对象时:
当我们执行userMapper.getAllUser(null, null)方法的时候,会调用executor的query方法,这个query方法会被拦截器拦截,生成executor的一个代理对象,所以执行query方法的时候就会执行代理对象的invoke()方法,而invoke()方法里面会调用插件的intercept()方法,实现一些自定义业务逻辑扩展。
本文通过一个简单的自定义分页插件的例子,总结了Mybatis的插件运行原理、加载时机和创建时机。Mybatis插件的功能非常强大,能拦截到SQL,添加分页参数实现分页功能,将具体查询的条件修改掉,达到偷梁换柱的功能等等。
注意:
如果多个插件同时拦截同一个目标对象的目标方法,会发生层层代理,举个例子,有两个自定义插件:
FirstPlugin和SecondPlugin【假设配置文件中配置的顺序是FirstPlugin、SecondPlugin】,具体执行的时候其实是先执行SecondPlugin的intercept()方法,然后再执行FirstPlugin的intercept()方法。
Mybatis注册插件时,创建动态代理对象的时候,是按照插件配置的顺序插件层层代理对象,执行插件的时候,则是按照逆向顺序执行。
如下图:
最后总结一下,自定义Mybatis插件的大体步骤:
鉴于笔者水平有限,如果文章有什么错误或者需要补充的,希望小伙伴们指出来,希望这篇文章对大家有帮助。