Mybatis拦截器实现的原理

Mybatis拦截器实现的原理

从源码角度来分析实现原理,不涉及到具体使用例子。

1. 拦截器长什么样子

/**
 * @author Clinton Begin
 */
public interface Interceptor {

  Object intercept(Invocation invocation) throws Throwable;

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

  default void setProperties(Properties properties) {
    // NOP
  }

}

大多数的实现都是实现了intercept方法,对于下面两个方法都没有怎么关注过。那么下面就来看看他是什么样子

default Object plugin(Object target)
  1. Plugin实现InvocationHandler,那么这里肯定有jdk的动态代理起作用。这里肯定要看他invoke方法。等会再说

  2. 再看它里面有三个参数

      private final Object target; //原始对象
      private final Interceptor interceptor; //拦截器
      private final Map<Class<?>, Set<Method>> signatureMap; //
    
  3. 再看wrap方法之前,先看看warp方法里面getSignatureMap

      private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
        //拿到拦截器上Intercepts注解
        Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
        // issue #251 
        if (interceptsAnnotation == null) {
          throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());
        }
        //Signature注解
        Signature[] sigs = interceptsAnnotation.value();
        
        //这个map里面保存着 需要拦截的class和这个class里面需要拦截的method。
        Map<Class<?>, Set<Method>> signatureMap = new HashMap<>();
        for (Signature sig : sigs) {
          Set<Method> methods = MapUtil.computeIfAbsent(signatureMap, sig.type(), k -> new HashSet<>());
          try {
            //得到Signature里面指定的方法,通过 type和args,method。
            //这里就是通过反射来获取指定类上面的具体方法,并且缓存起来。
            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;
      }
    
  4. 继续看wrap方法

      public static Object wrap(Object target, Interceptor interceptor) {
        Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
        Class<?> type = target.getClass();
    
        // 得到 查找type的所有的接口,是否包含在signatureMap里面,将包含的返回回来。注意,这的接口是查找所有的接口,包括父接口
        // signatureMap上面不是已经缓存了,需要拦截的类和具体方法的集合嗎。那么这里就需要遍历目标类以及他的父接口,看看目标类中那些接口中的那些方法需要利用代理。
        Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
        if (interfaces.length > 0) {
          
          //这里就很熟悉了,直接new代理。
          return Proxy.newProxyInstance(
              type.getClassLoader(),
              interfaces,
              new Plugin(target, interceptor, signatureMap));
        }
        return target;
      }
    
  5. 作为代理必要看看看invoke方法

      @Override
      public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
          //生成了代理对象之后。在调用方法的时候就会调用invoke方法,通过前面缓存的signatureMap来判断当前调用的方法是否需要
          // 利用拦截器。
          Set<Method> methods = signatureMap.get(method.getDeclaringClass());
          if (methods != null && methods.contains(method)) {
            //直接调用代理对象的方法。注意,这里传递过去的 实例可是未包装的对象,不是proxy。这个proxy其实是代理对象,如果
            // 直接调用proxy的话,就会造成栈溢出。
            return interceptor.intercept(new Invocation(target, method, args));
          }
          //这里在调用方法的时候也用的是原来的对象
          return method.invoke(target, args);
        } catch (Exception e) {
          throw ExceptionUtil.unwrapThrowable(e);
        }
      }
    
default void setProperties(Properties properties)

这个方法只是用来设置属性的。下面会讲

2. 拦截器是在什么时候创建出来的。

首先要知道,拦截器是代码写完之后,如果在mybatis里面是需要配置在xml文件中,如果在spring或者Springboot中就可以交给spring去管理,也可以交给mybatis来管理。

在这里只是单纯看在Mybatis中是怎么做的。

  <plugins>
    <plugin interceptor="org.apache.ibatis.session.mybatis.MyInterceptor">
      <property name="name" value="myInterceptor"/>
    plugin>
  plugins>

