MyBatis源码3_运行分析_04_拦截器

284 阅读8分钟

MyBatis运行

拦截器

拦截器功能

前面在MyBatis操作数据库中有介绍到,针对Executor、StatementHandler、ParameterHandler、ResultSetHandler都提供了拦截器增强。这四个组件主要工作在在于数据操作过程中,拦截器的功能就是拦截这四大组件的方法,方便使用MyBatis的开发者扩展或者干预操作数据库,如在了解了四大组件调用时机后,可以实现修改参数、修改sql、二次封装返回结果等功能,甚至数据同步,分库分表等功能都是可以的。

比较常见的框架,就如:PageHelper就是用了拦截器提供了自动分页功能,也有的自定义拦截器,实现了数据同步,缓存一致性等功能。

Mybatis允许用户通过自定义拦截器的方式改变sql的执行行为,例如在sql执行时追加sql分页语法,达到分页查询目的。用户自定义的插件只能对上文中所说的四大对象的方法进行拦截,这些方法分别是(包含方法重载):

​Executor​:update、query、flushStatements、commit、rollback、getTransaction、close、isClosed

​StatementHandler​: prepare、parameterize、batch、update、query

​ParameterHandler​: getParameterObject、setParameters

拦截器使用

拦截器接口声明:

public interface Interceptor {
  // 拦截到目标,调用方法
  Object intercept(Invocation invocation) throws Throwable;

  // 创建插件,使用动态代理增强当前对象,一般都是增强当前对象,也就是传this
  default Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }

  // 设置属性,在mybatis中,配置了plugin,可以设置key,value形式的属性,mybatis会封装成Properties,回调这个方法进行设置
  default void setProperties(Properties properties) {
    // NOP
  }

}

其它两个方法(intercept、setProperties)好理解,我们着重说一下这个方法Object plugin(Object target)

Object plugin(Object target) 方法:声明将拦截目标对象(target)使用拦截器(this)进行增强。我们前面已经有说道,拦截器可以拦截操作数据库的四大对象,因此这个的target就为它们四大对象中的其中一个,然后this就表示在执行target对象的拦截方法的时候,使用this#intercept方法增强。原理就是动态代理AOP,切点就是拦截tareget对象中的方法,增强就是当前对象的intercept方法(当然也可以是其它的Interceptor接口的实例,但是一般不会这样做)

分析完Interceptor接口声明,接下来我们以PageHelper来分析一下对拦截器的使用:

要使用拦截器,需要先用注解:@Intercepts声明要拦截的对象和方法,可以为多个。然后在MyBatis配置中将拦截器加入进去

    <plugins>
        <plugin interceptor="com.github.pagehelper.PageInterceptor">
            <!--参数配置-->
        </plugin>
    </plugins>
@Intercepts(
        {
                @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
                @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
        }
)
public class PageInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
            .......
            List resultList;
            //调用方法判断是否需要进行分页,如果不需要,直接返回结果
            if (!dialect.skip(ms, parameter, rowBounds)) {
                .......
                //判断是否需要进行 count 查询
                if (dialect.beforeCount(ms, parameter, rowBounds)) {
                    //查询总数
                    Long count = count(executor, ms, parameter, rowBounds, null, boundSql);
                    //处理查询总数,返回 true 时继续分页查询,false 时直接返回
                    if (!dialect.afterCount(count, parameter, rowBounds)) {
                        //当查询总数为 0 时,直接返回空的结果
                        return dialect.afterPage(new ArrayList(), parameter, rowBounds);
                    }
                }
                resultList = ExecutorUtil.pageQuery(dialect, executor,
                        ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);
            } else {
                //rowBounds用参数值,不使用分页插件处理时,仍然支持默认的内存分页
                resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
            }
            return dialect.afterPage(resultList, parameter, rowBounds);
    }


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

    @Override
    public void setProperties(Properties properties) {
        msCountMap = CacheFactory.createCache(properties.getProperty("msCountCache"), "ms", properties);
        String dialectClass = properties.getProperty("dialect");
        ..... 设置属性
    }

}

以上截取了对PageHelper分页关键代码的一部分,可以看到它首先它用@Intercepts注解声明了拦截Executor中的query方法。然后在拦截器中的intercept中,实现了自动分页逻辑。我们在自己实现拦截器的时候可以参考。

拦截器使用弊端:MyBatis的拦截器虽然使用非常灵活,但是如果对不熟悉MyBatis中数据库操作四大对象的同学来说(Executor,StatementHandler,ParamterHandler,ResultSetHandler),还是不太友好,不知道在使用@Intercept该配置拦截哪个方法,并且在什么时候被调用,以及在intercept方法中Invocation的参数是什么样的。还有就是使用了拦截器修改了sql或者参数返回结果等,在排查问题的时候有一定难度,因为不熟悉它的人,可能在排查mapper.xml什么的,发现sql啥的都没问题,但是就是拦截器给悄悄影响了会增加排查难度和学习成本。网上也好多人说PageHelper中的count和分页对于开发者来说是一个黑盒模式,不便于新手使用。

拦截器原理

