一步一步剖析Mybatis Plugin拦截器原理

实现优雅的拦截器链

责任链模式的定义:多个处理器依次处理同一个业务请求。一个业务请求先经过A拦截器处理,然后再传递给B拦截器,B拦截器处理完后再传递给C拦截器,以此类推,形成一个链条。链条上的每个拦截器各自承担各自的处理职责,所以叫作责任链模式

利用JDK动态代理就可以组织多个拦截器,通过这些拦截器我们可以在业务方法执行的前后做很多想做的事。具体分析可以从一个普通的需求开始:现在要对多个接口的业务方法做一个日志记录和方法执行耗时的统计。

静态代理模式肯定不行,如果这样的接口很多,代理类就会爆炸,要用动态代理。

JDK动态代理

public interface Target {
    String execute(String sql);
}

public class TargetImpl implements Target {
    @Override
    public String execute(String sql) {
        System.out.println("execute() " + sql);
        return sql;
    }
}

代理类以及对代理的测试。

public class TargetProxy implements InvocationHandler {

    private Object target;

    public TargetProxy(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println(" 拦截前...");
        Object result = method.invoke(target, args);
        System.out.println(" 拦截后...");
        return result;
    }

    public static <T> T wrap(Object target) {
        return (T)Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), new TargetProxy(target));
    }
}

public class TestProxy {

    public static void main(String[] args) {
        System.out.println("-----------通过代理拦截execute方法--------");
        Target target = new TargetImpl();
        //返回的是代理对象
        Target targetProxy = TargetProxy.wrap(target);
        targetProxy.execute("Hello World");
   } 
}

运行结果:

------------通过代理拦截execute方法--------
 拦截前...
execute() Hello World
 拦截后...

以上实现的缺点:拦截逻辑全部写到了invoke()方法中,不符合面向对象的思想,拦截逻辑与实际业务方法调用的解耦不彻底

动态代理 + interceptor接口

设计一个Interceptor接口,需要做什么特定类型的拦截处理实现接口即可。

public interface Interceptor {
    /**
     * 具体拦截处理
     */
    void intercept();
}

public class LogInterceptor implements Interceptor {
    @Override
    public void intercept() {
        System.out.println(" 记录日志 ");
    }
}

public class TransactionInterceptor implements Interceptor {
    @Override
    public void intercept() {
        System.out.println(" 开启事务 ");
    }
}

代理类要做修改,在实际调用业务方法前,先遍历所有拦截器。

public class TargetProxyWithInterceptor implements InvocationHandler {

    private Object target;

    private List<Interceptor> interceptorList;

    public TargetProxyWithInterceptor(Object target, List<Interceptor> interceptorList) {
        this.target = target;
        this.interceptorList = interceptorList;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //处理拦截,在每次执行业务代码之前遍历拦截器
        for (Interceptor interceptor : interceptorList) {
            interceptor.intercept();
        }
        //执行实际的业务逻辑
        return method.invoke(target, args);
    }

    public static <T> T wrap(Object target, List<Interceptor> interceptorList) {
        TargetProxyWithInterceptor targetProxy = new TargetProxyWithInterceptor(target, interceptorList);
        return (T)Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),
            targetProxy);
    }
}

现在可以根据需要动态的添加多个拦截器了,测试代码:

public class TestProxy {

    public static void main(String[] args) {
        System.out.println("------------抽象拦截接口,各类拦截器独立--------");
        Target target = new TargetImpl();
        List<Interceptor> interceptorList = new ArrayList<>();
        interceptorList.add(new LogInterceptor());
        interceptorList.add(new TransactionInterceptor());
        Target targetProxyWithInterceptor = TargetProxyWithInterceptor.wrap(target, interceptorList);
        targetProxyWithInterceptor.execute("Hello World");
	}
}

运行结果:

------------抽象拦截接口,各类拦截器独立--------
 记录日志 
 开启事务 
execute() Hello World

以上实现的缺点:只能做前置拦截(拦截request),不能后置拦截(拦截response)。而且拦截器并不知道要拦截的对象的信息。应该做更一步的抽象,把拦截对象、要拦截的方法、方法的参数三者再封装一下。目标对象的真实业务方法放到Interceptor中去执行完成,这样就可以实现前后拦截。

动态代理 + interceptor接口 + 拦截对象信息封装

设计一个Invocation类来描述拦截对象、要拦截的方法、方法的参数三者。

public class Invocation {
    /**
     * 要拦截的目标对象
     */
    private Object target;
    /**
     * 要拦截的执行方法
     */
    private Method method;
    /**
     * 方法的参数
     */
    private Object[] args;

    public Invocation(Object target, Method method, Object[] args) {
        this.target = target;
        this.method = method;
        this.args = args;
    }
    
