MyBatis 中的配置文件主要是 mybatis-config.xml 配置文件和 Mapper.xml 映射配置文件。在 MyBatis 初始化的过程中,除了会读取这些配置文件,还会加载配置文件指定的类,处理类中的注解,创建一些配置对象,最终完成框架中各个模块的初始化。

BaseBuilder

MyBatis 的初始化的第一个步骤就是加载和解析 mybatis-config.xml 这个全局配置文件。入口是 SqlSessionFactoryBuilder 的 build() 方法,其具体实现如下:

  1. public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
  2. // 创建XMLConfigBuilder对象来读取mybatis-config.xml配置文件
  3. XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
  4. // 解析配置文件得到Configuration对象,创建DefaultSqlSessionFactory对象
  5. return new DefaultSqlSessionFactory(parser.parse());
  6. }

SqlSessionFactoryBuilder 的 build() 方法会创建 XMLConfigBuilder 对象来解析 mybatis-config.xml 配置文件的内容,而 XMLConfigBuilder 继承自 BaseBuilder 抽象类,BaseBuilder 的子类如下图所示:
image.png
BaseBuilder 抽象类中包含了一些核心字段。这些字段的初始化工作交由子类完成,BaseBuilder 只对外提供配置的获取。

  1. // MyBatis中几乎全部的配置信息都会保存到Configuration对象中。Configuration对象是在MyBatis初始化过程中创建且是全局唯一的
  2. protected final Configuration configuration;
  3. // 在mybatis-config.xml配置文件中可以使用<typeAliases>标签定义别名,这些定义的别名都会记录在该对象中
  4. protected final TypeAliasRegistry typeAliasRegistry;
  5. // 在mybatis-config.xml配置文件中可以使用<typeHandlers>标签添加自定义TypeHandler器,完成指定数据库类型与Java类型的转换,这些TypeHandler都会记录在该对象中
  6. protected final TypeHandlerRegistry typeHandlerRegistry;

BaseBuilder 中记录的 TypeAliasRegistry 对象和 TypeHandlerRegistry 对象,其实是全局唯一的,并且它们都是在 Configuration 对象初始化时创建的,代码如下:

  1. public BaseBuilder(Configuration configuration) {
  2. this.configuration = configuration;
  3. this.typeAliasRegistry = this.configuration.getTypeAliasRegistry();
  4. this.typeHandlerRegistry = this.configuration.getTypeHandlerRegistry();
  5. }

在 BaseBuilder 构造函数中,通过相应的 Configuration.get*() 方法得到 TypeAliasRegistry 对象和 TypeHandlerRegistry 对象,并赋值给 BaseBuilder 的相应字段。

1. XMLConfigBuilder

XMLConfigBuilder 是 BaseBuilder 的众多子类之一,主要负责解析 mybatis-config.xml 配置文件。其核心字段如下:

  1. // 记录当前 XMLConfigBuilder 对象是否已成功解析完 mybatis-config.xml 配置文件
  2. private boolean parsed;
  3. // 一个XML解析器,用来解析 mybatis-config.xml 配置文件
  4. private final XPathParser parser;
  5. // <environment>标签配置的名称
  6. private String environment;
  7. // 对 Reflector 对象的创建和缓存
  8. private final ReflectorFactory localReflectorFactory = new DefaultReflectorFactory();

