MyBatis12-《通用源码指导书:MyBatis源码详解》笔记-session、plugin包

本系列文章是我从《通用源码指导书:MyBatis源码详解》一书中的笔记和总结
本书是基于MyBatis-3.5.2版本,书作者 易哥 链接里是CSDN中易哥的微博。但是翻看了所有文章里只有一篇简单的介绍这本书。并没有过多的展示该书的魅力。接下来我将自己的学习总结记录下来。如果作者认为我侵权请联系删除,再次感谢易哥提供学习素材。本段说明将伴随整个系列文章,尊重原创,本人已在微信读书购买改书。
版权声明:本文为CSDN博主「架构师易哥」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/onlinedct/article/details/107306041

session包是整个 MyBatis应用的对外接口包,是离用户最近的包。

    public static void main(String[] args) {
    
    
        // 第一阶段:MyBatis的初始化阶段
        String resource = "mybatis-config.xml";
        // 得到配置文件的输入流
        InputStream inputStream = null;
        try {
    
    
            inputStream = Resources.getResourceAsStream(resource);
        } catch (IOException e) {
    
    
            e.printStackTrace();
        }
        // 得到SqlSessionFactory
        SqlSessionFactory sqlSessionFactory =
                new SqlSessionFactoryBuilder().build(inputStream);

        // 第二阶段:数据读写阶段
        try (SqlSession session = sqlSessionFactory.openSession()) {
    
    
            // 找到接口对应的实现
            UserMapper userMapper = session.getMapper(UserMapper.class);
            // 组建查询参数
            User userParam = new User();
            userParam.setSchoolName("Sunny School");
            // 调用接口展开数据库操作
            List<User> userList =  userMapper.queryUserBySchoolName(userParam);
            // 打印查询结果
            for (User user : userList) {
    
    
                System.out.println("name : " + user.getName() + " ;  email : " + user.getEmail());
            }
        }
    }

代码中涉及的 SqlSessionFactory类、SqlSession类都是session包中的类,通过这些类就可以触发 MyBatis对数据库展开操作。这也验证了 session包是整个 MyBatis的对外接口包这一结论。

1.SqlSession及其相关类

在进行查询操作时,只需要和SqlSession对象打交道即可。而 SqlSession 对象是由SqlSessionFactory 生产出来的,SqlSessionFactory 又是由SqlSessionFactoryBuilder创建的。
在这里插入图片描述

1.1.SqlSession的生成链

SqlSession及其相关类组成了一个生成链。SqlSessionFactoryBuilder生成了 SqlSessionFactory,SqlSessionFactory生成了 SqlSession。
SqlSessionFactoryBuilder 类是 SqlSessionFactory 的建造者类,它能够根据配置文件创建出 SqlSessionFactory 对象。下面来看 SqlSessionFactoryBuilder 类中一个核心的build方法。

  /**
   * 建造一个SqlSessionFactory对象
   * @param reader 读取字符流的抽象类
   * @param environment 环境信息
   * @param properties 配置信息
   * @return SqlSessionFactory对象
   */
  public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
    
    
    try {
    
    
      // 传入配置文件,创建一个XMLConfigBuilder类
      XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
      // 分两步:
      // 1、解析配置文件,得到配置文件对应的Configuration对象
      // 2、根据Configuration对象,获得一个DefaultSqlSessionFactory
      return build(parser.parse());
    } catch (Exception e) {
    
    
      throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
    
    
      ErrorContext.instance().reset();
      try {
    
    
        reader.close();
      } catch (IOException e) {
    
    
      }
    }
  }

创建 SqlSessionFactory对象的过程主要分为三步:

  1. 传入配置文件,创建一个 XMLConfigBuilder类准备对配置文件展开解析。
  2. 解析配置文件,得到配置文件对应的 Configuration对象。
  3. 根据 Configuration对象,获得一个DefaultSqlSessionFactory。

