1.Mybatis插件介绍
在Mybatis中,它的四大组件(Executor、StatementHandler、ParameterHandler、ResultSetHandler)提供了简单易用的插件扩展机制,我们可以基于Mybatis插件机制实现分页、分表,监控等功能。而Mybatis对持久层的操作就是借助于四大核心对象。Mybatis支持用插件对四大核心对象进行拦截,对Mybatis来说插件就是拦截器,用来增强核心对象的功能,增强功能本质上是借助于底层的动态代理实现的。换句话说,Mybatis中的操作的四大对象都是代理对象。
Mybatis所允许拦截的方法如下
- 执行器Executor (update、query、commit、rollback等方法);
- SQL语法构建器StatementHandler (prepare、parameterize、batch、updatesquery等方法);
- 参数处理器ParameterHandler (getParameterObject、setParameters方法);
- 结果集处理器ResultSetHandler (handleResultSets、handleOutputParameters等方法);
2.Mybatis插件原理
在四大对象创建的时候:
1.每个创建出来的对象不是直接返回的,而是interceptorChain.pluginAll(parameterHandler);
2.获取到所有的Interceptor(拦截器,即插件所需要实现的接口);调用interceptor.plugin(target);返回target包装后的对象;
3.插件机制,我们可以使用插件为目标对象创建一个代理对象;运用AOP(面向切面)我们的插件可以为四大对象创建出代理对 象,代理对象就可以拦截到四大对象的每一个执行。
拦截
插件具体是如何拦截并附加额外功能的呢?以ParameterHandler来说:
public ParameterHandler newParameterHandler(MappedStatement mappedStatement,Object object, BoundSql sql, InterceptorChain interceptorChain){
ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement,object,sql);
parameterHandler = (ParameterHandler)interceptorChain.pluginAll(parameterHandler);
return parameterHandler;
}
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
interceptorChain保存了所有的拦截器(interceptors),是Mybatis初始化的时候创建的。调用拦截器链中的拦截器依次对目标进行拦截和增强。interceptor.plugin(target)中的target可以理解为Mybatis中的四大对象。返回的target即是被重重代理后的对象。
那么如果我们想要拦截Executor的query方法,那么可以使用注解的方式定义插件:
@Intercepts({
@Signature(
type = Executor.class, //被拦截的接口字节码
method = "query", //被拦截的方法名称
args={MappedStatement.class,Object.class,RowBounds.class,ResultHandler.class}) //被拦截方法的入参字节码。如果方法重载,就能通过方法名和入参来确定唯一性
})
public class ExamplePlugin implements Interceptor {
//省略逻辑
}
除此之外,我们还需将插件配置到sqlMapConfig.xml中
<plugins>
<plugin interceptor="com.google.plugin.ExamplePlugin"></plugin>
</plugins>
这样Mybatis在启动时可以加载插件,并保存插件实例到相关对象(InterceptorChain,拦截器链)中。
待准备工作做完后,MyBatis处于就绪状态。我们在执行SQL时,需要先通过DefaultSqlSessionFactory创建SqlSession,Executor实例会在创建SqlSession的过程中被创建,Executor实例创建完毕之后,Mybatis会通过JDK动态代理为实例生成代理类。这样,插件逻辑即可在Executor相关方法被调用前执行。这就是Mybatis插件机制的基本原理。
3.自定义Mybatis插件
Mybatis的插件接口-Interceptor,所有插件类都要实现其中的3个方法:
- intercept方法:插件的核心方法,只要被代理的目标对象的目标方法被调用时,每次都会执行intercept方法。
- plugin方法:生成target的代理对象,并存到拦截器链中。
- setProperties:传递插件所需要的参数,一般在plugin标签中配置属性。
设计一个自定义插件
@Intercepts({//大扩号表示可以定义多个@Signature对多个地方拦截,都用这个拦截器
@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class MyPlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("对方法进行了增强"); //增强逻辑
return invocation.proceed(); //执行原方法
}
@Override
public Object plugin(Object target) { //target为要拦截的对象
Object wrap = Plugin.wrap(target, this); //生成代理对象
return wrap;
}
//插件初始化的时候调用,也只调用一次,插件配置的属性从这里设置进来
@Override
public void setProperties(Properties properties) {
System.out.println("获取到的配置文件参数是:"+properties);
}
}
mapper接口
public interface UserMapper {
List<User> selectUser();
}
sqlMapConfig.xml
<plugins>
<plugin interceptor="com.google.plugin.MyPlugin">
<!--配置参数-->
<property name="name" value="Bob"/>
</plugin>
</plugins>
mapper.xml
<mapper namespace="com.google.mapper.UserMapper">
<select id="selectUser" resultType="com.google.pojo.User">
SELECT id,username FROM user
</select>
</mapper>
测试类
public class PluginTest {
@Test
public void test() throws IOException {
InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
List<User> userList = sqlSession.selectList("com.google.mapper.IUserMapper.selectUser");
userList.stream().forEach(System.out::println);
}
4.源码分析
执行插件逻辑
Plugin实现了InvocationHandler接口,因此它的invoke方法会拦截所有的方法调用。invoke方法会对所拦截的方法进行检测,以决定是否执行插件逻辑。该方法的逻辑如下:
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
//获取被拦截的方法列表,比如signatureMap.get(Executor.class), 可能返回 [query,update,commit]
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);
}
}
invoke方法的代码比较少,逻辑不难理解。首先,invoke方法会检测被拦截的方法是否配置在@Signature注解中,若是,则执行插件逻辑封装在intercept中,该方法的参数类型为Invocation invocation,主要用于存储目标类,方法以及方法参数列表。简单看看该类的定义:
public class Invocation {
private final Object target;
private final Method method;
private final Object[] args;
public Invocation(Object targetf Method method, Object[] args) {
this.target = target;
this.method = method;
this.args = args;
}
//省略部分代码.....
public Object proceed() throws InvocationTargetException,IllegalAccessException {
//调用被拦截的方法
return method.invoke(target, args);
}