XMLConfigBuilder 的 parse() 方法是解析 mybatis-config.xml 配置文件的入口,它通过调用内部的
parseConfiguration() 方法实现整个解析过程,具体实现如下:

  1. private void parseConfiguration(XNode root) {
  2. // 解析 <properties> 标签
  3. propertiesElement(root.evalNode("properties"));
  4. // 解析 <settings> 标签
  5. Properties settings = settingsAsProperties(root.evalNode("settings"));
  6. // 设置 vfsImpl字段
  7. loadCustomVfs(settings);
  8. // 处理日志相关组件
  9. loadCustomLogImpl(settings);
  10. // 解析 <typeAliases> 标签
  11. typeAliasesElement(root.evalNode("typeAliases"));
  12. // 解析 <plugins> 标签
  13. pluginElement(root.evalNode("plugins"));
  14. // 解析 <objectFactory> 标签
  15. objectFactoryElement(root.evalNode("objectFactory"));
  16. // 解析 <objectWrapperFactory> 标签
  17. objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
  18. // 解析 <reflectorFactory> 标签
  19. reflectorFactoryElement(root.evalNode("reflectorFactory"));
  20. // 将settings值设置到 Configuration 中
  21. settingsElement(settings);
  22. // 解析 <environments> 标签
  23. environmentsElement(root.evalNode("environments"));
  24. // 解析 <databaseIdProvider> 标签
  25. databaseIdProviderElement(root.evalNode("databaseIdProvider"));
  26. // 解析 <typeHandlers> 标签
  27. typeHandlerElement(root.evalNode("typeHandlers"));
  28. // 解析 <mappers> 标签
  29. mapperElement(root.evalNode("mappers"));
  30. }

除了 mybatis-config.xml 这个全局配置文件之外,MyBatis 初始化的时候还会加载 <mappers> 标签下定义的 Mapper 映射文件。 标签中会指定 Mapper.xml 映射文件的位置,通过解析 标签,MyBatis 就能够知道去哪里加载这些 Mapper.xml 文件了。

mapperElement() 方法会初始化 XMLMapperBuilder 对象来加载各个 Mapper.xml 映射文件。同时,还会扫描 Mapper 映射文件相应的 Mapper 接口,处理其中的注解并将 Mapper 接口注册到 MapperRegistry 中。该方法的具体实现如下:

  1. private void mapperElement(XNode parent) throws Exception {
  2. if (parent != null) {
  3. // 遍历每个子标签
  4. for (XNode child : parent.getChildren()) {
  5. // 如果指定了<package>子标签,则会扫描指定包路径,并向 MapperRegistry 注册 Mapper 接口
  6. if ("package".equals(child.getName())) {
  7. String mapperPackage = child.getStringAttribute("name");
  8. configuration.addMappers(mapperPackage);
  9. } else {
  10. // 解析<mapper>子标签,这里会获取resource、url、class三个属性,这三个属性互斥
  11. String resource = child.getStringAttribute("resource");
  12. String url = child.getStringAttribute("url");
  13. String mapperClass = child.getStringAttribute("class");
  14. // 如果<mapper>子标签指定了resource或url属性,都会创建XMLMapperBuilder对象来解析指定Mapper.xml配置文件
  15. if (resource != null && url == null && mapperClass == null) {
  16. ErrorContext.instance().resource(resource);
  17. InputStream inputStream = Resources.getResourceAsStream(resource);
  18. // 创建XMLMapperBuilder对象,解析映射配置文件
  19. XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
  20. mapperParser.parse();
  21. } else if (resource == null && url != null && mapperClass == null) {
  22. ErrorContext.instance().resource(url);
  23. InputStream inputStream = Resources.getUrlAsStream(url);
  24. XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
  25. mapperParser.parse();
  26. } else if (resource == null && url == null && mapperClass != null) {
  27. // 如果<mapper>子标签指定了class属性,则向MapperRegistry注册class属性指定的Mapper接口
  28. Class<?> mapperInterface = Resources.classForName(mapperClass);
  29. configuration.addMapper(mapperInterface);
  30. } else {
  31. throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
  32. }
  33. }
  34. }
  35. }
  36. }

2. XMLMapperBuilder