DefaultSqlSessionFactory对象则可以创建出 SqlSession的子类 DefaultSqlSession类的对象,该过程由openSessionFromDataSource方法完成:

  /**
   * 从数据源中获取SqlSession对象
   * @param execType 执行器类型
   * @param level 事务隔离级别
   * @param autoCommit 是否自动提交事务
   * @return SqlSession对象
   */
  private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    
    
    Transaction tx = null;
    try {
    
    
      // 找出要使用的指定环境
      final Environment environment = configuration.getEnvironment();
      // 从环境中获取事务工厂
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      // 从事务工厂中生产事务
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      // 创建执行器
      final Executor executor = configuration.newExecutor(tx, execType);
      // 创建DefaultSqlSession对象
      return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
    
    
      closeTransaction(tx); // may have fetched a connection so lets call close()
      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
    
    
      ErrorContext.instance().reset();
    }
  }

1.2.DefaultSqlSession类

我们已经说过,session包是整个 MyBatis应用的对外接口包,而 executor包是最为核心的执行器包。DefaultSqlSession 类做的主要工作则非常简单——把接口包的工作交给执行器包处理。

public class DefaultSqlSession implements SqlSession {
    
    
  // 配置信息
  private final Configuration configuration;
  // 执行器
  private final Executor executor;
  // 是否自动提交
  private final boolean autoCommit;
  // 缓存是否已经被污染
  private boolean dirty;
  // 游标列表
  private List<Cursor<?>> cursorList;

DefaultSqlSession类的属性中包含一个 Executor对象,DefaultSqlSession类将主要的操作都交给属性中的 Executor对象处理。以selectList方法为例,相关数据库查询操作都由 Executor对象的 query方法来完成。

  /**
   * 查询结果列表
   * @param <E> 返回的列表元素的类型
   * @param statement SQL语句
   * @param parameter 参数对象
   * @param rowBounds  翻页限制条件
   * @return 结果对象列表
   */
  @Override
  public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    
    
    try {
    
    
      // 获取查询语句
      MappedStatement ms = configuration.getMappedStatement(statement);
      // 交由执行器进行查询
      return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
    } catch (Exception e) {
    
    
      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
    
    
      ErrorContext.instance().reset();
    }
  }

1.3.SqlSessionManager类

在 SqlSession的相关类中,SqlSessionManager既实现了SqlSessionFactory接口又实现了 SqlSession接口
在这里插入图片描述
这种既实现工厂接口又实现工厂产品接口的类是很少见的。因此,我们单独研究一下SqlSessionManager类是如何实现的,以及其存在的意义。

public class SqlSessionManager implements SqlSessionFactory, SqlSession {
    
    

  // 构造方法中传入的SqlSessionFactory对象
  private final SqlSessionFactory sqlSessionFactory;
  // 在构造方法中创建的SqlSession代理对象
  private final SqlSession sqlSessionProxy;
  // 该变量用来存储被代理的SqlSession对象
  private final ThreadLocal<SqlSession> localSqlSession = new ThreadLocal<>();

  /**
   * SqlSessionManager构造方法
   * @param sqlSessionFactory SqlSession工厂
   */
  private SqlSessionManager(SqlSessionFactory sqlSessionFactory) {
    
    
    this.sqlSessionFactory = sqlSessionFactory;
    this.sqlSessionProxy = (SqlSession) Proxy.newProxyInstance(
        SqlSessionFactory.class.getClassLoader(),
        new Class[]{
    
    SqlSession.class},
        new SqlSessionInterceptor());
  }

SqlSessionManager在构造方法中创建了一个 SqlSession的代理对象,该代理对象可以拦截被代理对象的方法。拦截到的方法会交给SqlSessionInterceptor内部类的invoke方法进行处理。

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    
    
