暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Mybatis 插件机制

我与风来 2021-06-28
1214

官方文档中提到:Mybatis 允许在映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatais 允许使用插件来拦截的方法调用包括:

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)

  • ParameterHandler (getParameterObject, setParameters)

  • ResultSetHandler (handleResultSets, handleOutputParameters)

  • StatementHandler (prepare, parameterize, batch, update, query)

插件的使用非常简单,只需要实现 Interceptor
接口,并指定想要拦截的方法签名即可。

// ExamplePlugin.java
@Intercepts({@Signature(
  type= Executor.class,
  method = "update",
  args = {MappedStatement.class,Object.class})})
public class ExamplePlugin implements Interceptor {
  private Properties properties = new Properties();
  public Object intercept(Invocation invocation) throws Throwable {
    // implement pre processing if need
    Object returnObject = invocation.proceed();
    // implement post processing if need
    return returnObject;
  }
  public void setProperties(Properties properties) {
    this.properties = properties;
  }
}

然后将该插件配置到 Configuration
中:

java形式

    @Autowired
    private List<SqlSessionFactory> sqlSessionFactoryList;
    @Bean
    public MybatisEntityPluginInterceptor mybatisEntityPluginInterceptor(){
        ExamplePlugin e = new ExamplePlugin();
        for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
            sqlSessionFactory.getConfiguration().addInterceptor(e);
        }
        return e;
    }

或者使用xml配置

<!-- mybatis-config.xml -->
<plugins>
  <plugin interceptor="org.mybatis.example.ExamplePlugin">
    <property name="someProperty" value="100"/>
  </plugin>
</plugins>

插件类最终会被放入到 Configuration
对象的 interceptorChain
成员变量中。

上一篇描述执行过程时,忽略了插件,下面列出的是在上篇文章提到插件机制的地方:

  • Executor 创建的时候:  
    executor = (Executor) interceptorChain.pluginAll(executor);

  • StatementHandler 创建的时候:  
    statementHandler=(StatementHandler)interceptorChain.pluginAll(statementHandler);

  • ParameterHandler 创建的时候:  
    parameterHandler=(ParameterHandler)interceptorChain.pluginAll(parameterHandler);

  • ResultSetHandler 创建的时候:  
    resultSetHandler= (ResultSetHandler)interceptorChain.pluginAll(resultSetHandler);

这刚好与上文官方文档给出的插件支持的拦截相吻合,为了支撑拦截,我们能看到的似乎只有这一行代码,而且对于不同的对象,代码居然相同。下面来细谈这个过程。

首先,看到 InterceptorChain
这个类,该类的代码比较简单:

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;
  }

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

  public List<Interceptor> getInterceptors() {
    return Collections.unmodifiableList(interceptors);
  }

}

假设 interceptors 已正确赋值,我们的插件 ExamplePlugin
已正确的添加了进来。那么在执行过程中,调用的 pluginAll()
方法遍历了拦截器,并调用拦截器的 plugin 方法。

拦截器的类定义为:

public interface Interceptor {

  Object intercept(Invocation invocation) throws Throwable;

  Object plugin(Object target);

  void setProperties(Properties properties);

}

所以,plugin 方法由自己实现,实现一般如下:

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

可以看出做拦截,其实是包装目标对象,并返回新的对象出去。mybatis 提供了一个这样的静态方法:Plugin.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);
    if (interfaces.length > 0) {
      return Proxy.newProxyInstance(
          type.getClassLoader(),
          interfaces,
          new Plugin(target, interceptor, signatureMap));
    }
    return target;
  }

    private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
    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[] 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;
  }

这一步所做的操作是根据 Intercepts
注解,获取需要代理的接口和方法,然后根据接口生成反射对象,最终返回的便是这个反射生成的对象。

所以,插件机制的本质便是通过反射需要的 InvocationHandler
类来完成调用拦截
,这里 Plugin 便是实现了
InvocationHandler
类的调用处理器,查看它的 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)) {
        return interceptor.intercept(new Invocation(target, method, args));
      }
      return method.invoke(target, args);
    } catch (Exception e) {
      throw ExceptionUtil.unwrapThrowable(e);
    }
  }

可以看出只拦截通过注解指定的方法,并且会将目标对象、方法和参数包装为 Invocation
对象,传递给拦截器的 intercept()
方法,由拦截器自己决定是否还需要再调用目标对象的原方法。,这整个过程可总结为如下图(以 Executor 为例):

Mybatis 插件全过程

对象的生成过程一般与它在运行时方法的调用过程顺序相反。

这便是整个拦截过程了。从本质来看,它是通过代理实现。需要注意的是拦截器调用的顺序。比如说拦截器 A,B,C 按照顺序添加到这个 InterceptorChain
中,那么调用的顺序应该是相反的,即 C, B, A, target。当然,是否允许方法的调用蔓延这整个链,需要你主动的在拦截器的 intercept() 方法中调用传入的参数 Invocation 对象的 proceed() 方法。

mybatis 确实使用了较多的反射,并且这还是在执行映射方法的过程中生成的。倘若在实现 Interceptor 的 plugin 方法时,根据拦截器想要拦截的目标类的类型做下判断,然后再决定是否使用 Plugin
进行 wrap 操作,那么,这应该算个小的优化。虽然当类型不匹配时,调用 plugin 方法不会产生任何错误(它返回了target),但它需要执行额外的解析拦截器元数据动作。所以,我们能够提前做出判断来避免这种情况是最好的。

mybatis 的插件机制足够简单,这也决定了它的限制很小,我们可以尽情发挥。不过在试图修改或重写已有方法的行为时,很可能会破坏 MyBatis 的核心模块。因为这些都是更底层的类和方法,所以使用插件的时候需要确保自己足够了解,即便如此,也需要特别当心。


文章转载自我与风来,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论