通过对 XMLConfigBuilder.mapperElement() 方法的介绍我们知道,XMLMapperBuilder 负责解析映射配置文件,它也继承了 BaseBuilder 抽象类,其中 parse() 方法是解析映射文件的入口,具体代码如下:

  1. public void parse() {
  2. // 判断是否已经加载过该映射文件
  3. if (!configuration.isResourceLoaded(resource)) {
  4. // 处理<mapper>节点
  5. configurationElement(parser.evalNode("/mapper"));
  6. // 将resource添加到Configuration的loadedResources集合中保存,它是Set<String>类型的集合,记录了已经加载过的映射文件
  7. configuration.addLoadedResource(resource);
  8. // 注册Mapper接口
  9. bindMapperForNamespace();
  10. }
  11. // 处理configurationElement()方法中解析失败的<resultMap>节点
  12. parsePendingResultMaps();
  13. // 处理configurationElement()方法中解析失败的<cache-ref>节点
  14. parsePendingCacheRefs();
  15. // 处理configurationElement()方法中解析失败的SQL语句节点
  16. parsePendingStatements();
  17. }

可以清晰地看到,configurationElement() 方法才是真正解析 Mapper.xml 映射文件的地方,其中定义了处理 Mapper.xml 映射文件的核心流程:

  1. private void configurationElement(XNode context) {
  2. try {
  3. // 获取<mapper>节点的namespace属性
  4. String namespace = context.getStringAttribute("namespace");
  5. if (namespace == null || namespace.equals("")) {
  6. throw new BuilderException("Mapper's namespace cannot be empty");
  7. }
  8. // 设置 MapperBuilderAssistant 的 currentNamespace 字段,记录当前命名空间
  9. builderAssistant.setCurrentNamespace(namespace);
  10. // 解析<cache-ref>节点
  11. cacheRefElement(context.evalNode("cache-ref"));
  12. // 解析<cache>节点
  13. cacheElement(context.evalNode("cache"));
  14. // 解析<parameterMap>节点(该节点已废弃)
  15. parameterMapElement(context.evalNodes("/mapper/parameterMap"));
  16. // 解析<resultMap>节点
  17. resultMapElements(context.evalNodes("/mapper/resultMap"));
  18. // 解析<sql>节点
  19. sqlElement(context.evalNodes("/mapper/sql"));
  20. // 解析<select>、<insert>、<update>、<delete>等SQL节点
  21. buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
  22. } catch (Exception e) {
  23. throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
  24. }
  25. }

2.1

MyBatis 拥有非常强大的二级缓存功能,该功能可以非常方便地进行配置,MyBatis 默认情况下没有开启二级缓存,如果要为某命名空间开启二级缓存功能,则需要在相应映射配置文件中添加 <cache> 节点,还可以通过配置 节点的相关属性,为二级缓存配置相应的特性(本质上就是添加相应的装饰器)。

XMLMapperBuilder 中解析 标签的核心逻辑位于 cacheElement() 方法中,具体步骤如下:

  • 获取 标签中的各项属性,比如 type、flushInterval、size 等属性;
  • 读取 标签下的子标签信息,这些信息将用于初始化二级缓存;
  • MapperBuilderAssistant 会根据上述配置信息,创建一个全新的 Cache 对象并添加到 Configuration 的 caches 集合中。

Configuration 中的 caches 集合是一个 StrictMap 类型的集合,其中的 Key 是 Cache 对象的唯一标识,默认值是 Mapper.xml 映射文件的 namespace,Value 才是真正的二级缓存对应的 Cache 对象。StrictMap 类是 MyBatis 定义的一个内部类,它继承自 HashMap,并进行了一些改动。

在创建 Cache 对象时,通过 CacheBuilder 对象完成创建,该对象中各个字段的含义如下:

  1. public class CacheBuilder {
  2. // Cache 对象的唯一标识,一般情况下对应映射文件中的配置 namespace
  3. private final String id;
  4. // Cache 接口的真正实现类,默认是 PerpetualCache
  5. private Class<? extends Cache> implementation;
  6. // 缓存装饰器集合,默认只包含 LruCache.class
  7. private final List<Class<? extends Cache>> decorators;
  8. // 缓存大小
  9. private Integer size;
  10. // 缓存清理时间周期
  11. private Long clearInterval;
  12. // 是否可读写
  13. private boolean readWrite;
  14. private Properties properties;
  15. // 是否阻塞
  16. private boolean blocking;
  17. ......
  18. }