      // 尝试从当前线程中取出SqlSession对象
      final SqlSession sqlSession = SqlSessionManager.this.localSqlSession.get();
      if (sqlSession != null) {
    
     // 当前线程中确实取出了SqlSession对象
        try {
    
    
          // 使用取出的SqlSession对象进行操作
          return method.invoke(sqlSession, args);
        } catch (Throwable t) {
    
    
          throw ExceptionUtil.unwrapThrowable(t);
        }
      } else {
    
     // 当前线程中还没有SqlSession对象
        // 使用属性中的SqlSessionFactory对象创建一个SqlSession对象
        try (SqlSession autoSqlSession = openSession()) {
    
    
          try {
    
    
            // 使用新创建的SqlSession对象进行操作
            final Object result = method.invoke(autoSqlSession, args);
            autoSqlSession.commit();
            return result;
          } catch (Throwable t) {
    
    
            autoSqlSession.rollback();
            throw ExceptionUtil.unwrapThrowable(t);
          }
        }
      }
    }

可以看出,当 SqlSession的代理对象拦截到方法时,会尝试从当前线程的 ThreadLocal中取出一个 SqlSession对象。

  • 如果 ThreadLocal中存在 SqlSession对象,代理对象则将操作交给取出的 SqlSession对象进行处理。
  • 如果 ThreadLocal中不存在 SqlSession对象,则使用属性中的 SqlSessionFactory对象创建一个 SqlSession对象,然后代理对象将操作交给新创建的 SqlSession对象进行处理。

SqlSessionManager各个属性的含义也清晰起来

  // 构造方法中传入的SqlSessionFactory对象
  private final SqlSessionFactory sqlSessionFactory;
  // 在构造方法中创建的SqlSession代理对象
  private final SqlSession sqlSessionProxy;
  // 该变量用来存储被代理的SqlSession对象
  private final ThreadLocal<SqlSession> localSqlSession = new ThreadLocal<>();

了解了SqlSessionManager的主要方法和属性的含义之后,其结构已经十分清晰了,那它存在的意义又是什么呢?毕竟作为工厂的 DefaultSqlSessionFactory或作为产品的DefaultSqlSession都能实现它的功能。其实SqlSessionManager将工厂和产品整合到一起后,提供了下面两点功能。

  • SqlSessionManager总能给出一个产品(从线程ThreadLocal取出或者新建)并使用该产品完成相关的操作,外部使用者不需要了解细节,因此省略了调用工厂生产产品的过程。
  • 提供了产品复用的功能。工厂生产出的产品可以放入线程ThreadLocal 保存(需要显式调用 startManagedSession 方法),从而实现产品的复用。这样既保证了线程安全又提升了效率。

很多场景下,用户使用的是工厂生产出来的产品,而不关心产品是即时生产的还是之前生产后缓存的。在这种情况下,可以参考 SqlSessionManager的设计,来提供一种更为高效的给出产品的方式。在源码阅读的过程中,我们可能无法提前得知某些类的功能。这时候需要先阅读其源码,然后在源码的基础上猜测其功能。这种源码阅读的方式比较费时费力,但有时却难以避免。我们在阅读 SqlSessionManager源码时就采用了这种方式。

2.Configuration类

我们知道配置文件 mybatis-config.xml是 MyBatis配置的主入口,包括映射文件的路径也是通过它指明的。而配置文件的根节点就是 configuration 节点,因此该节点内保存了所有的配置信息。

configuration节点的信息经过解析后都存入了 Configuration对象中,因此 Configuration对象中就包含了 MyBatis运行的所有配置信息。

并且 Configuration类还对配置信息进行了进一步的加工,为许多配置项设置了默认值,为许多实体定义了别名等。因而Configuration类是MyBatis中极为重要的一个类。

/**
 * 主要内容分为以下几个部分:
 * 1、大量的配置项,和与`<configuration>`标签中的配置对应
 * 2、创建类型别名注册机,并向内注册了大量的类型别名
 * 3、创建了大量Map,包括存储映射语句的Map,存储缓存的Map等,这些Map使用的是一种不允许覆盖的严格Map
 * 4、给出了大量的处理器的创建方法,包括参数处理器、语句处理器、结果处理器、执行器。
 *    注意这里并没有真正创建,只是给出了方法。
 */

public class Configuration {
    
    

  // <environment>节点的信息
  protected Environment environment;

