Mybatis 插件(拦截器)原理讲解

                      Mybatis 插件(拦截器)原理讲解

  在mybatis的配置文件中可以看到有个 标签,既可以叫插件也可以叫拦截器.顾名思义,就是可以对一些类的方法进行拦截,那么可以拦截那些类呢,主要可以看下Configuration 这个类中的这几个方法newParameterHandler,newResultSetHandler,newStatementHandler,newExecutor ,对应4个可以拦截的接口ParameterHandler,ResultSetHandler,StatementHandler,Executor基本上mybatis 就是对这几个返回接口的实现类去拦截。
在这里插入图片描述
            图一
  那么这个拦截器给提供哪些功能呢,例如在查询之前希望对sql 进行相关改动,比如代码里面查询语句select * from table,如果不想在所有的查询语句手写分页limit而又要分页效果,那么必须要在查询之前修改mybatis封装好的sql语句,这个就需要拦截器实现,statement 对象进行拦截,修改对象中的sql 变成分页sql 再重新赋值,那么此时statement就是一个一个可以执行分页的对象。
那么拦截器是如何实现的呢,仿照官方给的样例写一个自己的插件实现类。
  新建一个拦截器实现类

@Intercepts({@Signature(
        type= StatementHandel.class,
        method = "prepare",
        args = {Connection.class,Object.class})})
public class ExamplePlugin implements Interceptor {
    public Object intercept(Invocation invocation) throws Throwable {
        //编写自己的业务逻辑
        System.out.println(“在prepare方法执行之前修改sql”)
        return invocation.proceed();
    }
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }
    public void setProperties(Properties properties) {
    }
}

  其次mybatis xml 注册改插件
<plugins> <plugin interceptor="org.format.mybatis.cache.interceptor.ExamplePlugin"> </plugin> </plugins>
  这个实现类实现了Interceptor接口,来看下这个接口

public interface Interceptor {
    Object intercept(Invocation var1) throws Throwable;

    Object plugin(Object var1);

    void setProperties(Properties var1);
}

  上面这个接口有2个比较重要的方法,intercept 和 plugin,
  第一个方法就是在执行你某个方法时先执行intercept这个方法,例如ExamplePlugin 这个实现类,它拦截了所有的StatementHandle 的实现类的prepare方法,当StatementHandle 的实现类执行所有的prepare操作就会先执行ExamplePlugin 里面的intercept方法,这里先大致介绍下各自功能,具体下面将。
  第二个方法就是返回一个动态代理的对象,这下就清楚了,通过动态代理去执行前置操作。例如我们前面提到的在我们原始的sql 查询之前,通过动态代理拼接上分页,而不用我们在mapper文件里面写分页的sql,只专注于写查询sql。
  下面通过源码解析配置文件逐步了解拦截器原理,拦截器属于plugin 插件,在配置文件configuration解析中 可以在XmlConfigurationBuilder中prase方法中看到通过this.pluginElement(root.evalNode(“plugins”));解析xml 的plugins 节点数据,看下是如何解析的

private void pluginElement(XNode parent) throws Exception {
    if (parent != null) {
        Iterator i$ = parent.getChildren().iterator();

        while(i$.hasNext()) {
            XNode child = (XNode)i$.next();
            String interceptor = child.getStringAttribute("interceptor");
            Properties properties = child.getChildrenAsProperties();
            Interceptor interceptorInstance = (Interceptor)this.resolveClass(interceptor).newInstance();
            interceptorInstance.setProperties(properties);
            this.configuration.addInterceptor(interceptorInstance);
        }
    }

}

  在上面代码中,将解析xml得到的注册插件ExamplePlugin 的对象,然后执行addInterceptor的方法,addInterceptor方法调用了InterceptorChain类的addInterceptor方法
public void addInterceptor(Interceptor interceptor) {
this.interceptorChain.addInterceptor(interceptor);
}
  看下InterceptorChain类,代码如下

public class InterceptorChain {
    private final List<Interceptor> interceptors = new ArrayList();

    public InterceptorChain() {
    }

    public Object pluginAll(Object target) {
        Interceptor interceptor;
        for(Iterator i$ = this.interceptors.iterator(); i$.hasNext(); target = interceptor.plugin(target)) {
            interceptor = (Interceptor)i$.next();
        }

        return target;
    }