2.2

通过上述介绍我们知道,可以通过 标签为每个 namespace 开启二级缓存,同时还会将 namespace 与关联的二级缓存 Cache 对象记录到 Configuration.caches 集合中,即二级缓存是 namespace 级别的。但在有的场景中,我们会需要在多个 namespace 共享同一个二级缓存,也就是共享同一个 Cache 对象。

为此,MyBatis 提供了 <cache-ref> 标签来引用另一个 namespace 的二级缓存。在 Configuration 中维护了一个 cacheRefMap 字段(HashMap 类型),其中 key 是 标签所属的namespace 标识,value 是 标签引用的 namespace 值,这样就可以将两个 namespace 关联起来了,即这两个 namespace 共用一个 Cache 对象。
image.png
这里会使用到一个叫 CacheRefResolver 的 Cache 引用解析器。CacheRefResolver 中记录了被引用缓存的 namespace 以及当前 namespace 关联的 MapperBuilderAssistant 对象。在解析 标签的时候,MapperBuilderAssistant 会通过 useCacheRef() 方法从 Configuration.caches 集合中,根据被引用的namespace 查找共享的 Cache 对象来初始化 currentCache,而不再创建新的Cache 对象,从而实现二级缓存的共享。

2.3

在 JDBC 编程中,为了将查询结果集中的数据映射成对象,我们需要自己写代码从结果集中获取数据,然后封装成对应的对象并设置对象之间的关系,而这些都是大量的重复性代码。为了减少这些重复的代码,MyBatis 使用 <resultMap> 节点定义了结果集与结果对象(Java Bean对象)之间的映射规则, 节点可以满足绝大部分的映射需求,从 而减少开发人员的重复性劳动,提高开发效率。

其中, 标签下的每一个子标签,如 等,都被解析一个 ResultMapping 对象,其中维护了数据库表中一个列与对应 Java 类中一个属性之间的映射关系,核心字段如下:

  1. public class ResultMapping {
  2. private Configuration configuration;
  3. // 对应节点的 property 属性,表示与该列进行映射的JavaBean对象的属性名
  4. private String property;
  5. // 对应节点的 column 属性,表示从数据库中得到的列名或是列名的别名
  6. private String column;
  7. // 对应节点的 javaType 属性,表示的是一个JavaBean的完全限定名,或一个类型别名
  8. private Class<?> javaType;
  9. // 对应节点的JdbcType属性,表示的是进行映射的列的JDBC类型
  10. private JdbcType jdbcType;
  11. // 对应节点的 typeHandler 属性,表示的是类型处理器,它会覆盖默认的类型处理器
  12. private TypeHandler<?> typeHandler;
  13. // 对应节点的 resultMap 属性,该属性通过id引用了另一个<resultMap>节点定义,它负责将结果集中的一部分列映射成其他关联的结果对象
  14. // 这样我们就可以通过 join 方式进行关联查询,然后直接映射成多个对象,并同时设置这些对象之间的组合关系
  15. private String nestedResultMapId;
  16. // 对应节点的 select 属性,该属性通过id引用了另一个<select>节点定义,它会把指定的列的值传入select属性指定的select语句中作为参数进行查询。
  17. // 注意,使用 select 属性可能会导致 N+1 问题
  18. private String nestedQueryId;
  19. private Set<String> notNullColumns;
  20. private String columnPrefix;
  21. private List<ResultFlag> flags;
  22. private List<ResultMapping> composites;
  23. private String resultSet;
  24. private String foreignColumn;
  25. // 是否延迟加载
  26. private boolean lazy;
  27. ......
  28. }