  // 以下为<settings>节点中的配置信息
  protected boolean safeRowBoundsEnabled;
  protected boolean safeResultHandlerEnabled = true;
  protected boolean mapUnderscoreToCamelCase;
  protected boolean aggressiveLazyLoading;
  protected boolean multipleResultSetsEnabled = true;
  protected boolean useGeneratedKeys;
  protected boolean useColumnLabel = true;
  protected boolean cacheEnabled = true;
  protected boolean callSettersOnNulls;
  protected boolean useActualParamName = true;
  protected boolean returnInstanceForEmptyRow;

  protected String logPrefix;
  protected Class<? extends Log> logImpl;
  protected Class<? extends VFS> vfsImpl;
  protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;
  protected JdbcType jdbcTypeForNull = JdbcType.OTHER;
  protected Set<String> lazyLoadTriggerMethods = new HashSet<>(Arrays.asList("equals", "clone", "hashCode", "toString"));
  protected Integer defaultStatementTimeout;
  protected Integer defaultFetchSize;
  protected ResultSetType defaultResultSetType;
  protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;
  protected AutoMappingBehavior autoMappingBehavior = AutoMappingBehavior.PARTIAL;
  protected AutoMappingUnknownColumnBehavior autoMappingUnknownColumnBehavior = AutoMappingUnknownColumnBehavior.NONE;
  // 以上为<settings>节点中的配置信息

  // <properties>节点信息
  protected Properties variables = new Properties();
  // 反射工厂
  protected ReflectorFactory reflectorFactory = new DefaultReflectorFactory();
  // 对象工厂
  protected ObjectFactory objectFactory = new DefaultObjectFactory();
  // 对象包装工厂
  protected ObjectWrapperFactory objectWrapperFactory = new DefaultObjectWrapperFactory();
  // 是否启用懒加载,该配置来自<settings>节点
  protected boolean lazyLoadingEnabled = false;
  // 代理工厂
  protected ProxyFactory proxyFactory = new JavassistProxyFactory(); // #224 Using internal Javassist instead of OGNL
  // 数据库编号
  protected String databaseId;
  // 配置工厂,用来创建用于加载反序列化的未读属性的配置。
  protected Class<?> configurationFactory;
  // 映射注册表
  protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
  // 拦截器链(用来支持插件的插入)
  protected final InterceptorChain interceptorChain = new InterceptorChain();
  // 类型处理器注册表,内置许多,可以通过<typeHandlers>节点补充
  protected final TypeHandlerRegistry typeHandlerRegistry = new TypeHandlerRegistry();
  // 类型别名注册表,内置许多,可以通过<typeAliases>节点补充
  protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();
  // 语言驱动注册表
  protected final LanguageDriverRegistry languageRegistry = new LanguageDriverRegistry();
  // 映射的数据库操作语句
  protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection")
      .conflictMessageProducer((savedValue, targetValue) ->
          ". please check " + savedValue.getResource() + " and " + targetValue.getResource());
  // 缓存
  protected final Map<String, Cache> caches = new StrictMap<>("Caches collection");
  // 结果映射,即所有的<resultMap>节点
  protected final Map<String, ResultMap> resultMaps = new StrictMap<>("Result Maps collection");
  // 参数映射,即所有的<parameterMap>节点
  protected final Map<String, ParameterMap> parameterMaps = new StrictMap<>("Parameter Maps collection");
  // 主键生成器映射
  protected final Map<String, KeyGenerator> keyGenerators = new StrictMap<>("Key Generators collection");
  // 载入的资源,例如映射文件资源
  protected final Set<String> loadedResources = new HashSet<>();
  // SQL语句片段,即所有的<sql>节点
  protected final Map<String, XNode> sqlFragments = new StrictMap<>("XML fragments parsed from previous mappers");

  // 暂存未处理完成的一些节点
  protected final Collection<XMLStatementBuilder> incompleteStatements = new LinkedList<>();
  protected final Collection<CacheRefResolver> incompleteCacheRefs = new LinkedList<>();
  protected final Collection<ResultMapResolver> incompleteResultMaps = new LinkedList<>();
  protected final Collection<MethodResolver> incompleteMethods = new LinkedList<>();

