Mybatis-使用JDK动态代理实现插件系统讲解

Mybatis插件

Mybatis对(Executor,StatementHandler,ParameterHandler,ResultSetHandler)四大组件提供了简单易用的插件扩展机制。支持插件对四大核心对象进行拦截处理,对Mybatis来说插件就是拦截器,他底层使用了动态代理来实现插件机制,换句话说,Mybatis的四大组件都是代理对象。
Mybatis四大组件
允许使用插件来拦截的方法调用包括:

  • 执行器Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
  • 参数处理器ParameterHandler (getParameterObject, setParameters)
  • 结果集处理器ResultSetHandler (handleResultSets, handleOutputParameters)
  • SQL语法构建器StatementHandler (prepare, parameterize, batch, update, query)
Mybatis插件原理

四大对象创建的时候:

  1. 每个创建出来的对象不是直接返回,而是通过interceptorChain.pluginAll(parameterHandler)生成代理对象,
  2. interceptorChain.pluginAll方法做了什么呢,他从SqlMapConfig.xml中注册的plugin中找到自己的拦截器,并通过interceptor.plugin(target)返回target的包装对象;
  3. 最后实际调用的是拦截器,我们就可以在拦截器里面,做一些自定义处理实现插件;
实现自定义插件例子
  1. 创建自定义插件类,
    // 告诉Mybatis这是个插件
    @Intercepts({
            // 告诉Mybatis这个插件具体拦截目标
            @Signature(
                    // 指定拦截类
                    type = Executor.class,
                    // 指定拦截方法
                    method = "query",
                    // 指定拦截方法的参数(兼容重载)
                    args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
            )
            // 这里可以添加多个拦截目标
    })
    public class MyPlugin implements Interceptor {
    
        /**
        * 这里写具体的拦截业务逻辑
        */
        @Override
        public Object intercept(Invocation invocation) throws Throwable {
            // 代理的目标对象
            final Object target = invocation.getTarget();
            // 执行方法
            final Method method = invocation.getMethod();
            // 参数
            final Object[] args = invocation.getArgs();
            System.out.println("拦截之前:");
            System.out.println("拦截目标对象:" + target.getClass());
            System.out.println("拦截目标方法:" + method.getName());
            System.out.println("拦截方法调用参数" + args);
            // 前置处理流程在此处之前写
            // final Object proceed = invocation.proceed(); 默认处理
            final Object proceed = method.invoke(target, args);
            // 后置处理逻辑在此处之后写
            System.out.println("拦截之后:");
            // 返回处理的对象
            return proceed;
        }
    
        /**
        * * 这里写注册插件逻辑,不写也可以,接口有默认实现
        * * default Object plugin(Object target) {
        * * return Plugin.wrap(target, this);
        * * }
        *
        * @param target 需要代理的对象
        * @return
        */
        @Override
        public Object plugin(Object target) {
            // 这里添加自定义处理
            System.out.println("注册插件");
            /* 如果有多个插件代理相同目标,target 可能是上一个插件的代理对象,所以必须调用 Plugin.wrap(target, this);
            如果直接返回新对象,则可能丢失之前的插件性能 */
            return Plugin.wrap(target, this);
        }
    
        /**
        * 这里能获取配置文件里配置的自定义属性
        */
        @Override
        public void setProperties(Properties properties) {
            System.out.println("获取自定义属性:" + properties);
        }
    }
    
  2. SqlMapConfig.xml中注册插件
    <plugins>
        <!--interceptor是自定义插件类的全限定名-->
        <plugin interceptor="com.wjy.plugin.MyPlugin">
            <!--这里添加自定义属性-->
            <property name="test" value="测试属性"/>
        </plugin>
    </plugins>
    
  3. 测试方法
        @Test
        public void testPlugin() throws IOException {
            final SqlSession sqlSession = new SqlSessionFactoryBuilder()
                    .build(Resources.getResourceAsReader("SqlMapConfig.xml"))
                    .openSession();
            final AnnotationOrderDao orderDao = sqlSession.getMapper(AnnotationOrderDao.class);
            final List<Order> list = orderDao.list();
            System.out.println(list);
        }
    
  4. 执行测试数据结果
    获取自定义属性:{test=测试属性}  # 加载程序时获取自定义属性
    注册插件 # 这里是Executor对象注册插件
    拦截之前: 
    拦截目标对象:class org.apache.ibatis.executor.CachingExecutor
    拦截目标方法:query
    拦截方法调用参数[Ljava.lang.Object;@1fa1cab1
    注册插件 # 其他三大对象之一注册对象
    注册插件 # 其他三大对象之一注册对象
    注册插件 # 其他三大对象之一注册对象
    Preparing: select * from orders # 执行SQL
    拦截之后:
    [order{id=1, ordertime='2019-12-12', total=3000, user=null}, order{id=2, ordertime='2019-12-12', total=4000, user=null}, order{id=3, ordertime='2019-12-12', total=5000, user=null}, order{id=4, ordertime='2019-12-13', total=5001, user=null}]
    
    为什么注册了四次插件?因为Mybatis对四大对象都做了代理,注册的时候,具体区分给那个对象代理是在Plugin.wrap(target, this);里面判断的,所以同一个插件会执行四次注册;
源码分析
  1. Mybatis创建执行器对象源码Configuration.java
    public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
        ...省略代码
        // 这里调用interceptorChain.pluginAll生成执行器代理对象
        return (Executor) interceptorChain.pluginAll(executor);
    }
    
  2. interceptorChain.pluginAll源码
    // target 为需要代理的四大对象。
    public Object pluginAll(Object target) {
        // interceptors为配置文件配置的插件
        for (Interceptor interceptor : interceptors) {
            // 循环注册插件,生成代理对象(interceptor.plugin这个方法我们在写插件的时候,可以重写)
            target = interceptor.plugin(target);
        }
        return target;
    }
    
  3. interceptor.plugin源码,interceptor.plugin这个方法我们在写插件的时候,可以重写
    default Object plugin(Object target) {
        // 调用Plugin.wrap具体注册逻辑
        return Plugin.wrap(target, this);
    }
    
  4. Plugin.wrap源码
    public static Object wrap(Object target, Interceptor interceptor) {
        // 获取插件指定的拦截对象(四大对象)
        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;
    }
    
  5. 插件拦截具体执行逻辑Plugin.invoke
    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);
        }
    }
    
总结

实现一个Mybatis插件需要这几个步骤:

  1. 自定义插件类实现Interceptor接口,使用@Intercepts声明插件,指定拦截目标
  2. 配置文件添加插件配置
  3. 启动程序,插件生效
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值