本章主要描述 MyBatis 插件模块的原理,从以下两点出发:

  1. MyBatis 是如何加载插件配置的?
  2. MyBatis 是如何实现用户使用自定义拦截器对 SQL 语句执行过程中的某一点进行拦截的?

示例准备

首先准备两个拦截器示例,代码如下。

  1. @Intercepts({
  2. @Signature(type = Executor.class, method = "query",
  3. args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
  4. @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class,
  5. ResultHandler.class, CacheKey.class, BoundSql.class})})
  6. public class AInterceptor implements Interceptor {
  7. private static final Logger LOGGER = LoggerFactory.getLogger(AInterceptor.class);
  8. /**
  9. * 执行拦截逻辑的方法
  10. *
  11. * @param invocation
  12. * @return
  13. * @throws Throwable
  14. */
  15. @Override
  16. public Object intercept(Invocation invocation) throws Throwable {
  17. LOGGER.info("--------------执行拦截器A前--------------");
  18. Object obj = invocation.proceed();
  19. LOGGER.info("--------------执行拦截器A后--------------");
  20. return obj;
  21. }
  22. /**
  23. * 决定是否触发intercept()方法
  24. *
  25. * @param target
  26. * @return
  27. */
  28. @Override
  29. public Object plugin(Object target) {
  30. return Plugin.wrap(target, this);
  31. }
  32. /**
  33. * 根据配置初始化Interceptor对象
  34. *
  35. * @param properties
  36. */
  37. @Override
  38. public void setProperties(Properties properties) {
  39. }
  40. }
  1. @Intercepts({
  2. @Signature(type = Executor.class, method = "query",
  3. args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
  4. @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class,
  5. ResultHandler.class, CacheKey.class, BoundSql.class})})
  6. public class BInterceptor implements Interceptor {
  7. private static final Logger LOGGER = LoggerFactory.getLogger(BInterceptor.class);
  8. /**
  9. * 执行拦截逻辑的方法
  10. *
  11. * @param invocation
  12. * @return
  13. * @throws Throwable
  14. */
  15. @Override
  16. public Object intercept(Invocation invocation) throws Throwable {
  17. LOGGER.info("--------------执行拦截器B前--------------");
  18. Object obj = invocation.proceed();
  19. LOGGER.info("--------------执行拦截器B后--------------");
  20. return obj;
  21. }
  22. /**
  23. * 决定是否触发intercept()方法
  24. *
  25. * @param target
  26. * @return
  27. */
  28. @Override
  29. public Object plugin(Object target) {
  30. return Plugin.wrap(target, this);
  31. }
  32. /**
  33. * 根据配置初始化Interceptor对象
  34. *
  35. * @param properties
  36. */
  37. @Override
  38. public void setProperties(Properties properties) {
  39. }
  40. }

MyBatis 配置文件 mybatis-config.xml 增加 plugin 配置。

  1. <plugins>
  2. <plugin interceptor="com.yjw.mybatis.test.mybatis.plugin.AInterceptor"/>
  3. <plugin interceptor="com.yjw.mybatis.test.mybatis.plugin.BInterceptor"/>
  4. </plugins>

加载插件配置

在 MyBatis 初始化时,会通过 XMLConfigBuilder#pluginElement 方法解析 mybatis-config.xml 配置文件中定义的 节点,得到相应的 Interceptor 对象,最后将 Interceptor 对象添加到 Configuration.interceptorChain 字段中保存。源码如下所示。

  1. private void pluginElement(XNode parent) throws Exception {
  2. if (parent != null) {
  3. for (XNode child : parent.getChildren()) {
  4. String interceptor = child.getStringAttribute("interceptor");
  5. Properties properties = child.getChildrenAsProperties();
  6. // 创建Interceptor对象
  7. Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
  8. interceptorInstance.setProperties(properties);
  9. // 保存到Configuration.interceptorChain字段中
  10. configuration.addInterceptor(interceptorInstance);
  11. }
  12. }
  13. }
  14. public void addInterceptor(Interceptor interceptor) {
  15. interceptorChain.addInterceptor(interceptor);
  16. }

拦截过程

继续介绍 MyBatis 的拦截器如何对 Exector、StatementHandler、ParameterHandler、ResultSetHandler 进行拦截。

在 MyBatis 中使用的这四类对象,都是通过 Configuration 创建的,方法如下图所示。如果配置了自定义拦截器,则会在该系列方法中,通过 InterceptorChain.pluginAll() 方法为目标对象创建代理对象,所以通过 Configuration.new*() 系列方法得到的对象实际是一个代理对象。

image.png

以 newExecutor() 方法为例进行分析,其他方法原理类似,newExecutor() 方法的具体实现如下所示。

  1. public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
  2. executorType = executorType == null ? defaultExecutorType : executorType;
  3. executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
  4. Executor executor;
  5. // 默认是SIMPLE
  6. if (ExecutorType.BATCH == executorType) {
  7. executor = new BatchExecutor(this, transaction);
  8. } else if (ExecutorType.REUSE == executorType) {
  9. executor = new ReuseExecutor(this, transaction);
  10. } else {
  11. executor = new SimpleExecutor(this, transaction);
  12. }
  13. if (cacheEnabled) {
  14. executor = new CachingExecutor(executor);
  15. }
  16. // 通过InterceptorChain.pluginAll()方法创建Exector代理对象
  17. executor = (Executor) interceptorChain.pluginAll(executor);
  18. return executor;
  19. }