  // 用来存储跨namespace的缓存共享设置
  protected final Map<String, String> cacheRefMap = new HashMap<>();

MyBatis中的 BaseBuilder、BaseExecutor、Configuration、ResultMap等近 20个类都在属性中引用了 Configuration对象,这使得 Configuration对象成了 MyBatis全局共享的配置信息中心,能为其他对象提供配置信息的查询和更新服务。

Configuration 类是为了保存配置信息而设置的解析实体类,虽然成员变量众多,但成员方法却都很简单,不再展开介绍。

为了便于配置信息的快速查询,Configuration 类中还设置了一个内部类 StrictMap。StrictMap是 HashMap的子类,它有以下特点。

  • 不允许覆盖其中的键值。即如果要存入的键已经在 StrictMap 中存在了,则会直接抛出异常。这一点杜绝了配置信息因为覆盖发生的混乱。
  • 自动尝试使用短名称再次存入给定数据。例如,向 StrictMap 中存入键为“com.github.yeecode.clazzName”的数据,则除了存入该数据外,StrictMap 还会以“clazzName”为键再存入一份(如果短名称“clazzName”不会引发歧义的话)。这使得配置信息支持以短名称进行查询(如果短名称不会引发歧义的话)。
    /**
     * 向Map中写入键值对
     * @param key 键
     * @param value 值
     * @return 旧值,如果不存在旧值则为null。因为StrictMap不允许覆盖,则只能返回null
     */
    @Override
    @SuppressWarnings("unchecked")
    public V put(String key, V value) {
    
    
      if (containsKey(key)) {
    
    
        //如果已经存在此key了,直接报错
        throw new IllegalArgumentException(name + " already contains value for " + key
            + (conflictMessageProducer == null ? "" : conflictMessageProducer.apply(super.get(key), value)));
      }
      if (key.contains(".")) {
    
    
        // 例如key=“com.github.yeecode.clazzName”,则shortName = “clazzName”,即获取一个短名称
        final String shortKey = getShortName(key);
        if (super.get(shortKey) == null) {
    
    
          // 以短名称为键,放置一次
          super.put(shortKey, value);
        } else {
    
    
          // 放入该对象,表示短名称会引发歧义
          super.put(shortKey, (V) new Ambiguity(shortKey));
        }
      }
      // 以长名称为键,放置一次
      return super.put(key, value);
    }

3.plugin包

MyBatis还提供插件功能,允许其他开发者为 MyBatis开发插件以扩展 MyBatis的功能。与插件相关的类在 MyBatis的 plugin包中。阅读 plugin包的源码,学习如何开发 MyBatis插件,并通过源码分析 MyBatis实现插件插入与管理的机制。

3.1.责任链模式

在有些场景下,一个目标对象可能需要经过多个对象的处理。例如,我们要筹办一场校园晚会,需要针对演员进行如下的准备工作。