介绍完 ResultMapping 对象(即 标签下各个子标签的解析结果)后,我们再来看 标签如何被解析。整个 标签最终会被解析成 ResultMap 对象,它与 ResultMapping 之间的映射关系如下图所示:
image.png
其核心字段如下:

  1. public class ResultMap {
  2. private Configuration configuration;
  3. // <resultMap>节点的id属性
  4. private String id;
  5. // <resultMap>的type属性
  6. private Class<?> type;
  7. // 维护了整个<resultMap>标签解析之后得到的全部映射关系,也就是全部 ResultMapping 对象。
  8. private List<ResultMapping> resultMappings;
  9. // 记录了映射关系中带有ID标志的映射关系,例如<id>节点和<constructor>节点的<idArg>子节点
  10. private List<ResultMapping> idResultMappings;
  11. // 记录了映射关系中带有 Constructor 标志的映射关系,例如<constructor>所有子元素
  12. private List<ResultMapping> constructorResultMappings;
  13. // 记录了映射关系中不带有 Constructor 标志的映射关系
  14. private List<ResultMapping> propertyResultMappings;
  15. // 维护了所有映射关系中涉及的 column 属性值,也就是所有的列名(或别名)
  16. private Set<String> mappedColumns;
  17. private Set<String> mappedProperties;
  18. private Discriminator discriminator;
  19. // 当前 <resultMap> 标签是否嵌套了其他 <resultMap> 标签
  20. private boolean hasNestedResultMaps;
  21. // 当前 <resultMap> 标签是否含有嵌套查询。也就是说,这个映射关系中是否指定了 select 属性
  22. private boolean hasNestedQueries;
  23. // 当前 ResultMap 是否开启自动映射
  24. private Boolean autoMapping;
  25. }

2.4

在映射配置文件中,可以使用 <sql> 节点定义可重用的 SQL 语句片段。当需要重用 节点中定义的 SQL 语句片段时,只需要使用 节点引入相应的片段即可,这样在编写 SQL 语句以及维护这些 SQL 语句时都会比较方便。

3. XMLStatementBuilder

除了上面介绍的节点,在映射配置文件中还有一类比较重要的节点需要解析,就是 SQL 节点。这些 SQL 节点主要用于定义 SQL 语句,它们不再由 XMLMapperBuilder 负责解析,而是由 XMLStatementBuilder 解析。

下面,我们来分析 XMLStatementBuilder 解析 SQL 标签的入口方法——parseStatementNode() 方法,在该方法中首先会根据 id 属性和 databaseId 属性决定加载匹配的 SQL 标签,然后解析其中的 标签和 标签,相关的代码片段如下:

  1. public void parseStatementNode() {
  2. // 获取SQL标签的id以及databaseId属性
  3. String id = context.getStringAttribute("id");
  4. String databaseId = context.getStringAttribute("databaseId");
  5. // 若databaseId属性值与当前使用的数据库不匹配,则不加载该SQL标签
  6. // 若存在相同id且databaseId不为空的SQL标签,则不再加载该SQL标签
  7. if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
  8. return;
  9. }
  10. // 根据SQL标签的名称决定其SqlCommandType
  11. String nodeName = context.getNode().getNodeName();
  12. SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
  13. // 获取SQL标签的属性值,例如,fetchSize、timeout、parameterType、parameterMap、resultMap、resultType、lang、resultSetType、flushCache、useCache等。
  14. ...
  15. // 在解析SQL语句之前,先处理其中的<include>标签
  16. XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
  17. includeParser.applyIncludes(context.getNode());
  18. // 获取SQL标签的parameterType、lang两个属性
  19. ... ...
  20. // 解析<selectKey>标签
  21. processSelectKeyNodes(id, parameterTypeClass, langDriver);
  22. ...
  23. }

3.1

在实际应用中,我们会在 标签中定义一些能够被重用的 SQL 片段,在 XMLMapperBuilder.sqlElement() 方法中会根据当前使用的 DatabaseId 匹配 标签,只有匹配的 SQL 片段才会被加载到内存。

在解析 SQL 标签之前,MyBatis 会先将 <include> 标签转换成对应的 SQL 片段(即定义在 标签内的文本),这个转换过程是在 XMLIncludeTransformer.applyIncludes() 方法中实现的,其中不仅包含了 标签的处理,还包含了 ${} 占位符的处理。

