MyBatis 源码及 MyBatis-Spring 集成

发表于 2023-03-19

MyBatis 源码

MyBatis 的源码相对于 Spring 来说很简单。通过各种 Builder 构造器解析配置文件,并完成 SqlSessionFactory 的创建。

整体的 Spring 流程就是:

  • 解析 XML 配置文件,创建 DataSource、TransactionFactory 相关的环境配置。
  • 根据相关的插件配置生成插件的调用链。
  • 解析 Mapper 文件并完成相关的 SQL 解析。
  • 如果有使用二级缓存,每个 Mapper 会创建相关的 Cache 调用链。
  • 使用 JDK 动态代理,为 Mapper 接口创建动态代理。
  • 通过 Mapper 接口(动态代理)调用方法,进入 MapperProxy 去动态生成 SQL(由 Executor 完成执行)。
  • 相应的如果有缓存处理缓存,有插件会通过动态代理完成插件的调用。

配置文件解析构造器

XMLConfigBuilder

XMLConfigBuilder 用于构建 Configuration 对象。包含了 MyBatis 需要的所有的信息,并会根据该 Configuration 对象创建 SqlSessionFactory 工厂,用于创建 Session、Mapper 对象。

在 XMLConfigBuilder 中会解析到 mybatis-config.xml 文件中的配置项,例如:environmentsmapperssettingsplugins 等等配置。

XMLConfigBuilder 对象调用 parse 方法,进入 parseConfiguration 方法,开始逐步解析 XML 配置文件并完成相关配置的解析创建 Configuration 对象。

/**
 * 根据 XML 文件中的配置信息,逐步解析创建 Configuration 对象
 */
private void parseConfiguration(XNode root) {
  try {
    // issue #117 read properties first
    propertiesElement(root.evalNode("properties"));
    Properties settings = settingsAsProperties(root.evalNode("settings"));
    loadCustomVfs(settings);
    loadCustomLogImpl(settings);
    typeAliasesElement(root.evalNode("typeAliases"));
    // 在这里解析插件
    pluginElement(root.evalNode("plugins"));
    objectFactoryElement(root.evalNode("objectFactory"));
    objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
    reflectorFactoryElement(root.evalNode("reflectorFactory"));
    settingsElement(settings);
    // 在这里根据配置的信息创建:TransactionFactory、DataSourceFactory、DataSource
    environmentsElement(root.evalNode("environments"));
    databaseIdProviderElement(root.evalNode("databaseIdProvider"));
    typeHandlerElement(root.evalNode("typeHandlers"));
    // 在这里向下深入,将进行 Mapper 文件的扫描解析、创建
    mapperElement(root.evalNode("mappers"));
  } catch (Exception e) {
    throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
  }
}

XMLMapperBuilder

XMLMapperBuilder 用于分析 Mapper,他的作用主要是通过解析 Mybatis 的 Mapper 文件来完成 Mapper 对象的创建和初始化工作。

Mapper 的配置一般是有 package、resource、url、class 这四种。

在 XMLMapperBuilder 中会解析到 Mapper 的 xml 文件中的所有标签/节点。我们常用的有:cacheresultMap、SQL 相关的标签(selectupdate)。

/**
 * 解析 Mapper xml 文件
 */
private void configurationElement(XNode context) {
  try {
    // 指定 namespace
    String namespace = context.getStringAttribute("namespace");
    if (namespace == null || namespace.isEmpty()) {
      throw new BuilderException("Mapper's namespace cannot be empty");
    }
    builderAssistant.setCurrentNamespace(namespace);
    // 缓存标签
    cacheRefElement(context.evalNode("cache-ref"));
    cacheElement(context.evalNode("cache"));
    parameterMapElement(context.evalNodes("/mapper/parameterMap"));
    // resultMap 标签
    resultMapElements(context.evalNodes("/mapper/resultMap"));
    // select 标签
    sqlElement(context.evalNodes("/mapper/sql"));
    // SQL 相关的标签(select、insert、update、delete),在这里向下深入,将解析生成 MappedStatement
    buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
  } catch (Exception e) {
    throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
  }
}

XMLStatementBuilder

XMLStatementBuilder 将解析 SQL 相关的标签(select、insert、update、delete)生成 MappedStatement

获取 XMLLanguageDriver 并由 XMLScriptBuilder 完成 SQL 标签的解析,最后创建成 MappedStatement 记录到 configuration 中。