    public void addInterceptor(Interceptor interceptor) {
        this.interceptors.add(interceptor);
    }
}

  在该类中有一个interceptors 属性,该属性是一个list 数组,存放的是Interceptor 实现类的数组,也就是我们自定义拦截器的对象数组,通过上面的configuration 对象的addInterceptor方法调用本类的addInterceptor将拦截器实现类的对象存放进数组中,**并且是final 类型,final类型的好处是对象的地址不可改变,但里面的值可以改边,这样可以在多线程中保证数据安全。
  还有有一个pluginAll 方法,主要功能就是遍历拦截器实现类对象数组,然后是根据是否注册了拦截类,如果注册了就返回对象的代理类,如果没有注册,就返回原本的对象。这个方法先简单说明下作用,具体如何实现将在查询的时候调用该方法,再去详细介绍,这里大致介绍下功能。
  到这一步,所有插件已经初始化完成了,下面分析拦截类是如何工作的。 要了解拦截类是如何工作的先看看拦截的实现类ExamplePlugin ,在上面代码ExamplePlugin 类里面,

  有一个@Intercepts注解,注解中有级几个比较重要的属性,
  第一个type:也就是需要拦截哪些接口,也就是上面介绍的在Configuration类中通过4个方法生成的4个拦截对象ParameterHandler ResultSetHandler StatementHandler Executor。因此,可以知道mybatis一共可以实现4个类型的拦截器实现类。这个例子中拦截的是statementHandler 这个接口,主要负责与数据库交互的功能;
  第二个就是method,就是拦截该接口的哪个方法。由于动态代理是原始类执行所有方法都会执行代理类的invoke方法,所以指明了拦截哪个方法就拦截某一种方法,而避免了类的所有方法都会被拦截。在上面的代码中拦截的是prepare方法,因此所有StatementHandler的实现类执行prepare方法执行都会被拦截,而该类执行其他方法都不会被拦截,这个在后面详细说明。
  第三个args 就是表明该方法有哪些参数,由于java有重载机制,方法名相同,参数不同,说以表明方法参数,直接定位到拦截某一方法。
  下面通过一次查询来展示是如何拦截StatementHandler 实现类的prepare方法。首先普通查询都会进入SimpleExecutor中的doQuery 方法,在该代码的主要关注StatementHandler 的创建,因为我们主要拦截StatementHandler 类。下面是doQuery 方法:

public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null;

    List var9;
    try {
        Configuration configuration = ms.getConfiguration();
        StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, rowBounds, resultHandler, boundSql);
        stmt = this.prepareStatement(handler, ms.getStatementLog());
        var9 = handler.query(stmt, resultHandler);
    } finally {
        this.closeStatement(stmt);
    }

    return var9;
}

  通过configuration的newStatementHandler方法创建StatementHandler对象,看下如何创建

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);
    StatementHandler statementHandler = (StatementHandler)this.interceptorChain.pluginAll(statementHandler);
    return statementHandler;
}

  这行代码中,首先创建一个RoutingStatementHandler对象,因此StatementHandler 就是一个RoutingStatementHandler对象。接下来的段代码就比较重要了也是插件里面的核心,就是
StatementHandler statementHandler = (StatementHandler)this.interceptorChain.pluginAll(statementHandler);
该行代码将传入的RoutingStatementHandler对象转化为一个动态代理类,进入InterceptorChain类的pluginAll方法看下是如何转化的。

public Object pluginAll(Object target) {
        Interceptor interceptor;
        for(Iterator i$ = this.interceptors.iterator(); i$.hasNext(); target = interceptor.plugin(target)) {
            interceptor = (Interceptor)i$.next();
        }

        return target;
    }

  上面代码中通过循环interceptors数组,而interceptors数组里面存放的是我们自定义的拦截器实现类对象ExamplePlugin(可能有多个,这里我们只有一个)。target = interceptor.plugin(target)这行代码里面的target 就是RoutingStatementHandler对象,interceptor就是ExamplePlugin 对象,所以,最终执行的是ExamplePlugin 类的plugin方法,方法如下

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