  • 给演员发送邮件,告知晚会的时间、地点,该工作由邮件发送员负责。
  • 根据演员性别为其准备衣服,该工作由物资管理员负责。
  • 如果演员未成年,则为其安排校车接送,该工作由对外联络员负责。

用代码展示这一过程:

// 不使用责任链模式
System.out.println("不使用责任链模式:");
// 创建三个工作人员实例
MailSender mailSender = new MailSender();
MaterialManager materialManager = new MaterialManager();
ContactOfficer contactOfficer = new ContactOfficer();
// 依次处理每个参与者
for (Performer performer : performerList) {
    
    
    System.out.println("process " + performer.getName() + ":");
    new MailSender().handle(performer);
    new MaterialManager().handle(performer);
    new ContactOfficer().handle(performer);
    System.out.println("---------");
}

而责任链模式将多个处理器组装成一个链条,被处理对象被放置到链条的起始端后,会自动在整个链条上传递和处理。这样被处理对象不需要和每个处理器打交道,也不需要了解整个链条的传递过程,于是便实现了被处理对象和单个处理器的解耦。

// 使用责任链模式
System.out.println("使用责任链模式:");
// 创建责任链
Handler handlerChain = new MailSender();
handlerChain.setNextHandler(new MaterialManager()).setNextHandler(new ContactOfficer());

// 依次处理每个参与者
for (Performer performer : performerList) {
    
    
    System.out.println("process " + performer.getName() + ":");
    handlerChain.triggerProcess(performer);
    System.out.println("---------");
}

这样,每个演员不需要和工作人员直接打交道,也不需要关心责任链上到底有多少个工作人员。责任链模式不仅降低了被处理对象和处理器之间的耦合度,还使得我们可以更为灵活地组建处理过程。例如,我们可以很方便地向责任链中增、删处理器或者调整处理器的顺序。

3.2.MyBatis插件开发

要想了解一个功能模块的源码,一种简单的办法是先学会使用这个模块。我们开发一个功能非常简单的 MyBatis插件,来了解 MyBatis插件的开发过程。我们要开发的插件的功能是:在 MyBatis查询列表形式的结果时,打印出结果的数目。
整个插件的源码非常简单

@Intercepts({
    
    
        @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {
    
    Statement.class})
})
public class YeecodeInterceptor implements Interceptor {
    
    
    private String info;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
    
    
        // 执行原有方法
        Object result = invocation.proceed();
        // 打印原方法输出结果的数目
        System.out.println(info + ":" + ((List) result).size());
        // 返回原有结果
        return result;
    }

    @Override
    public void setProperties(Properties properties) {
    
    
        // 为拦截器设置属性
        info = properties.get("preInfo").toString();
    }
}

MyBatis插件是一个实现了 Interceptor接口的类。Interceptor的含义是拦截器,因此我们所说的 MyBatis插件真正的叫法是MyBatis拦截器。由于 plugin包中还有一个叫 Plugin的类,为了避免混淆,在接下来的叙述中,我们用拦截器来指代我们编写的插件类。

拦截器类上有注解 Intercepts,Intercepts的参数是 Signature注解数组。每个 Signature注解都声明了当前拦截器类要拦截的方法。Signature注解中参数的含义如下。

  • type:拦截器要拦截的类型。YeecodeInterceptor 拦截器要拦截的类型是ResultSetHandler类型。
  • method:拦截器要拦截的 type类型中的方法。YeecodeInterceptor拦截器要拦截的是ResultSetHandler类型中的 handleResultSets方法。
  • args:拦截器要拦截的type类型中method方法的参数类型列表。在YeecodeInterceptor拦截器中,ResultSetHandler类型中的 handleResultSets方法只有一个 Statement类型的参数。

当要拦截多个方法时,只需在 Intercepts数组中放入多个Signature注解即可。
Interceptor接口中有三个方法供拦截器类实现,这三个方法的含义如下。

  • intercept:拦截器类必须实现该方法。拦截器拦截到目标方法时,会将操作转接到该 intercept 方法上,其中的参数Invocation 为拦截到的目标方法。在YeecodeInterceptor拦截器的 intercept方法中,会先执行原有的方法并获得原方法的输出结果,然后打印出原方法输出结果的数目,最后返回原有结果。这样YeecodeInterceptor拦截器便实现了打印结果数目的功能。
  • plugin:拦截器类可以选择实现该方法。该方法中可以输出一个对象来替换输入参数传入的目标对象。在YeecodeInterceptor拦截器中,我们没有实现该方法,而是直接使用了 Interceptor接口中的默认实现。在默认实现中,会调用 Plugin.wrap方法给出一个原有对象的包装对象,然后用该对象来替换原有对象。
  • setProperties:拦截器类可以选择实现该方法。该方法用来为拦截器设置属性。在YeecodeInterceptor拦截器中,我们使用该方法为拦截器设置 info属性的值。

拦截器配置结束后,还需要将拦截器设置到 MyBatis的配置中才能生效。

 <plugin interceptor="com.github.yeecode.mybatisdemo.plugin.YeecodeInterceptor">
     <property name="preInfo" value="本次查询记录数目"/>
 </plugin>