要知道拦截器原理,先要带着问题去探究:拦截器能拦哪些?是怎么拦的?

拦截器能拦哪些?

前面已经返回说道了,拦截器能够拦截Executor、StatementHandler、ParameterHandler、ResultSetHandler这个四大组件的方法。

需要拦截的类需要在创建的时候调用interceptorChain.pluginAll(executor);进行注册为拦截器插件才可以使用。其实就是用拦截器,针对这个对象生成了一个动态代理对象,进行增强。

它们注册的源码都是在Configuration中创建四大组件里面:

Configuration#newExecutor():executor = (Executor) interceptorChain.pluginAll(executor);

Configuration#newStatementHandler:statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);

Configuration#newParameterHandler:parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);

Configuration#newResultSetHandler:resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);

InterceptorChain#pluginAll

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

拦截器是怎么拦截的?

要回答这个问题,先回顾一下Spring的AOP功能,AOP提供了配置目标增强,通过动态代理的方式在原来的对象上创建了代理对象,从而能够拦截目标方法的执行,并且增强。

MyBatis的拦截器是同样的原理,在上面的interceptorChain.pluginAll()方法注册了哪些对象是可以被拦截的,接下会在Interceptor里面调用回调方法Object plugin(Object target) -> Plugin.wrap(target, this);将当前拦截器进行生成代理对象增强,增强后,调用目标对象的方法的时候,会调用Interceptor#intercept方法。

动态代理,对Interceptor增强源码分析:

Plugin.wrap(target, this)

  public static Object wrap(Object target, Interceptor interceptor) {  
    // 获取需要增强的方法,以及判断是否能够增强,这里的Map中Class就是@Signature配置的type;Set<Method>是@Signature配置的method
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    Class<?> type = target.getClass();
    // 根据@Signature配置的Class获取是否有注册到插件列表中(interceptorChain.pluginAll(....))
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    if (interfaces.length > 0) {
      // 创建代理对象
      return Proxy.newProxyInstance(
          type.getClassLoader(),
          interfaces,
          new Plugin(target, interceptor, signatureMap));
    }
    return target;
  }

MyBatis的拦截器采用JDK代理,增强逻辑由Plugin#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)) {   
        // 回调拦截器中的intercept方法
        return interceptor.intercept(new Invocation(target, method, args));
      }
      return method.invoke(target, args);
    } catch (Exception e) {
      throw ExceptionUtil.unwrapThrowable(e);
    }
  }

拦截器参数:intercept(new Invocation(target, method, args))里面的target就是具体的四大组件的具体实例对象。

拦截器总结

MyBatis工作流程:

2023-05-08-20-13-16-image.png

MyBatis的拦截器实现思路跟Spring、Strusts、Filter的拦截器实现思路不一样,像SpringMVC的拦截器采用的是责任链设计模式,在拦截器中调用目标方法,将方法返回结果调用拦截器回调方法,进行一层层封装传递调用从而达到拦截器功能(这种方式相对来说对开发要去局限性比较大,被拦截对象要有明确的回调方法)。MyBatis采用的动态代理实现,具体是否拦截采用注解的方式进行判断,这种好处将拦截器更加灵活,一个拦截器可以拦截更多的方法,但是也带来了不可控的特性,开发者需要明确知道被拦截的对象中包含哪些方法和参数才能准确的拦截。

简单来说,MyBatis的拦截器就对应了AOP中的切面,@Signature就是声明切点。

单独使用MyBatis的拦截器

在了解了MyBatis的拦截器原理后,比如我们要自己实现有一个组件,使用MyBatis提供的拦截器,进行相应方法拦截。

定义被拦截目标的接口。主义MyBatis拦截器原理是JDK动态代理,因此一定要有接口,当然有时间我们可以自己实现一个Cglib的拦截器。

public interface Target {

    void invoke(String param);
}

被拦截目标实现

public class InterceptTargetImpl implements Target {

    @Override
    public void invoke(String param) {
        System.out.println(this + "调用拦截目标实现里面的方法:" + param);
    }
}

定义拦截器

@Intercepts(
        {@Signature(type = Target.class, method = "invoke", args = {String.class})}
)
public class TargetInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        System.out.println(this+"被拦截对象:" + invocation.getTarget() + "  被拦截方法:" + invocation.getMethod());
        return invocation.proceed();
    }
}

测试

public class MyBatisInterceptorTest {

    public static void main(String[] args) {
        InterceptorChain interceptorChain = new InterceptorChain();
        // 添加拦截器
        interceptorChain.addInterceptor(new TargetInterceptor());

        // 被拦截对象,使用拦截器增强
        Target target = new InterceptTargetImpl();
        target = (Target) interceptorChain.pluginAll(target);

        // 调用增强后的方法
        target.invoke("hello");
    }
}

结果:

TargetInterceptor@368239c8被拦截对象:InterceptTargetImpl@9e89d68  被拦截方法:public abstract void Target.invoke(java.lang.String)
InterceptTargetImpl@9e89d68调用拦截目标实现里面的方法:hello

当我们自己开发组件的时候也要参考这些优秀框架的开发思路,即使脱离了框架本身依旧能够很好的发挥作用。