3.2

在有的数据库表设计场景中,我们会添加一个自增 ID 字段作为主键。有时,我们希望在执行 insert 语句的时候返回这个自增 ID 值,<selectKey> 标签就可以实现自增 ID 的获取。 标签不仅可以获取自增 ID,还可以指定其他 SQL 语句,从其他表或执行数据库的函数获取字段值。

XMLStatementBuilder 的 parseSelectKeyNode() 方法用来解析 标签,其会解析 标签的各个属性,并根据这些属性值将其中的 SQL 语句解析成 MappedStatement 对象,具体实现如下:

  1. private void parseSelectKeyNode(String id, XNode nodeToHandle, Class<?> parameterTypeClass, LanguageDriver langDriver, String databaseId) {
  2. // 解析<selectKey>标签的resultType、statementType、keyProperty等属性
  3. ...
  4. // 通过LanguageDriver解析<selectKey>标签中的SQL语句,得到对应的SqlSource对象
  5. SqlSource sqlSource = langDriver.createSqlSource(configuration, nodeToHandle, parameterTypeClass);
  6. // <selectKey>节点中只能配置select语句
  7. SqlCommandType sqlCommandType = SqlCommandType.SELECT;
  8. // 创建MappedStatement对象
  9. builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
  10. fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
  11. resultSetTypeEnum, flushCache, useCache, resultOrdered,
  12. keyGenerator, keyProperty, keyColumn, databaseId, langDriver, null);
  13. id = builderAssistant.applyCurrentNamespace(id, false);
  14. // 创建<selectKey>标签对应的KeyGenerator对象,这个KeyGenerator对象会添加到Configuration.keyGenerators集合中
  15. MappedStatement keyStatement = configuration.getMappedStatement(id, false);
  16. configuration.addKeyGenerator(id, new SelectKeyGenerator(keyStatement, executeBefore));
  17. ...
  18. }

3.3 解析 SQL 节点

经过 标签和 标签的处理流程之后,XMLStatementBuilder 中的 parseStatementNode()方法接下来就要开始处理 SQL 语句了,具体代码逻辑如下:

  1. public void parseStatementNode() {
  2. // 前面是解析<selectKey>和<include>标签的逻辑,这里不再展示
  3. ...
  4. // 当执行到这里时,<selectKey>和<include>标签已经被解析完毕,并删除掉了
  5. // 下面是解析SQL语句的逻辑,也是parseStatementNode()方法的核心
  6. // 通过LanguageDriver.createSqlSource()方法创建SqlSource对象
  7. SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
  8. // 获取SQL标签中配置的resultSets、keyProperty、keyColumn等属性,以及前面解析<selectKey>标签得到的KeyGenerator对象等
  9. ...
  10. // 根据上述属性信息创建MappedStatement对象,并添加到Configuration.mappedStatements集合中保存
  11. builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
  12. fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
  13. resultSetTypeEnum, flushCache, useCache, resultOrdered,
  14. keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
  15. ...
  16. }

这里解析 SQL 语句使用的是 LanguageDriver 接口,其核心实现是 XMLLanguageDriver。我们也可以提供自定义的 LanguageDriver 实现,并在 mybatis-config.xml 中通过 defaultScriptingLanguage 配置指定使用该自定义实现。在 LanguageDriver.createSqlSource() 实现中,XMLLanguageDriver 会依赖 XMLScriptBuilder 来创建 SqlSource 对象,XMLScriptBuilder 首先会判断 SQL 语句是否为动态 SQL,判断依据是该 SQL 语句中是否含有未解析的 “${}” 占位符。

3.4 SqlSource

映射配置文件中定义的 SQL 节点会被解析成 MappedStatement 对象,其中的 SQL 语句会被解析成 SqISource 对象。MyBatis 使用 SqISource 接口表示映射文件或注解中定义的 SQL 语句,但它表示的 SQL 语句是不能直接被数据库执行的,因为其中可能含有动态 SQL 语句相关的节点或占位符等需要解析的元素。

  1. public interface SqlSource {
  2. // 根据映射文件或注解描述的SQL语句,以及传入的参数,返回可执行的SQL语句
  3. BoundSql getBoundSql(Object parameterObject);
  4. }

