美文网首页Spring全家桶
MyBatis详解7.插件

MyBatis详解7.插件

作者: 卢卡斯哔哔哔 | 来源:发表于2019-01-24 16:56 被阅读0次

    点击进入我的博客

    MyBatis详解1.概述
    MyBatis详解2.MyBatis使用入门
    MyBatis详解3.MyBatis配置详解
    MyBatis详解4.映射器Mapper
    MyBatis详解5.动态SQL
    MyBatis详解6.MyBatis技术内幕
    MyBatis详解7.插件
    MyBatis详解8.集成Spring

    1 插件的接口

    Configuration创建Executor、StatementHandler、ResultSetHandler、ParameterHandler的方法中,都可以通过InterceptorChain来添加插件,这使得我们能够在这些对象调度的时候插入我们的代码去执行一些特殊的要求以满足特殊的场景需求。要实现自定义的插件,必须实现org.apache.ibatis.plugin.Interceptor接口。

    public interface Interceptor {
      Object intercept(Invocation invocation) throws Throwable;
      Object plugin(Object target);
      void setProperties(Properties properties);
    }
    
    • intercept方法:它将直接覆盖你所拦截对象原有的方法,是最主要的方法。参数ivocation对象,通过它可以反射调度原来对象的方法。
    • plugin方法:target是被拦截对象,它的作用是给被拦截对象生成一个代理对象,并返回它。为了方便使用,MyBatis通过org.apache.ibatis.plugin.Plugin#warp方法包装了Proxy.newProxyInstance方法,来创建代理对象。
    • setProperties方法:允许在plugin元素中配置所需参数,方法在插件初始化的时候就被调用了一次,然后把插件对象存入到配置中,以便后面再取出。

    2 插件的初始化

    通过org.apache.ibatis.session.SqlSessionFactoryBuilder#build()方法构建SqlSessionFactory的时候,我们会调用org.apache.ibatis.builder.xml.XMLConfigBuilder#parse()方法,此时会调用org.apache.ibatis.builder.xml.XMLConfigBuilder#pluginElement()方法完成对插件的初始化。

      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).newInstance();
            interceptorInstance.setProperties(properties);
            // 向configuration中添加Interceptor
            configuration.addInterceptor(interceptorInstance);
          }
        }
      }
    

    在解析配置文件的时候,在MyBatis的上下文初始化过程中,就开始读入插件节点和我们配置的参数,同时使用反射技术生成对应的插件实例,然后调用插件方法中的setProperties方法设置我们配置的参数,最后将插件实例保存到配置对象中,以便读取和使用它。所以插件的实例对象是一开始就被初始化的,使用它的时候直接拿出来就可以了

      public void addInterceptor(Interceptor interceptor) {
        interceptorChain.addInterceptor(interceptor);
      }
    

    3 插件的代理和反射设计

    插件的设计模式

    插件用的是责任链模式。责任链模式,就是一个对象在多个角色中传递,处在传递链上的任何角色都有处理它的机会。MyBatis的责任链是由InterceptorChain去定义的,在Configuration创建Executor、StatementHandler、ResultSetHandler、ParameterHandler的方法中,都会执行interceptorChain.pluginAll()方法。

    public class InterceptorChain {
      private final List<Interceptor> interceptors = new ArrayList<Interceptor>();
     
      public Object pluginAll(Object target) {
        for (Interceptor interceptor : interceptors) {
          target = interceptor.plugin(target);
        }
        return target;
      }
    }
    

    我们知道interceptor#plugin方法是生成代理对象的方法。在pluginAll方法中,首先遍历interceptors来取出插件,然后为Executor对象创建代理并返回。其他三个对象的处理方法也是相同的。

    工具类Plugin

    MyBatis提供了一个工具类Plugin来生成代理对象,它实现了InvocationHandler接口,采用JDK动态代理来实现代理功能。其最主要的方法是wrap()和invoke()。

    public class Plugin implements InvocationHandler {
      // 生成target对象的代理类
      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;
      }
    
      // 代理类调用的方法
      @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);
        }
      }
    }
    
    Invocation对象
    public class Invocation {
      public Object proceed() throws InvocationTargetException, IllegalAccessException {
        return method.invoke(target, args);
      }
    }
    
    

    Invocation对象中有一个proceed方法,这个方法就是调度被代理对象的真实方法。现在假设有n个插件,我们知道第一个传递的参数是四大对象的本身,然后调用一次wrap方法产生第一个代理对象,而这里的反射就是反射四大对象本身的真实方法。如果有第二个插件,我们会将第一个代理对象传递给wrap方法,生成第二个代理对象,这里的反射就是指第一个代理对象的 invoke方法,依此类推直至最后一个代理对象。如果每一个代理对象都调用这个 proceed方法,那么最后四大对象本身的方法也会被调用,只是它会从最后一个代理对象的invoke方法运行到第一个代理对象的invoke方法,直至四大对象的真实方法。

    4 工具类MetaObject

    MyBatis中,四大对象给我们提供的public设置参数的方法很少,我们难以通过其自身得到相关的属性信息,但是通过MetaObject,可以有效读取或者修改一些重要对象的属性。它有3个常用方法:

    • MetaObject forObject方法:用于包装对象。这个方法我们已经不再使用了,而是用 MyBatis为我们提供的SystemMetaObject.forObject()方法。
    • Object getValue方法:用于获取对象属性值,支持OGNL。
    • void setValue方法:用于修改对象属性值,支持OGNL。

    5 插件开发实例

    现在假设我们要自动为SELECT语句后面增加一个LIMIT来控制每次SELECT的行数,可以通过拦截StatementHandler的prepare来更改SELECT语句本身实现,如下所示。

    @Intercepts(
            @Signature(
                    type = StatementHandler.class,
                    method = "prepare",
                    args = {Connection.class, Integer.class}
            )
    )
    public class MyPlugin implements Interceptor {
        private Properties properties;
    
        @Override
        public Object intercept(Invocation invocation) throws Throwable {
            // 获取StatementHandler对象
            StatementHandler statementHandler = (StatementHandler)invocation.getTarget();
    
            // 通过SystemMetaObject获取元信息
            MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
    
            String sql = (String) metaObject.getValue("delegate.boundSql.sql");
    
            // 在原SQL后面添加LIMIT
            if(sql != null && sql.startsWith("SELECT")) {
                sql = sql + "LIMIT " + properties.getProperty("row") ;
                metaObject.setValue("delegate.boundSql.sql", sql);
            }
    
            return invocation.proceed();
        }
    
        @Override
        public Object plugin(Object target) {
            // 一般来说,在plugin方法中只需要这一行代码就够了
            // 因为Plugin.wrap会根据@Intercepts的注解判断是返回原对象还是返回包装后的对象
            return Plugin.wrap(target, this);
        }
    
        // 此代码在MyBatis上下文初始化的时候就会调用
        @Override
        public void setProperties(Properties properties) {
            // 展示配置的properties
            properties.forEach((key, value) -> System.out.println("key=" + key + ", value=" + value));
            this.properties = properties;
        }
    }
    
        <plugins>
            <plugin interceptor="com.ankeetc.spring.plugin.MyPlugin">
                <property name="row" value="1"/>
            </plugin>
        </plugins>
    
    开发插件过程
    1. 首先创建一个插件类MyPlugin并继承Interceptor接口
    2. 在插件类上用@Intercepts注解确定要拦截的对象、方法和参数。
    3. 在plugin方法中添加代码Plugin.wrap(target, this);。一般来说,在plugin方法中只需要这一行代码就够了,因为Plugin.wrap会根据@Intercepts的注解判断是返回原对象还是返回包装后的对象。
    4. 在intercept方法中编写具体的逻辑。
    5. 在配置文件中引入插件。
    插件编写注意
    1. 能不用插件尽量不要用插件,因为它将修改MyBatis的底层设计。
    2. 插件生成的是层层代理对象的责任链模式,通过反射方法运行,性能不高,所以减少插件就能减少代理,从而提高系统的性能。
    3. 编写插件需要了解MyBatis的运行原理,了解四大对象及其方法的作用,准确判断需要拦截什么对象,什么方法,参数是什么,才能确定签名如何编写。
    4. 在插件中往往需要读取和修改 My Batis映射器中的对象属性,你需要熟练掌握MyBatis映射器内部组成的知识。
    5. 插件的代码编写要考虑全面,特别是多个插件层层代理的时候,要保证逻辑的正确性。

    相关文章

      网友评论

        本文标题:MyBatis详解7.插件

        本文链接:https://www.haomeiwen.com/subject/lhzyjqtx.html