/**
 * 解析 SQL 相关的标签(select、insert、update、delete)
 */
public void parseStatementNode() {
  ....
  // 获取 XMLLanguageDriver,由 XMLScriptBuilder 解析成 SqlSource
  String lang = context.getStringAttribute("lang");
  LanguageDriver langDriver = getLanguageDriver(lang);
  ....
  SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
  ....
  // 创建 MappedStatement
  builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap,
      parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, resultOrdered,
      keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets, dirtySelect);
}

XMLScriptBuilder(XMLLanguageDriver)

XMLScriptBuilder 用于解析 SQL 标签( whereif 等)生成 SqlSource

/**
 * 解析 SQL 标签( `where`、`if` 等)生成 SqlSource
 */
public SqlSource parseScriptNode() {
  MixedSqlNode rootSqlNode = parseDynamicTags(context);
  SqlSource sqlSource;
  if (isDynamic) {
    sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
  } else {
    sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
  }
  return sqlSource;
}

缓存

在解析 Mapper 文件时,根据是否存在 cache 标签创建对应的二级缓存。

private void configurationElement(XNode context) {
  ....
  // 解析创建二级缓存的入口
  cacheElement(context.evalNode("cache"));
  ....
}

二级缓存采用的装饰者模式,不同的装饰者承担了不同的责任,例如:日志、同步、序列化。

最核心的缓存实现是:org.apache.ibatis.cache.impl.PerpetualCache

生成的缓存对象的委托结构如下:

- org.apache.ibatis.cache.decorators.SynchronizedCache         (缓存的同步)
	- org.apache.ibatis.cache.decorators.LoggingCache          (缓存的日志输出)
		- org.apache.ibatis.cache.decorators.SerializedCache   (缓存序列化)
			- org.apache.ibatis.cache.decorators.LruCache      (缓存的失效策略)
				- org.apache.ibatis.cache.impl.PerpetualCache  (缓存数据存储)

在执行就会从 SynchronizedCache --> LoggingCache 的方向逐步调用,经过装饰者的处理,完成不同的功能。

插件

插件会在 XMLConfigBuilder 解析最外层配置文件时生成对应的插件拦截器,并添加到 interceptorChain 执行链中。

在调用的 MyBatis 的四大对象时(ExecutorStatementHandlerParameterHandlerResultSetHandler)会到 interceptorChain 执行链中匹配合适的插件(匹配规则:指定的四大对象,然后根据方法名称和签名完成匹配)。

举个例子来理解插件如何生效的。

Executor 在 DefaultSqlSessionFactory#openSessionFromDataSource 开启 Session 时完成创建 Executor 并使用动态代理织入插件。

/**
 * 创建 Executor
 */
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
  executorType = executorType == null ? defaultExecutorType : executorType;
  Executor executor;
  if (ExecutorType.BATCH == executorType) {
    executor = new BatchExecutor(this, transaction);
  } else if (ExecutorType.REUSE == executorType) {
    executor = new ReuseExecutor(this, transaction);
  } else {
    executor = new SimpleExecutor(this, transaction);
  }
  if (cacheEnabled) {
    executor = new CachingExecutor(executor);
  }
  // 插件将通过动态代理应用到 Executor 对象上:Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap));
  return (Executor) interceptorChain.pluginAll(executor);
}

在 Executor 执行时创建会创建 StatementHandler (默认 StatementType.PREPARED 创建 PreparedStatementHandler)。

StatementHandler 对象还需要同时创建两个对象:parameterHandlerresultSetHandler

四大对象时(ExecutorStatementHandlerParameterHandlerResultSetHandler)都会通过 configuration 完成插件的代理。