该方法调用了Plugin类的wrap 方法,并传入RoutingStatementHandler对象和 ExamplePlugin 对象。
  去看下Plugin类,发现其实现了InvocationHandler接口,因此该类是一个动态代理了,也就是我们可以知道所有注册的插件其实是由该类区代理的,看下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);
        return interfaces.length > 0 ? Proxy.newProxyInstance(type.getClassLoader(),                     interfaces, new Plugin(target, interceptor, signatureMap)) : target    
}

  这4行代码一行一行的分析
  Map<Class<?>, Set> signatureMap = getSignatureMap(interceptor);这行代码是通过传进来的interceptor 对象(ExamplePlugin),通过map 的key 和 value 的类型可以知道,signatureMap key里面存放的是被拦截的接口类StatementHandel,value 是被拦截类的方法名称数组prepare。
  Class<?> type = target.getClass(); 由于target 指向的是RoutingStatementHandler对象,获取的就是RoutingStatementHandler 类
  Class<?>[] interfaces = getAllInterfaces(type, signatureMap) 这行代码是将RoutingStatementHandler 和 signatureMap 比较,如果被拦截类注册过interfaces 长度大于1,如果没有注册过长度为0
interfaces.length > 0 ? Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)) : target 这行代码就是该方法的核心,通过判断interfaces的长度是否大于0,也就是是否注册过拦截类。如果注册过,通过Proxy.newProxyInstance()返回一个实例化的动态代理类,如果没有,就返回target 也就是原有传进来的对象。为什么需要这样,因为我们在最前面知道可以拦截4种接口,不管我没配没配置拦截实现类,这4种接口在new的时候都会调用pluginAll方法,也就会执行wrap方法,所有有一个比较,如果注册,返回代理类,如果没有注册,返回原有的对象。
  到此,RoutingStatementHandler对象的动态代理类已经通过Plugin类生成了,那么,回到上面SimpleExecutor 的doquery方法,StatementHandler的对象handler已经是一个代理类,接下来看prepare方法调用的地方

private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
        Connection connection = this.getConnection(statementLog);
        Statement stmt = handler.prepare(connection);
        handler.parameterize(stmt);
        return stmt;
    }

  在该类中,handler是RoutingStatementHandler的代理类,由于执行了prepare方法,代理类将会拦截被代理类所有方法,会执行invoke方法。
进入Plugin类的 invoke方法,代码如下:

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
            Set<Method> methods = (Set)this.signatureMap.get(method.getDeclaringClass());
            return methods != null && methods.contains(method) ? this.interceptor.intercept(new Invocation(this.target, method, args)) : method.invoke(this.target, args);
        } catch (Exception var5) {
            throw ExceptionUtil.unwrapThrowable(var5);
        }
}

Set methods = (Set)this.signatureMap.get(method.getDeclaringClass());获取拦截实现类注册过的拦截方法也就是prepare,
return methods != null && methods.contains(method) ? this.interceptor.intercept(new Invocation(this.target, method, args)) : method.invoke(this.target, args); 判断当前拦截的方法是否在拦截实现类注册过(因为动态代理会拦截原始类的所有方法,因此该处只会拦截那些注册过的方法),这行代码将会执行对象interceptor(也就是类ExamplePlugin)的intercept方法,如果不符合(就是RoutingStatementHandler除了注册过了的prepare方法其他方法的执行)就执行其他方法方法。
  下面讨论符合的情况,也就是执行了prepare方法被拦截,由于执行了拦截实现类ExamplePlugin的intercept方法,进入该方法

public Object intercept(Invocation invocation) throws Throwable {
     //编写自己的业务逻辑
    System.out.println(“在prepare方法执行之前修改sql”)
    return invocation.proceed();
}

  该方法接收了一个Invocation对象,其中 中包含了3个属性,在Plugin中new 的时候包含了target(被拦截的对象RoutingStatementHandler),method(被拦截的方法),还有一个Plugin对象,比较重要的是target对象传进来,也就是被拦截的对象(RoutingStatementHandler)。在return之前,可以处理自己的逻辑,处理完后在去让mybatis处理它自己的逻辑。例如可以通过在自己的逻辑中对target对象(RoutingStatementHandler)进行相关操作修改RoutingStatementHandler对象的属性,那么这个对象的地址不会改变。
  接下来有一个invocation.proceed();进入Invocation 的 proceed方法

 public Object proceed() throws InvocationTargetException, IllegalAccessException {
        return this.method.invoke(this.target, this.args);
 }

  可以看到也是一个method.invoke方法,java的反射机制,相当于去执行RoutingStatementHandler的preapre方法。那么我们在执行RoutingStatementHandler的preapre方法之前处理了自己的逻辑。
  到这,拦截器一个完整的流程就走完了,可以看到我们通过自定义的拦截类ExamplePlugin拦截被拦截类RoutingStatementHandler的prepare方法,在RoutingStatementHandler对象执行prepare方法前处理自己的逻辑。这里只讲解了被拦截接口其中的一种,其他类似,就不再赘述。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值