只要从经典的new SqlSessionFactoryBuilder().build(reader);开始看,就可以看到下面的代码

 private void parseConfiguration(XNode root) {
    try {
      // issue #117 read properties first
      propertiesElement(root.evalNode("properties")); //解析properties标签,并把他放在 parser和config的 Variables 里面
      Properties settings = settingsAsProperties(root.evalNode("settings"));//加载setting标签
      loadCustomVfs(settings); //lcnote 这里的vfs是啥?怎么用
      loadCustomLogImpl(settings);
      typeAliasesElement(root.evalNode("typeAliases"));

      //从这里开始,都是解析具体的标签,new出对象,将标签下面的属性设置进去,
      // 从解析的这里基本也能看出mybatis里面重要的几个点,首先是objectFactory,objectFactory。objectFactory,plugins
      pluginElement(root.evalNode("plugins"));
      
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectFactory"));
      reflectorFactoryElement(root.evalNode("objectFactory"));

      //这里就具体设置setting标签了
      settingsElement(settings);

      // read it after objectFactory and objectWrapperFactory issue #631
      environmentsElement(root.evalNode("environments"));
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      typeHandlerElement(root.evalNode("typeHandlers"));
      
      //lcnote 这里是重点,解析mapper文件,
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }

上面就是mybatis解析配置文件的重要代码,这里只看 pluginElement(root.evalNode("plugins"))

pluginElement(root.evalNode(“plugins”))
  // 这里会直接new出来实现了Interceptor接口的类,并且把interceptor标签下面的属性都调用setProperties设置进去,并且把这个类添加到 Interceptor里面
  private void pluginElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        String interceptor = child.getStringAttribute("interceptor");
        Properties properties = child.getChildrenAsProperties();//这个就是property标签里面的元素,这里会变为一个 Properties
        
        // 重点就是下面这里,解析到一个,通过构造方法直接创建出来,注意,这里是无参构造。
        Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
        //设置属性,这里调用就是 拦截器里面的setProperties方法。设置属性了
        interceptorInstance.setProperties(properties);
        
        //添加到拦截器集合中,注意Mybatis中一个很重要的对象 configuration。这个对象基本上包含了Mybatis中的所有。
        configuration.addInterceptor(interceptorInstance);
      }
    }
  }

看到这里,也就明白了,在解析配置文件,解析到 plugins 标签的时候创建出来的。通过无参构造。

3. 拦截器可以用在什么地方?

有一个简单的方式,直接可以看到,之前说了configuration对象中包含了Mybatis运行时候的所有的东西,并且从上面的代码也可以看出,将所有的代理对象添加到了 configuration.addInterceptor(interceptorInstance);里面。那么只要看看在哪里调用就好了,很简单。直接看。
Mybatis拦截器实现的原理_第1张图片

四个地方

  1. ParameterHandler (设置参数)
  2. ResultSetHandler (处理结果)
  3. StatementHandler (这就是jdbc中的通过Connection获取到的Statement)
  4. Executor (执行器,从逻辑上来看,整个过程,从设置参数,解析sql,处理返回结果,这一个过程都需要在一个执行器中完成。并且Mybatis支持三种 SIMPLE, REUSE, BATCH,还有一种CachingExecutor,这只是代理。真正的实现还是委托给了前三种)

在创建这四个对象的时候,调用interceptorChain.pluginAll(parameterHandler)通过上面的分析,可以知道,这里会创建代理对象。如果拦截器配置的话。通过之前的JDK可以知道,如果是动态代理对象的话,在每一个方法调用的时候就会调用InvocationHandler,同样的这里也会这样。意思就是调用所有方法的时候都有可能用到拦截器。

4. 拦截器为啥要有Intercepts注解呢?

想想Jdk的动态代理,只要是动态代理对象,在调用方法的时候都会调用InvocationHandler。所有的方法都会,那这不是很好吧?想要的是,在指定的方法上面做拦截,而不是全部。这就是InterceptsSignature注解的作用。从这两个注解就可以解析出,什么接口上的什么方法需要应用拦截器。从而达到目的。

说到这里,我想到了cglib,

cglib和jdk的代理不一样的地方,除了jdk的实现是接口,cglib可以给类增强,但是两者是实现本质上都是字节码,都是生成了一个新的类,然后继承要代理的类或者实现了要代理的接口。我觉得两者最大的区别就是cglib可以支持多个MethodInterceptor并且可以通过CallbackFilter来指定每个方法应该执行那个MethodInterceptor。

Mybatis拦截器就到这里了,如有不正确的地方,欢迎指出。谢谢。

你可能感兴趣的:(mybaties,java)