    /**
     * 执行目标对象的方法
     * @return
     * @throws Exception
     */
    public Object process() throws InvocationTargetException, IllegalAccessException {
        return method.invoke(target, args);
    }
}

重新定义拦截器,Invocation对象就是被拦截的信息封装。

public interface InvocationInterceptor {

    /**
     * 具体拦截处理
     * @param invocation
     * @return
     * @throws Exception
     */
    Object intercept(Invocation invocation) throws Exception;
}

public class TransactionInvocationInterceptor implements InvocationInterceptor {
    @Override
    public Object intercept(Invocation invocation) throws Exception {

        System.out.println(" 开启事务 ");
        Object result = invocation.process();
        System.out.println(" 提交事务 ");
        return result;
    }
}

代理类要做修改,调用拦截器的intercept()方法前要先构建Invocation对象。

public class TargetProxyWithInvocation implements InvocationHandler {

    private Object target;

    private InvocationInterceptor interceptor;

    public TargetProxyWithInvocation(Object target, InvocationInterceptor interceptor) {
        this.target = target;
        this.interceptor = interceptor;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Invocation invocation = new Invocation(target,method,args);
        return interceptor.intercept(invocation);
    }

    public static <T> T wrap(Object target, InvocationInterceptor interceptor) {
        TargetProxyWithInvocation targetProxy = new TargetProxyWithInvocation(target, interceptor);
        return (T)Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),
            targetProxy);
    }
}

现在可以实现前后拦截了,并且拦截器能获取被拦截对象的信息,测试代码:

public class TestProxy {

    public static void main(String[] args) {
        System.out.println("------------将被拦截对象信息封装成Invocation对象,实现前后拦截--------");
        Target target = new TargetImpl();
        InvocationInterceptor interceptor = new TransactionInvocationInterceptor();
        Target targetProxyWithInvocation = TargetProxyWithInvocation.wrap(target, interceptor);
        targetProxyWithInvocation.execute("Hello World");
   }
}

运行结果:

--------将被拦截对象信息封装成Invocation对象,实现前后拦截--------
 开启事务 
execute() Hello World
 提交事务 

优化点:对于被拦截的目标对象来说,只需要了解对它插入了什么拦截器就好。再修改一下拦截器接口,增加一个插入目标类的方法。

public interface InvocationInterceptor {

    /**
     * 具体拦截处理
     * @param invocation
     * @return
     * @throws Exception
     */
    Object intercept(Invocation invocation) throws Exception;

    /**
     * 将当前拦截器插入到目标对象中
     * @param target 被拦截的目标对象
     * @return 代理对象
     */
    default <T> T plugin(Object target) {
        return (T)TargetProxyWithInvocation.wrap(target, this);
    }
}

目标对象仅仅需要在执行其业务方法前,插入自己需要的拦截器就好了,测试代码:

public class TestProxy {

    public static void main(String[] args) {
        System.out.println("------------Interceptor接口中加plugin方法,方便目标对象插入自己需要的拦截器--------");
        InvocationInterceptor transactionInterceptor = new TransactionInvocationInterceptor();
        Target proxy = transactionInterceptor.plugin(target);
        InvocationInterceptor logInterceptor  = new LogInvocationInterceptor();
        //代理嵌套再代理
        proxy = logInterceptor.plugin(proxy);
        proxy.execute("Hello World");
    }
}

运行结果

------------Interceptor接口中加plugin方法,方便目标对象插入自己需要的拦截器--------
 开始记录日志 
 开启事务 
execute() Hello World
 提交事务 
 记录日志结束 

以上实现的缺点:添加多个拦截器的姿势不太美观,当拦截器很多时,写法很繁琐。可以设计一个InterceptorChain拦截器链类来管理多个拦截器。

动态代理 + interceptor接口 + 拦截对象信息封装 + 责任链

设计InterceptorChain拦截器链类 ,通过pluginAll() 方法一次性把所有的拦截器插入到目标对象中去。

public class InvocationInterceptorChain {

    private List<InvocationInterceptor> interceptorList = new ArrayList<>();

    public Object pluginAll(Object target) {
        for (InvocationInterceptor interceptor : interceptorList) {
            target = interceptor.plugin(target);
        }
        return target;
    }

    public void addInterceptor(InvocationInterceptor interceptor) {
        interceptorList.add(interceptor);
    }

    /**
     * 返回一个不可修改集合,表示只能通过addInterceptor方法添加拦截器
     * @return
     */
    public List<InvocationInterceptor> getInterceptorList() {
        return Collections.unmodifiableList(interceptorList);
    }
}

测试代码:

public class TestProxy {