在 InterceptorChain.pluginAll() 方法会遍历 interceptors 集合,并调用每个 interceptor 的 plugin() 方法创建代理对象,具体实现如下所示。

  1. public Object pluginAll(Object target) {
  2. for (Interceptor interceptor : interceptors) {
  3. target = interceptor.plugin(target);
  4. }
  5. return target;
  6. }

一般我们自定义拦截器的 plugin 方法,会使用 MyBatis 提供的 Plugin 工具类,它实现了 InvocationHandler 接口,并提供了 wrap() 静态方法用于创建代理对象,Plugin.wrap() 方法的具体实现如下所示。

  1. public static Object wrap(Object target, Interceptor interceptor) {
  2. // 获取用户自定义Interceptor中@Signature注解的信息
  3. // getSignatureMap()方法负责处理@Signature注解
  4. Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
  5. // 获取目标类型
  6. Class<?> type = target.getClass();
  7. // 获取目标类型实现的接口
  8. Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
  9. if (interfaces.length > 0) {
  10. // 使用JDK动态代理的方式创建代理对象
  11. return Proxy.newProxyInstance(
  12. type.getClassLoader(),
  13. interfaces,
  14. new Plugin(target, interceptor, signatureMap));
  15. }
  16. return target;
  17. }
  18. private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
  19. Set<Class<?>> interfaces = new HashSet<Class<?>>();
  20. while (type != null) {
  21. for (Class<?> c : type.getInterfaces()) {
  22. if (signatureMap.containsKey(c)) {
  23. interfaces.add(c);
  24. }
  25. }
  26. type = type.getSuperclass();
  27. }
  28. return interfaces.toArray(new Class<?>[interfaces.size()]);
  29. }

示例中 Exector 存在两个拦截器 AInterceptor 和 BInterceptor,在执行 InterceptorChain.pluginAll() 方法的时候,传给 getAllInterfaces() 方法的 type 字段第一次是 CacheExector 对象,第二次是 CacheExector 的代理对象,因为生成的代理对象也继承 Exector 接口,signatureMap.containsKey(c) 可以获得值,继续生成代理的代理对象,结构如下图所示。

image.png

在 Plugin.invoke() 方法中,会将当前调用方法与 signatureMap 集合中记录的方法信息进行比较,如果当前调用的方法是需要被拦截的方法,则调用其 intercept() 方法进行处理,如果不能被拦截则直接调用 target 的相应方法。Plugin.invoke() 方法的具体实现如下所示。

  1. @Override
  2. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  3. try {
  4. // 获取当前方法所在类或接口中,可被当前 Interceptor拦截的方法
  5. Set<Method> methods = signatureMap.get(method.getDeclaringClass());
  6. // 如果当前调用的方法需要被拦截,则调用interceptor.intercept()方法进行拦截处理
  7. if (methods != null && methods.contains(method)) {
  8. return interceptor.intercept(new Invocation(target, method, args));
  9. }
  10. // 如果当前调用的方法不能被拦截,则调用target对象的相应方法
  11. return method.invoke(target, args);
  12. } catch (Exception e) {
  13. throw ExceptionUtil.unwrapThrowable(e);
  14. }
  15. }

Interceptor.intercept() 方法的参数是 Invocation 对象,其中封装了目标对象、目标方法以及调用目标方法的参数,并提供了 process() 方法调用目标方法,如下所示。

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

需要注意的是,在 Interceptor.intercept() 方法中执行完拦截处理之后,如果需要调用目标方法,则通过 Invocation.process() 方法实现。

根据上面的分析,就不难理解示例的如下输出日志了,同时配置文件中插件的执行顺序也清楚了。

  1. [main] DEBUG org.apache.ibatis.logging.LogFactory - Logging initialized using 'class org.apache.ibatis.logging.slf4j.Slf4jImpl' adapter.
  2. [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - PooledDataSource forcefully closed/removed all connections.
  3. [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - PooledDataSource forcefully closed/removed all connections.
  4. [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - PooledDataSource forcefully closed/removed all connections.
  5. [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - PooledDataSource forcefully closed/removed all connections.
  6. [main] INFO com.yjw.mybatis.test.mybatis.plugin.BInterceptor - --------------执行拦截器B前--------------
  7. [main] INFO com.yjw.mybatis.test.mybatis.plugin.AInterceptor - --------------执行拦截器A前--------------
  8. [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Opening JDBC Connection
  9. [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - Created connection 360067785.
  10. [main] DEBUG com.yjw.mybatis.dao.StudentMapper.selectByPrimaryKey - ==> Preparing: select id, name, sex, selfcard_no, note from t_student where id = ?
  11. [main] DEBUG com.yjw.mybatis.dao.StudentMapper.selectByPrimaryKey - ==> Parameters: 1(Long)
  12. [main] DEBUG com.yjw.mybatis.dao.StudentMapper.selectByPrimaryKey - <== Total: 1
  13. [main] INFO com.yjw.mybatis.test.mybatis.plugin.AInterceptor - --------------执行拦截器A后--------------
  14. [main] INFO com.yjw.mybatis.test.mybatis.plugin.BInterceptor - --------------执行拦截器B后--------------
  15. Student [Hash = 550752602, id=1, name=张三, sex=1, selfcardNo=111, note=zhangsan]

作者:殷建卫 链接:https://www.yuque.com/yinjianwei/vyrvkf/ug43eu 来源:殷建卫 - 架构笔记 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。