这样,我们已经完成了一个简单的 MyBatis拦截器的开发、配置和使用工作。MyBatis 拦截器的开发还是很容易上手的,大家可以在日常使用中根据实际需要为其开发一些拦截器以扩展 MyBatis的功能。

4.MyBatis拦截器平台

为了便于开发者为 MyBatis开发拦截器,MyBatis在 plugin包中搭建了一个拦截器平台。
在这里插入图片描述
整个类图中最为核心的类便是 Plugin 类,它继承了java.lang.reflect.InvocationHandler接口,因此是一个基于反射实现的代理类。

public class Plugin implements InvocationHandler {
    
    
  // 被代理对象
  private final Object target;
  // 拦截器
  private final Interceptor interceptor;
  // 拦截器要拦截的所有的类,以及类中的方法
  private final Map<Class<?>, Set<Method>> signatureMap;

Plugin类的 signatureMap属性存储的是当前拦截器要拦截的类和方法,该信息就是通过 getSignatureMap方法从拦截器的Intercepts注解和 Signature注解中获取的。

  // 得到该拦截器interceptor要拦截的类型与方法。Map<Class<?>, Set<Method>> 中键为类型,值为该类型内的方法集合
  /**
   * 获取拦截器要拦截的所有类和类中的方法
   * @param interceptor 拦截器
   * @return 入参拦截器要拦截的所有类和类中的方法
   */
  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());
    }
    // 将Intercepts注解的value信息取出来,是一个Signature数组
    Signature[] sigs = interceptsAnnotation.value();
    // 将Signature数组数组放入一个Map中。键为Signature注解的type类型,值为该类型下的方法集合
    Map<Class<?>, Set<Method>> signatureMap = new HashMap<>();
    for (Signature sig : sigs) {
    
    
      Set<Method> methods = signatureMap.computeIfAbsent(sig.type(), k -> new HashSet<>());
      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;
  }

有了拦截器要拦截的类型信息之后,Plugin 就可以判断出当前的类型是否需要被拦截器拦截。如果一个类需要被拦截,则Plugin 会为这个类创建一个代理类。这部分操作在wrap方法中完成:

  /**
   * 根据拦截器的配置来生成一个对象用来替换被代理对象
   * @param target 被代理对象
   * @param interceptor 拦截器
   * @return 用来替换被代理对象的对象
   */
  public static Object wrap(Object target, Interceptor interceptor) {
    
    
    // 得到拦截器interceptor要拦截的类型与方法
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    // 被代理对象的类型
    Class<?> type = target.getClass();
    // 逐级寻找被代理对象类型的父类,将父类中需要被拦截的全部找出
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    // 只要父类中有一个需要拦截,说明被代理对象是需要拦截的
    if (interfaces.length > 0) {
    
    
      // 创建并返回一个代理对象,是Plugin类的实例
      return Proxy.newProxyInstance(
          type.getClassLoader(),
          interfaces,
          new Plugin(target, interceptor, signatureMap));
    }
    // 直接返回原有被代理对象,这意味着被代理对象的方法不需要被拦截
    return target;
  }

因此,如果一个目标类需要被某个拦截器拦截的话,那么这个类的对象已经在 warp方法中被替换成了代理对象,即 Plugin对象。当目标类的方法被触发时,会直接进入 Plugin对象的invoke方法。在 invoke方法中,会进行方法层面的进一步判断:如果拦截器声明了要拦截此方法,则将此方法交给拦截器执行;如果拦截器未声明拦截此方法,则将此方法交给被代理对象完成。

  /**
   * 代理对象的拦截方法,当被代理对象中方法被触发时会进入这里
   * @param proxy 代理类
   * @param method 被触发的方法
   * @param args 被触发的方法的参数
   * @return 被触发的方法的返回结果
   * @throws Throwable
   */
  @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);
    }
  }

所以 Plugin类完成了类层级和方法层级这两个层级的过滤工作。

  • 如果目标对象所属的类被拦截器声明拦截,则 Plugin用自身实例作为代理对象替换目标对象。
  • 如果目标对象被调用的方法被拦截器声明拦截,则Plugin将该方法交给拦截器处理。否则 Plugin将该方法交给目标对象处理。