    public static void main(String[] args) {
        System.out.println("------------设计InterceptorChain类管理所有拦截器并批量plugin到目标对象中--------");
        InvocationInterceptor interceptor1 = new TransactionInvocationInterceptor();
        InvocationInterceptor interceptor2 = new LogInvocationInterceptor();
        InvocationInterceptorChain invocationInterceptorChain = new InvocationInterceptorChain();
        invocationInterceptorChain.addInterceptor(interceptor1);
        invocationInterceptorChain.addInterceptor(interceptor2);

        target = (Target) invocationInterceptorChain.pluginAll(target);
        target.execute(" HelloWord ");
    }
}

运行结果:

------------设计InterceptorChain类管理所有拦截器并批量plugin到目标对象中--------
 开始记录日志 
 开启事务 
execute()  HelloWord 
 提交事务 
 记录日志结束 

MyBatis Plugin的设计与实现

MyBatis Plugin跟Servlet Filter、Spring Interceptor的功能是类似的,都是在不需要修改原有流程代码的情况下,拦截某些方法调用,在要拦截的方法调用的前后,执行一些额外的代码逻辑。它们的唯一区别在于拦截的位置是不同的。Servlet Filter主要拦截Servlet请求,Spring Interceptor主要拦截 Spring管理的Bean方法(比如Controller 类的方法等),而MyBatis Plugin主要拦截的是MyBatis在执行SQL的过程中涉及的一些方法。

使用Mybatis要引入其依赖包,如下:

<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.4.2</version>
</dependency>

有了前面的基础,再来看Mybatis的拦截器部分源码就轻松得多了。下图中的Plugin类实现了JDK的InvocationHandler接口,其实就是代理类,只是个人认为这个类名起得不好。

MyBatis拦截器使用示例

以下示例拦截器实现对SQL执行耗时的打印。

@Intercepts({
    @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
    @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})}
)
public class MybatisExampleInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
        Object parameter = null;
        if (invocation.getArgs().length > 1) {
            parameter = invocation.getArgs()[1];
        }
        StopWatch stopWatch = StopWatch.createStarted();
        try {
            return invocation.proceed();
        } finally {
            BoundSql boundSql = mappedStatement.getBoundSql(parameter);
            String sql = boundSql.getSql();
            sql.replace("\n", "")
                .replace("\t", "").replace("  ", " ")
                .replace("( ", "(").replace(" )", ")")
                .replace(" ,", ",").replaceAll(" +", " ");
            System.out.println("costTime=" + stopWatch.getTime() + ", mappedStatement Sql: " + sql);
        }
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        String prop1 = properties.getProperty("prop1");
        String prop2 = properties.getProperty("prop2");
        System.out.println("plugin prop1Value=" + prop1);
        System.out.println("plugin prop2Value=" + prop2);
    }
}


<!-- MyBatis全局配置文件:mybatis-config.xml,引入拦截器 -->
<plugins>
    <plugin interceptor="com.test.dao.interceptor.MybatisExampleInterceptor">
        <property name="prop1" value="value1"/>
        <property name="prop2" value="value2"/>
    </plugin>
</plugins>

我们知道不管是拦截器、过滤器还是插件,都需要明确地标明拦截的目标方法。以上代码中的@Intercepts注解实际上就是起了这个作用。其中,@Intercepts注解又可以嵌套@Signature注解。一个@Signature注解标明一个要拦截的目标方法。如果要拦截多个方法,就编写多条@Signature注解。@Signature注解包含三个元素:type、method、args。其中,type指明要拦截的类、method指明方法名、args指明方法的参数列表。通过指定这三个元素,我们就能完全确定一个要拦截的方法

默认情况下,MyBatis Plugin允许拦截的方法有下面这样几个:

为什么默认允许拦截的是这样几个类的方法呢?MyBatis底层是通过 Executor类来执行SQL的。Executor类会创建StatementHandler、ParameterHandler、ResultSetHandler三个对象,并且,首先使用 ParameterHandler设置SQL中的占位符参数,然后使用StatementHandler执行SQL语句,最后使用ResultSetHandler封装执行结果。所以,我们只需要拦截Executor、ParameterHandler、ResultSetHandler、StatementHandler这几个类的方法,基本上就能满足我们对整个SQL执行流程的拦截了。

实际上,除了统计SQL的执行耗时,利用MyBatis Plugin,我们还可以做很多事情,比如分库分表、自动分页、数据脱敏、加密解密等等。

MyBatis Plugin的设计

相对于Servlet Filter、Spring Interceptor中责任链模式的代码实现,MyBatis Plugin的代码实现有一些不同,因为它是借助动态代理来实现的。通常,责任链链模式的实现一般包含处理器(Handler)和处理器链(HandlerChain)两部分。这两个部分对应到Servlet Filter的源码就是Filter和FilterChain,对应到Spring Interceptor的源码就是 HandlerInterceptor和HandlerExecutionChain,对应到MyBatis Plugin的源码就是Interceptor和InterceptorChain。

