Mybatis插件

插件介绍

一般情况下,开源框架都会提供插件或其他形式的拓展点,供开发者自行拓展。这样的好处是显而易见的,一是增加了框架的灵活性。二是开发者可以结合实际需求,对框架进行拓展,使其能够更好的工作。以MyBatis为例,我们可基于MyBatis插件机制实现分页、分表,监控等功能。由于插件和业务 无关,业务也无法感知插件的存在。因此可以无感植入插件,在无形中增强功能
Mybati s作为一个应用广泛的优秀的ORM开源框架,这个框架具有强大的灵活性,在四大组件(Executor、StatementHandler、ParameterHandler、ResultSetHandler)处提供了简单易用的插 件扩展机制。Mybatis对持久层的操作就是借助于四大核心对象。MyBatis支持用插件对四大核心对象进 行拦截,对mybatis来说插件就是拦截器,用来增强核心对象的功能,增强功能本质上是借助于底层的 动态代理实现的,换句话说,MyBatis中的四大对象都是代理对象

MyBatis所允许拦截的方法如下:
  • 执行器Executor (update、query、commit、rollback等方法);
  • SQL语法构建器StatementHandler (prepare、parameterize、batch、updates query等方 法);
  • 参数处理器ParameterHandler (getParameterObject、setParameters方法);
  • 结果集处理器ResultSetHandler (handleResultSets、handleOutputParameters等方法);

插件原理

在四大对象创建的时候
  • 1、每个创建出来的对象不是直接返回的,而是interceptorChain.pluginAll(parameterHandler);
  • 2、获取到所有的Interceptor (拦截器)(插件需要实现的接口);调用 interceptor.plugin(target);返 回 target 包装后的对象
  • 3、插件机制,我们可以使用插件为目标对象创建一个代理对象;AOP (面向切面)我们的插件可 以
  • 为四大对象创建出代理对象,代理对象就可以拦截到四大对象的每一个执行;
interceptorChain保存了所有的拦截器(interceptors),是mybatis初始化的时候创建的。调用拦截器链中的拦截器依次的对目标进行拦截或增强。interceptor.plugin(target)中的target就可以理解为mybatis中的四大对象。返回的target是被重重代理后的对象

自定义插件

1.Mybatis 插件接口-Interceptor
  • Intercept方法,插件的核心方法
  • plugin方法,生成target的代理对象
  • setProperties方法,传递插件所需参数
2.自定义插件
@Intercepts({  //{}表明可以配置多个@Signature对多个地方进行拦截
        @Signature(
                type = Executor.class,   //拦截哪个接口
                method = "query",   // 拦截的方法
                args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}   // 拦截方法的参数,通过方法名和参数确定重载方法
        )
})
public class ExecutorInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        System.out.println("对方法进行增强");
        //执行原方法
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        System.out.println("包装的目标对象:" + target);
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        System.out.println("插件配置初始化参数: " + properties);
    }
}
3.在mybatis配置文件中配置插件
<plugins>
	<plugin interceptor="com.example.interceptor.ExecutorInterceptor"></plugin>
</plugins>
4.测试代码
@Test
    public  void test5() throws IOException {
        InputStream inputStream = Resources.getResourceAsStream("sqlMapConfig.xml");
        SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);

        SqlSession sqlSession1 = factory.openSession();
        SqlSession sqlSession2 = factory.openSession();

        User user1 = sqlSession1.selectOne("com.example.mapper.UserMapper.findById", 2);
        System.out.println(user1);

        sqlSession1.commit();
    }
5.查看控制台打印
17:33:11,454 DEBUG LogFactory:135 - Logging initialized using 'class org.apache.ibatis.logging.log4j.Log4jImpl' adapter.
插件配置初始化参数: {}
17:33:11,597 DEBUG PooledDataSource:335 - PooledDataSource forcefully closed/removed all connections.
......
17:33:11,649 DEBUG ResolverUtil:256 - Checking to see if class com.example.mapper.UserMapper matches criteria [is assignable to Object]
包装的目标对象:org.apache.ibatis.executor.CachingExecutor@4b53f538
插件配置初始化参数: {}
17:33:11,806 DEBUG PooledDataSource:335 - PooledDataSource forcefully closed/removed all connections.
......
17:33:11,813 DEBUG ResolverUtil:256 - Checking to see if class com.example.mapper.UserMapper matches criteria [is assignable to Object]
包装的目标对象:org.apache.ibatis.executor.CachingExecutor@588df31b
包装的目标对象:org.apache.ibatis.executor.CachingExecutor@33b37288
对方法进行增强
17:33:11,870 DEBUG UserMapper:62 - Cache Hit Ratio [com.example.mapper.UserMapper]: 1.0
User{id=2, username='tom'}
6.原理

这样MyBatis在启动时可以加载插件,并保存插件实例到相关对象(InterceptorChain,拦截器链) 中。待准备工作做完后,MyBatis处于就绪状态。我们在执行SQL时需要先通过DefaultSqlSessionFactory 创 建 SqlSession。Executor 实例会在创建 SqlSession 的过程中被创建, Executor实例创建完毕后,MyBatis会通过JDK动态代理为实例生成代理类。这样,插件逻辑即可在 Executor相关方法被调用前执行

源码分析

Plugin实现了 InvocationHandler接口,因此它的invoke方法会拦截所有的方法调用。invoke方法会 对 所拦截的方法进行检测,以决定是否执行插件逻辑

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

插件逻辑封装在intercept中,该方法的参数类型为Invocationo Invocation主要用于存储目标类,方法以及方法参数列表。

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 getTarget() {
        return target;
    }

    public Method getMethod() {
        return method;
    }

    public Object[] getArgs() {
        return args;
    }

    /**
     * 调用方法
     *
     * @return 调用结果
     */
    public Object proceed() throws InvocationTargetException, IllegalAccessException {
        return method.invoke(target, args);
    }

}