// ParameterHandler 插件代理的创建
return (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
// ResultSetHandler 插件代理的创建
return (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
// StatementHandler 插件代理的创建
return (StatementHandler) interceptorChain.pluginAll(statementHandler);
// Executor 插件代理的创建
return (Executor) interceptorChain.pluginAll(executor);

Spring 集成

Spring 和 MyBatis 的集成依赖于项目:https://github.com/mybatis/spring

MyBatis 通过自定义 Class 扫描器,来完成 Mapper 接口的扫描,并修改扫描出来的 BeanDefinition

将 Bean 定义中的 Class 改为 MapperFactoryBean,通过 FactoryBean 的 getObject() 调用sqlSession 创建 Mapper 代理对象。

MyBatis 关键类说明

  • @MapperScan
    • 指定 Mapper 接口所在的包
    • 使用 @Import(MapperScannerRegistrar.class) 导入 MapperScannerRegistrar
  • MapperScannerRegistrar
    • 注册 MapperScannerConfigurer
  • MapperScannerConfigurer
    • Bean 定义注册后置处理器(BeanDefinitionRegistryPostProcessor
    • 通过创建自定义的 ClassPathMapperScanner Class 扫描器,完成 Mapper 接口的扫描。
  • ClassPathMapperScanner
    • Class 扫描器
    • 通过调用 ClassPathBeanDefinitionScanner#doScan 并重写 isCandidateComponentcheckCandidate 实现扫描 Mapper 接口成 Bean 定义。
    • 在方法 processBeanDefinitions 将扫描出来的接口 Bean 定义中的 Mapper.class 改为 MapperFactoryBean.class,并将接口的 Class 通过参数传递给 MapperFactoryBean。
    • 通过修改 Bean 定义的 Class,让 Bean 的创建交给 MapperFactoryBean#getObject
  • MapperFactoryBean
    • 工厂 Bean,调用 getSqlSession().getMapper(this.mapperInterface) 完成实际的 Mapper 代理创建。

修改了 Mapper 接口 Bean 定义的 Class,让 Bean 定义转变成了一个工厂 Bean。

所以该 Bean 定义转变成 Bean 的实例化过程就从普通的 Spring 实例化,变成了 MapperFactoryBean#getObject 创建。

通过工厂 Bean 实例化 Mapper,就可以由 MapperFactoryBean 自定义实例化 Mapper 接口(动态代理)。

Spring MyBatis 项目配置

依赖

dependencies {
    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'

    implementation("mysql:mysql-connector-java:8.0.32")
    implementation group: 'org.mybatis', name: 'mybatis-spring', version: '2.1.0'
    implementation("org.mybatis:mybatis:3.5.13")
    implementation(project(":spring-context"))
    implementation(project(":spring-jdbc"))
    implementation(project(":spring-aop"))
    implementation(project(":spring-jdbc"))
    implementation("com.alibaba:druid:1.1.8")
}

配置类

@Configuration
//扫描指定包中的mapper文件
@MapperScan("com.devnolo.demo.spring.mybatis.mapper")
@ComponentScan("com.devnolo.demo.spring.mybatis")
//提供事务支持
@EnableTransactionManagement
public class ApplicationConfiguration {
	@Bean
	public DataSource dataSource(){
		DruidDataSource dataSource = new DruidDataSource();
		dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
		dataSource.setUrl("jdbc:mysql://192.168.0.0:3306/test?characterEncoding=utf8");
		dataSource.setUsername("user");
		dataSource.setPassword("password");
		dataSource.setMaxActive(100);
		dataSource.setMinIdle(20);
		dataSource.setMaxWait(1000);
		return dataSource;
	}

	@Bean
	public SqlSessionFactoryBean sqlSessionFactory() throws Exception {
		SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
		factory.setDataSource(dataSource());
		// 设置 SQL 映射文件路径
		factory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mybatis/mapper/*.xml"));
		return factory;
	}

	@Bean
	public TransactionManager transactionManager(){
		DataSourceTransactionManager trans =  new DataSourceTransactionManager();
		//设置数据源
		trans.setDataSource(dataSource());
		return trans;
	}
}

启动类

public class DemoMyBatisApplication {
	public static void main(String[] args) {
		AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(ApplicationConfiguration.class);
		TestMapper bean = applicationContext.getBean(TestMapper.class);
		Test test = bean.selectOne(1L);
		System.out.println(Optional.ofNullable(test).map(Test::getId).orElse(null));
	}
}

Mapper

Mapper 接口

public interface TestMapper {
    Test selectOne(Long id);
}

实体

public class Test implements Serializable {
	private static final long serialVersionUID = 1L;
    private Long id;
    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
}

Mapper XML

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.devnolo.demo.spring.mybatis.mapper.TestMapper">
    <cache/>
    <select id="selectOne" resultType="com.devnolo.demo.spring.mybatis.model.Test">
        select * from test
        <where>
            <if test="id > 0">
                and id = #{id}
            </if>
        </where>
    </select>
</mapper>