除此之外,MyBatis Plugin还包含另外一个非常重要的类:Plugin。它用来生成被拦截对象的动态代理。集成了MyBatis的应用在启动的时候,MyBatis框架会读取全局配置文件(前面例子中的mybatis-config.xml文件),解析出Interceptor(也就是例子中的MybatisExampleInterceptor),并且将它注入到Configuration类的 InterceptorChain对象中。这部分逻辑对应到源码如下所示:

public class XMLConfigBuilder extends BaseBuilder {

    private boolean parsed;

    public Configuration parse() {
        if (parsed) {
          throw new BuilderException("Each XMLConfigBuilder can only be used once.");
        }
        parsed = true;
        parseConfiguration(parser.evalNode("/configuration"));
        return configuration;
    }

    //解析配置文件
    private void parseConfiguration(XNode root) {
        try {
          //issue #117 read properties first
          propertiesElement(root.evalNode("properties"));
          Properties settings = settingsAsProperties(root.evalNode("settings"));
          loadCustomVfs(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);
        }
    }

    //解析插件
    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();
                //调用Interceptor上的setProperties()方法设置properties
                interceptorInstance.setProperties(properties);
                //下面这行代码最终会调用InterceptorChain.addInterceptor()方法
                configuration.addInterceptor(interceptorInstance);
            }
        }
    }

}


// Configuration类的addInterceptor()方法的代码
public void addInterceptor(Interceptor interceptor) {
    interceptorChain.addInterceptor(interceptor);
}

Interceptor的setProperties()方法就是一个单纯的setter方法,主要是为了方便通过配置文件配置Interceptor的一些属性值,没有其他作用。Interceptor类中intecept()和plugin()函数,InterceptorChain类中的 pluginAll()函数,Invocation类在本文第一部分中已经实现过,不再赘述。

前面提到,在执行SQL的过程中,MyBatis会创建Executor、StatementHandler、ParameterHandler、ResultSetHandler这几个类的对象,对应的创建代码在Configuration类中,如下所示:

public class Configuration {

    public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
        ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
        //嵌套调用InterceptorChain上每个Interceptor的plugin()方法进行嵌套代理
        parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
        return parameterHandler;
    }

    public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,
        ResultHandler resultHandler, BoundSql boundSql) {
        ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
        //嵌套调用InterceptorChain上每个Interceptor的plugin()方法进行嵌套代理
        resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
        return resultSetHandler;
    }

    public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
        StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
        //嵌套调用InterceptorChain上每个Interceptor的plugin()方法进行嵌套代理
        statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
        return statementHandler;
    }

    public Executor newExecutor(Transaction transaction) {
        return newExecutor(transaction, defaultExecutorType);
    }

    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);
        }
        //嵌套调用InterceptorChain上每个Interceptor的plugin()方法进行嵌套代理
        executor = (Executor) interceptorChain.pluginAll(executor);
        return executor;
    }
}

Plugin是借助Java InvocationHandler实现的动态代理类。用来代理给 target对象添加Interceptor功能。其中,要代理的target对象就是 Executor、StatementHandler、ParameterHandler、ResultSetHandler这四个类的对象。当然,只有interceptor与target互相匹配的时候,才会返回代理对象,否则就返回target对象本身。怎么才算是匹配呢?那就是 interceptor通过@Signature注解要拦截的类包含target对象,可以参看 wrap()函数的代码实现:

public class Plugin implements InvocationHandler {

  private Object target;
  private Interceptor interceptor;
  private Map<Class<?>, Set<Method>> signatureMap;

  //省略构造函数

  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 {
    //省略
  }

  private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
    Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
    if (interceptsAnnotation == null) {
      throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());      
    }
    Signature[] sigs = interceptsAnnotation.value();
    Map<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>();
    for (Signature sig : sigs) {
      Set<Method> methods = signatureMap.get(sig.type());
      if (methods == null) {
        methods = new HashSet<Method>();
        signatureMap.put(sig.type(), methods);
      }
      try {
        Method method = sig.type().getMethod(sig.method(), sig.args());
        methods.add(method);
      } catch (NoSuchMethodException e) {
        throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);
      }
    }
    return signatureMap;
  }
  //省略一些方法...
}

MyBatis中的责任链模式的实现方式比较特殊。它对同一个目标对象嵌套多次代理,也就是InteceptorChain中的pluginAll()函数要执行的任务。每个代理对象(Plugin 对象)代理一个拦截器(Interceptor 对象)功能。当执行Executor、StatementHandler、ParameterHandler、ResultSetHandler这四个类上的某个方法的时候,MyBatis会嵌套执行每层代理对象(Plugin 对象)上的invoke()方法。而invoke()方法会先执行代理对象中的 interceptor的intecept()函数,然后再执行被代理对象上的方法。就这样,一层一层地把代理对象上的intercept()函数执行完之后,MyBatis才最终执行那4个原始类对象上的方法。

  • 7
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值