正因为 Plugin类完成了大量的工作,拦截器自身所需要做的工作就非常简单了,主要分为两项:使用 Intercepts 注解和Signature 注解声明自身要拦截的类型与方法;通过intercept方法处理被拦截的方法。

当然,拦截器也可以重写 Interceptor接口中的 plugin方法,来实现更为强大的功能。

重写 plugin方法后,可以在 plugin方法中给出一个其他的类来替换目标对象(而不调用 Plugin类的 warp方法)。这样可以完全脱离 Plugin类去完成一些更为自由的操作。这种情况下,如何替换目标对象以及替换之后的处理逻辑完全由插件开发者自己掌控。

5. MyBatis拦截器链与拦截点

通过上一节我们了解到拦截器的生效原理,那么 MyBatis支持配置多个拦截器吗?
答案是肯定的。我们可以在 MyBatis 的配置文件中配置多个插件,这些插件会在MyBatis 的初始化阶段被依次写到InterceptorChain 类的 interceptors 列表中。这一过程在XMLConfigBuilder类的 pluginElement方法中展开:

  /**
   * 解析<plugins>节点
   * @param parent <plugins>节点
   * @throws Exception
   */
  private void pluginElement(XNode parent) throws Exception {
    
    
    if (parent != null) {
    
     // <plugins>节点存在
      for (XNode child : parent.getChildren()) {
    
     // 依次<plugins>节点下的取出每个<plugin>节点
        // 读取拦截器类名
        String interceptor = child.getStringAttribute("interceptor");
        // 读取拦截器属性
        Properties properties = child.getChildrenAsProperties();
        // 实例化拦截器类
        Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
        // 设置拦截器的属性
        interceptorInstance.setProperties(properties);
        // 将当前拦截器加入到拦截器链中
        configuration.addInterceptor(interceptorInstance);
      }
    }
  }

这些拦截器在列表中组成了一个拦截器链。拦截器是通过替换目标对象实现的(通常基于 Plugin类,使用动态代理对象替换目标对象),那么MyBatis中任何对象都可以被替换吗?

答案是否定的。MyBatis 中一共只有四个类的对象可以被拦截器替换,它们分别是ParameterHandler、ResultSetHandler、StatementHandler 和 Executor。而且替换只能发生在固定的地方,我们称其为拦截点。以 ParameterHandler 对象为例

  /**
   * 创建参数处理器
   * @param mappedStatement SQL操作的信息
   * @param parameterObject 参数对象
   * @param boundSql SQL语句信息
   * @return 参数处理器
   */
  public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
    
    
    // 创建参数处理器
    ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
    // 将参数处理器交给拦截器链进行替换,以便拦截器链中的拦截器能注入行为
    parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
    // 返回最终的参数处理器
    return parameterHandler;
  }

在 ParameterHandler对象的拦截点,ParameterHandler对象被作为参数传递给拦截器链的 pluginAll 方法,以便拦截器链中的拦截器能够将行为注入 ParameterHandler 对象中。

    /**
     * 向所有的拦截器链提供目标对象,由拦截器链给出替换目标对象的对象
     * @param target 目标对象,是MyBatis中支持拦截的几个类(ParameterHandler、ResultSetHandler、StatementHandler、Executor)的实例
     * @return 用来替换目标对象的对象
     */
    public Object pluginAll(Object target) {
    
    
        // 依次交给每个拦截器完成目标对象的替换工作
        for (Interceptor interceptor : interceptors) {
    
    
            target = interceptor.plugin(target);
        }
        return target;
    }

在 InterceptorChain类的 pluginAll方法中,会将目标对象依次交给每个拦截器进行替换处理(通常是对目标对象进行进一步的包装以注入拦截器的功能),最后得到的目标对象target汇聚了拦截器链中的每一个拦截器的功能,这其实就是责任链模式。这样,在程序运行中,拦截器链中的各个拦截器会依次发挥自身的作用。

猜你喜欢

转载自blog.csdn.net/d495435207/article/details/131344960