其实现类如下所示:
image.png
其中,DynamicSqlSource 负责处理动态 SQL 语句,RawSqlSource 负责处理静态语句,两者最终都会将处理后的 SQL 语句封装成 StaticSqlSource 返回。DynamicSqlSource 与 StaticSqlSource 的主要区别是:

  • StaticSqlSource 中记录的 SQL 语句中可能含有 “?” 占位符,但可以直接提交给数据库执行
  • DynamicSqlSource 中记录的 SQL 语句还需进行一系列解析才会最终得到数据库可执行的 SQL 语句

在 SQL 语句中定义的动态 SQL 节点、文本节点等,则由 SqlNode 接口的相应实现表示:

  1. public interface SqlNode {
  2. // 该方法会根据用户传入的实参,参数解析该SqlNode所记录的动态SQL节点
  3. // 并调用DynamicContext.appendSql()方法将解析后的SQL片段追加到DynamicContext.sqlBuilder中保存
  4. // 当SQL节点下的所有SqlNode完成解析后,就可以从DynamicContext中获取一条动态生成的、完整的SQL语句
  5. boolean apply(DynamicContext context);
  6. }

SqlNode 接口有多个实现类,每个实现类对应一个动态 SQL 节点,来解析指定类型的 SQL 节点,如 标签由 IfSqlNode 实现类来进行解析。
image.png
在经过 SqlNode.apply() 方法的解析后,SQL 语句会被传递到 SqlSourceBuilder 中进行进一步的解析。该类主要完成了两方面的操作,一方面是解析 SQL 语句中的 “#{}” 占位符中定义的属性,另一方面是将 SQL 语句中的 “#{}” 占位符替换成 “?” 占位符。SqlSourceBuilder 也是 BaseBuilder 的子类,其 parse() 方法如下:

  1. /**
  2. * @param originalSql 经过SqlNode.apply()方法处理过后的sql语句
  3. * @param parameterType 用户传入的实参类型
  4. * @param additionalParameters 记录了形参与实参的对应关系,其实就是经过SqlNode.apply()处理后的DynamicContext.bindings集合
  5. */
  6. public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
  7. // 该对象用于解析 "#{}" 占位符中的参数属性及替换占位符
  8. ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
  9. // 该对象用于配合解析
  10. GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
  11. String sql = parser.parse(originalSql);
  12. // 该对象封装了占位符被替换成 "?" 的sql语句及参数对应的ParameterMapping集合
  13. return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
  14. }

4. 绑定 Mapper 接口

每个映射配置文件的命名空间可以绑定一个 Mapper 接口,并注册到 MapperRegistry 中。在 MyBatis Mapper 文件映射那一篇我们已经讲过是如何进行映射的了。下面我们看是如何在初始化时进行绑定的。

具体实现逻辑在 XMLMapperBuilder.bindMapperForNamespace() 方法中,其完成了映射配置文件与对应 Mapper 接口的绑定。

  1. private void bindMapperForNamespace() {
  2. String namespace = builderAssistant.getCurrentNamespace();
  3. if (namespace != null) {
  4. Class<?> boundType = null;
  5. try {
  6. boundType = Resources.classForName(namespace);
  7. } catch (ClassNotFoundException e) {
  8. //ignore, bound type is not required
  9. }
  10. if (boundType != null) {
  11. if (!configuration.hasMapper(boundType)) {
  12. configuration.addLoadedResource("namespace:" + namespace);
  13. configuration.addMapper(boundType);
  14. }
  15. }
  16. }
  17. }

在 addMapper() 方法中,还会创建 MapperAnnotationBuilder 并调用 parse() 方法解析 Mapper 接口中的注解信息,如 @Select@SelectKey@ResultMap 等注解。