抛开 Spring ,你知道 MyBatis 加载 Mapper 的底层原理吗?

原文链接: 抛开 Spring ,你知道 MyBatis 加载 Mapper 的底层原理吗?

大家都知道,利用 Spring 整合 MyBatis,我们可以直接利用 @MapperScan 注解或者 @Mapper 注解,让 Spring 可以扫描全部的 Mapper 接口,解析然后加载。那么如果抛开 Spring,你们可知道 MyBatis 是如何解析和加载 Mapper 接口的?

如果不知道的话,可以跟着我这篇文章,一步一步地深入和解读源码,带你从底层来看通 MyBatis 解析加载 Mapper 的实现原理。

文章可是很长的,你能全部看完么?啊哈哈哈~

当然了,如果大家本来就对 MyBatis 挺熟悉的,可以根据自己的情况挑选着目录来看!

一、MyBatis 核心组件:

在解读源码之前,我们很有必要先了解 MyBatis 几大核心组件,知道他们都是做什么用的。

核心组件有:Configuration、SqlSession、Executor、StatementHandler、ParameterHandler、ResultSethandler。

下面简单介绍一下他们:

  • Configuration:用于描述 MyBatis 主配置文件信息,MyBatis 框架在启动时会加载主配置文件,将配置信息转换为 Configuration 对象。

  • SqlSession:面向用户的 API,是 MyBatis 与数据库交互的接口。

  • Executor:SQL 执行器,用于和数据库交互。SqlSession 可以理解为 Executor 组件的外观(外观模式),真正执行 SQL 的是 Executor 组件。

  • MappedStatement:用于描述 SQL 配置信息,MyBatis 框架启动时,XML 文件或者注解配置的 SQL 信息会被转换为 MappedStatement 对象注册到 Configuration 组件中。

  • StatementHandler:封装了对 JDBC 中 Statement 对象的操作,包括为 Statement 参数占位符设置值,通过 Statement 对象执行 SQL 语句。

  • TypeHandler:类型处理器,用于 Java 类型与 JDBC 类型之间的转换。

  • ParameterHandler:用于处理 SQL 中的参数占位符,为参数占位符设置值。

  • ResultSetHandler:封装了对 ResultSet 对象的处理逻辑,将结果集转换为 Java 实体对象。

二、简述 Mapper 执行流程:

SqlSession组件,它是用户层面的API。用户可利用 SqlSession 获取想要的 Mapper 对象(MapperProxy 代理对象);当执行 Mapper 的方法,MapperProxy 会创建对应的 MapperMetohd,然后 MapperMethod 底层其实是利用 SqlSession 来执行 SQL。

但是真正执行 SQL 操作的应该是 Executor组 件,Executor 可以理解为 SQL 执行器,它会使用 StatementHandler 组件对 JDBC 的 Statement 对象进行操作。当 Statement 类型为 CallableStatement 和 PreparedStatement 时,会通过 ParameterHandler 组件为参数占位符赋值。

ParameterHandler 组件中会根据 Java 类型找到对应的 TypeHandler 对象,TypeHandler 中会通过 Statement 对象提供的 setXXX() 方法(例如setString()方法)为 Statement 对象中的参数占位符设置值。

StatementHandler 组件使用 JDBC 中的 Statement 对象与数据库完成交互后,当 SQL 语句类型为 SELECT 时,MyBatis 通过 ResultSetHandler 组件从 Statement 对象中获取 ResultSet 对象,然后将 ResultSet 对象转换为 Java 对象。

我们可以用一幅图来描述上面各个核心组件之间的关系:

三、简单例子深入讲解底层原理

下面我将带着一个非常简单的 Mapper 使用例子来讲解底层的流程和原理。

例子很简单,首先是获取 MyBatis 的主配置文件的文件输入流,然后创建 SqlSessinoFactory,接着利用 SqlSessionFactory 创建 SqlSessin;然后利用 SqlSession 获取要使用的 Mapper 代理对象,最后执行 Mapper 的方法获取结果。

1、代码例子:

@Test
public  void testMybatis () throws IOException {
    // 获取配置文件输入流
    InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
    // 通过SqlSessionFactoryBuilder的build()方法创建SqlSessionFactory实例
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    // 调用openSession()方法创建SqlSession实例
    SqlSession sqlSession = sqlSessionFactory.openSession();
    // 获取UserMapper代理对象
    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    // 执行Mapper方法,获取执行结果
    List userList = userMapper.listAllUser();
    System.out.println(JSON.toJSONString(userList));
}

第一行代码,非常的明显,就是读取 MyBatis 的主配置文件。正常来说,这个主配置文件应该用来创建 Configuration ,但是这里却是传给 SqlSessionFactoryBuilder 来创建 SqlSessionFactory,然后就利用工厂模式来创建 SqlSession 了;上面我们也提及到, SqlSession 是提供给用户友好的数据库操作接口,那么岂不是说不需要 Configuratin 也可以直接获取 Mapper 然后操作数据库了?

那当然不是了,Configuration 是 MyBatis 的主配置类,它里面会包含 MyBatis 的所有信息(不管是主配置信息,还是所有 Mapper 配置信息),所以肯定是需要创建的。

所以其实在创建 SqlSessionFactory 时就已经初始化 Configuration 了,因为 SqlSession 需要利用 Executor、ParameterHandler 和 ResultSetHandler 等等各大组件互相配合来执行 Mapper,而 Configuration 就是这些组件的工厂类。

我们可以在 SqlSessionFactoryBuilder#build() 方法中看到 Configuration 是如何被初始化的:

public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
    try {
      XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
      return build(parser.parse());
    //......
  }

从上面的代码能看到,就是根据主配置文件的文件输入流创建 XMLConfigBuilder 对象,然后利用 XMLConfigBuilder#parse() 方法来创建 Configuration 对象。

当然了,虽然只是简单的调用了 XMLConfigBuilder#parse() 方法,可是里面包含的东西是非常的多的。例如: MyBatis 主配置文件的解析;如何根据 标签给每个 Mapper 接口生产 MapperProxy 代理类和将 SQL 配置转换为 MappedStatement;以及 、、、 等等标签是如何解析的。

当然了,这篇文章我们只会着重于关于 Mapper 配置的解析和加载,根据底层源码一步一步的去分析弄明白,至于其他的知识点就不过多讲解了。

2、XMLConfigBuilder 中关于 Configuration 的解析过程

Ⅰ. XMLConfigBuilder#parseConfiguration()

上面讲到 XMLConfigBuilder 会调用 parse() 方法去解析 MyBatis 的主配置文件,底层主要是利用 XPATH 来解析 XML 文件。代码如下:

public class XMLConfigBuilder extends BaseBuilder {

    // .....
    
    private final XPathParser parser;

    // .....

    public Configuration parse() {
        // 防止parse()方法被同一个实例多次调用
        if (parsed) {
          throw new BuilderException("Each XMLConfigBuilder can only be used once.");
        }
        parsed = true;
        // 调用XPathParser.evalNode()方法,创建表示configuration节点的XNode对象。
        // 调用parseConfiguration()方法对XNode进行处理
        parseConfiguration(parser.evalNode("/configuration"));
        return configuration;
    }
    
    private void parseConfiguration(XNode root) {
        try {
            propertiesElement(root.evalNode("properties"));
            Properties settings = settingsAsProperties(root.evalNode("settings"));
            loadCustomVfs(settings);
            typeAliasesElement(root.evalNode("typeAliases"));
            pluginElement(root.evalNode("plugins"));
            objectFactoryElement(root.evalNode("objectFactory"));
            objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
            reflectorFactoryElement(root.evalNode("reflectorFactory"));
            settingsElement(settings);
            environmentsElement(root.evalNode("environments"));
            databaseIdProviderElement(root.evalNode("databaseIdProvider"));
            typeHandlerElement(root.evalNode("typeHandlers"));
            // 最重要的关注点
            mapperElement(root.evalNode("mappers"));
        } catch (Exception e) {
          throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
        }
    }
    // ....
}

在 XMLConfigBuilder#parseConfiguration() 方法里面,会对主配置文件里的所有标签进行解析;当然了,由于我们这篇文章的主题是分析 Mapper 的解析和加载过程,所以接下来将直接关注 parseConfiguration() 方法里面的 mapperElement() 方法,其他部分大家可以直接去阅读 MyBatis 的源码。

备注:MyBatis 里面的所有 xxxBuilder 类都是继承与 BaseBuilder,而 BaseBuilder 要注意的点就是它持有着 Configuration 实例的引用。

Ⅱ . XMLConfigBuilder#mapperElement()

在 XMLConfigBuilder#mapperElement() 方法里面,主要是解析 标签里面的 标签和 标签,这两个标签主要是描述 Mapper 接口的全路径、Mapper 接口所在的包的全路径以及 Mapper 接口对应的 XML 文件的全路径。

代码如下:

private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        // 通过标签指定包名
        if ("package".equals(child.getName())) {
          String mapperPackage = child.getStringAttribute("name");
          configuration.addMappers(mapperPackage);
        } else {
          String resource = child.getStringAttribute("resource");
          String url = child.getStringAttribute("url");
          String mapperClass = child.getStringAttribute("class");
          // 通过resource属性指定XML文件路径
          if (resource != null && url == null && mapperClass == null) {
            ErrorContext.instance().resource(resource);
            InputStream inputStream = Resources.getResourceAsStream(resource);
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
            mapperParser.parse();
          } else if (resource == null && url != null && mapperClass == null) {
            // 通过url属性指定XML文件路径
            ErrorContext.instance().resource(url);
            InputStream inputStream = Resources.getUrlAsStream(url);
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
            mapperParser.parse();
          } else if (resource == null && url == null && mapperClass != null) {
            // 通过class属性指定接口的完全限定名
            Class mapperInterface = Resources.classForName(mapperClass);
            configuration.addMapper(mapperInterface);
          } else {
            throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
          }
        }
      }
    }
  }

从上面的代码来看,主要是将标签分为 和 来解析,而再细一点可以分为两种解析情况:一种是指定了Mapper接口的 XML 文件,而另外一种是指定了 Mapper 接口。

那么我们可以先看看指定 XML 文件是如何解析与加载 Mapper 的。

3、XMLMapperBuilder 中关于 Mapper 的解析过程

Mapper 接口的 XML 文件的解析当然也是利用 XPath,但此时不再是 XMLConfigBuilder 来负责了,而是需要创建一个 XMLMapperBuilder 对象,而 XMLMapperBuilder 需要传入 XML 文件的文件输入流。

Ⅰ . XMLMapperBuilder#parse()

我们可以看看 XMLMapperBuilder#parse() 方法,XML 文件的解析流程就是在这里面:

public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
      // 调用XPathParser的evalNode()方法获取根节点对应的XNode对象
      configurationElement(parser.evalNode("/mapper"));
      // 將资源路径添加到Configuration对象中
      configuration.addLoadedResource(resource);
      bindMapperForNamespace();
    }
    // 继续解析之前解析出现异常的ResultMap对象
    parsePendingResultMaps();
    // 继续解析之前解析出现异常的CacheRef对象
    parsePendingCacheRefs();
    // 继续解析之前解析出现异常标签配置
    parsePendingStatements();
}

解析前,会先判断 Configuratin 是否已经加载这个 XML 资源,如果不存在,则调用 configurationElement() 方法;在方法里面会解析所有的 、、、、 和 标签。

Ⅱ . XMLMapperBuilder#configuratinElement()

下面我们先看一下 XMLMapperBuilder#configuratinElement() 方法的代码:

private void configurationElement(XNode context) {
    try {
      // 获取命名空间
      String namespace = context.getStringAttribute("namespace");
      if (namespace == null || namespace.equals("")) {
        throw new BuilderException("Mapper's namespace cannot be empty");
      }
      // 设置当前正在解析的Mapper配置的命名空间
      builderAssistant.setCurrentNamespace(namespace);
      // 解析标签
      cacheRefElement(context.evalNode("cache-ref"));
      // 解析标签
      cacheElement(context.evalNode("cache"));
      // 解析所有的标签
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      // 解析所有的标签
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      // 解析所有的标签
      sqlElement(context.evalNodes("/mapper/sql"));
      // 解析所有的标签
      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);
    }
}

当然了,我们此时将所有注意力集中在 buildStatementFromContext 方法即可。

Ⅲ . XMLMapperBuilder#buildStatementFromContext()

在这个方法里面,会调用重载的 buildStatementFromContext 方法;但是这里还不是真正解析的地方,而是遍历所有标签,然后创建一个 XMLStatementBuilder 对象,对标签进行解析。代码如下:

private void buildStatementFromContext(List list, String requiredDatabaseId) {
    for (XNode context : list) {
      // 通过XMLStatementBuilder对象,对标签进行解析
      final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
      try {
        // 调用parseStatementNode()方法解析
        statementParser.parseStatementNode();
      } catch (IncompleteElementException e) {
        configuration.addIncompleteStatement(statementParser);
      }
    }
  }

4、XMLStatementBuilder 中关于 的解析过程

Ⅰ. XMLStatementBuilder#parseStatementNode()

那么我们接着看看 XMLStatementBuilder#parseStatementNode 方法:

public void parseStatementNode() {
    String id = context.getStringAttribute("id");
    String databaseId = context.getStringAttribute("databaseId");

    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
      return;
    }
    // 解析标签属性
    Integer fetchSize = context.getIntAttribute("fetchSize");
    Integer timeout = context.getIntAttribute("timeout");
    String parameterMap = context.getStringAttribute("parameterMap");
    String parameterType = context.getStringAttribute("parameterType");
    Class parameterTypeClass = resolveClass(parameterType);
    String resultMap = context.getStringAttribute("resultMap");
    String resultType = context.getStringAttribute("resultType");
    // 获取LanguageDriver对象
    String lang = context.getStringAttribute("lang");
    LanguageDriver langDriver = getLanguageDriver(lang);
    // 获取Mapper返回结果类型Class对象
    Class resultTypeClass = resolveClass(resultType);
    String resultSetType = context.getStringAttribute("resultSetType");
    // 默认Statement类型为PREPARED
    StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType",
            StatementType.PREPARED.toString()));
    ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);

    String nodeName = context.getNode().getNodeName();
    SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
    boolean useCache = context.getBooleanAttribute("useCache", isSelect);
    boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

    // 將标签内容,替换为标签定义的SQL片段
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    includeParser.applyIncludes(context.getNode());

    // 解析标签
    processSelectKeyNodes(id, parameterTypeClass, langDriver);
    
    // 通过LanguageDriver解析SQL内容,生成SqlSource对象
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    String resultSets = context.getStringAttribute("resultSets");
    String keyProperty = context.getStringAttribute("keyProperty");
    String keyColumn = context.getStringAttribute("keyColumn");
    KeyGenerator keyGenerator;
    String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
    keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
    // 获取主键生成策略
    if (configuration.hasKeyGenerator(keyStatementId)) {
      keyGenerator = configuration.getKeyGenerator(keyStatementId);
    } else {
      keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
          configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
          ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
    }

    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered, 
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
  }

从上面的代码可得,首先会解析标签里的所有属性;然后创建 LanguageDriver 来解析标签里面的 SQL 配置,并生成对应的 SqlSource 对象;最后,利用工具类 MapperBuilderAssistant 来将上面解析的内容组装成 MappedStatement 对象,并且注册到 Configuration 中。

5、详细介绍 SqlSource 与 LanguageDriver 接口

上面我们说到,解析的 SQL 内容会生成对应的 SqlSource 对象,那么我们先看看 SqlSource 接口,代码如下:

public interface SqlSource {

  BoundSql getBoundSql(Object parameterObject);

}

SqlSource 接口的定义非常简单,只有一个 getBoundSql() 方法,该方法返回一个 BoundSql 实例。

所以说 BoundSql 才是对 SQL 语句及参数信息的封装,它是 SqlSource 解析后的结果,BoundSql 的代码如下:

public class BoundSql {

  // Mapper配置解析后的sql语句
  private final String sql;
  // Mapper参数映射信息
  private final List parameterMappings;
  // Mapper参数对象
  private final Object parameterObject;
  // 额外参数信息,包括标签绑定的参数,内置参数
  private final Map additionalParameters;
  // 参数对象对应的MetaObject对象
  private final MetaObject metaParameters;
  // ... 省略 get/set 和 构造函数
}

因为 SQL 的解析是利用 LanguageDriver 组件完成的,所以我们再接着看看 LanguageDriver 接口,代码如下:

public interface LanguageDriver {

  ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql);

  SqlSource createSqlSource(Configuration configuration, XNode script, Class parameterType);

  SqlSource createSqlSource(Configuration configuration, String script, Class parameterType);

}

如上面的代码所示,LanguageDriver 接口中一共有3个方法,其中 createParameterHandler() 方法用于创建 ParameterHandler 对象,另外还有两个重载的 createSqlSource() 方法,这两个重载的方法用于创建 SqlSource 对象。

MyBatis 中为 LanguageDriver 接口提供了两个实现类,分别为 XMLLanguageDriver 和 RawLanguageDriver。

  • XMLLanguageDriver 为 XML 语言驱动,实现了动态 SQL 的功能,也就是说可以利用 MyBatis 提供的 XML 标签(常用的

    等标签)结合OGNL表达式语法来实现动态的条件判断。

  • RawLanguageDriver 表示仅支持静态 SQL 配置,不支持动态 SQL 功能。

接下来我们重点了解一下 XMLLanguageDriver 实现类的内容,代码如下:

public class XMLLanguageDriver implements LanguageDriver {

  @Override
  public ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
    return new DefaultParameterHandler(mappedStatement, parameterObject, boundSql);
  }
  @Override
  public SqlSource createSqlSource(Configuration configuration, XNode script, Class parameterType) {
    // 该方法用于解析XML文件中配置的SQL信息
    // 创建XMLScriptBuilder对象
    XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
    // 调用 XMLScriptBuilder对象parseScriptNode()方法解析SQL资源
    return builder.parseScriptNode();
  }

  @Override
  public SqlSource createSqlSource(Configuration configuration, String script, Class parameterType) {
    // 该方法用于解析Java注解中配置的SQL信息
    // 字符串以标签开头,则以XML方式解析
    if (script.startsWith("")) {
      XPathParser parser = new XPathParser(script, false, configuration.getVariables(), new XMLMapperEntityResolver());
      return createSqlSource(configuration, parser.evalNode("/script"), parameterType);
    } else {
      // 解析SQL配置中的全局变量
      script = PropertyParser.parse(script, configuration.getVariables());
      TextSqlNode textSqlNode = new TextSqlNode(script);
      // 如果SQL中是否仍包含${}参数占位符,则返回DynamicSqlSource实例,否则返回RawSqlSource
      if (textSqlNode.isDynamic()) {
        return new DynamicSqlSource(configuration, textSqlNode);
      } else {
        return new RawSqlSource(configuration, script, parameterType);
      }
    }
  }

}

如上面的代码所示,XMLLanguageDriver 类实现了 LanguageDriver 接口中两个重载的 createSqlSource() 方法,分别用于处理 XML 文件和 Java 注解中配置的 SQL 信息,将 SQL 配置转换为 SqlSource 对象。

第一个重载的 createSqlSource() 方法用于处理 XML 文件中配置的 SQL 信息,该方法中创建了一个 XMLScriptBuilder 对象,然后调用 XMLScriptBuilder 对象的 parseScriptNode() 方法将 SQL 资源转换为 SqlSource 对象。

第二个重载的 createSqlSource() 方法用于处理 Java 注解中配置的 SQL 信息,该方法中首先判断 SQL 配置是否以 标签开头。如果是,则以 XML 方式处理 Java 注解中配置的 SQL 信息;否则只是简单处理,替换 SQL 中的全局参数即可。如果 SQL 中仍然包含 ${} 参数占位符,则 SQL 语句仍然需要根据传递的参数动态生成,所以使用 DynamicSqlSource 对象描述 SQL 资源,否则说明 SQL 语句不需要根据参数动态生成,使用 RawSqlSource 对象描述 SQL 资源。

从 XMLLanguageDriver 类的 createSqlSource() 方法的实现来看,我们除了可以通过 XML 配置文件结合 OGNL 表达式配置动态 SQL 外,还可以通过 Java 注解的方式配置,只需要注解中的内容加上 标签。

当然了,此时我们只需先关注第一个重载的 createSqlSource() 方法即可。

我们可以看到方法中,会先创建 XMLScriptBuilder 对象,接着调用 XMLScriptBuilder 对象 parseScriptNode() 方法解析SQL资源。

这一层套一层的,真深啊,不过这分层确实还是非常的棒的,每个类的职责很专一,使得代码看起来很舒服,而且也大大地提高了代码的可扩展性。

6、XMLScriptBuilder 中关于 SQL 资源的解析过程

Ⅰ . XMLScriptBuilder#parseScriptNode()

那么接下来,我们可以继续看看 XMLScriptBuilder 的 parseScriptNode 方法是如何解析的,代码如下:

public SqlSource parseScriptNode() {
    // 调用parseDynamicTags()方法將SQL配置转换为SqlNode对象
    MixedSqlNode rootSqlNode = parseDynamicTags(context);
    SqlSource sqlSource = null;
    // 判断Mapper SQL配置中是否包含动态SQL元素,如果是创建DynamicSqlSource对象,否则创建RawSqlSource对象
    if (isDynamic) {
      sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {
      sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    return sqlSource;
  }

从上面的代码来看,会先调用 parseDynamicTags() 方法,将 SQL 配置转换为 SqlNode 对象;然后根据变量 isDynamic 判断 Mapper SQL 配置中是否包含动态 SQL 元素,如果是创建 DynamicSqlSource 对象,否则创建 RawSqlSource 对象返回给 XMLStatementBuilder 的 parseStatementNode 方法。

Ⅱ. XMLScriptBuilder#parseDynamicTags()

那么我们先看看 XMLScriptBuilder#parseDynamicTags() 方法吧,代码如下:

protected MixedSqlNode parseDynamicTags(XNode node) {
    List contents = new ArrayList();
    NodeList children = node.getNode().getChildNodes();
    // 对XML子元素进行遍历
    for (int i = 0; i < children.getLength(); i++) {
      XNode child = node.newXNode(children.item(i));
      // 如果子元素为SQL文本内容,则使用TextSqlNode描述该节点
      if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
        String data = child.getStringBody("");
        TextSqlNode textSqlNode = new TextSqlNode(data);
        // 判断SQL文本中包含${}参数占位符,则为动态SQL
        if (textSqlNode.isDynamic()) {
          contents.add(textSqlNode);
          isDynamic = true;
        } else {
          // 如果SQL文本中不包含${}参数占位符,则不是动态SQL
          contents.add(new StaticTextSqlNode(data));
        }
      } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) {
        // 如果子元素为、等标签,则使用对应的NodeHandler处理
        String nodeName = child.getNode().getNodeName();
        NodeHandler handler = nodeHandlerMap.get(nodeName);
        if (handler == null) {
          throw new BuilderException("Unknown element  in SQL statement.");
        }
        handler.handleNode(child, contents);
        isDynamic = true;
      }
    }
    return new MixedSqlNode(contents);
  }

上面的代码的注解已经写得非常的明白了,就是遍历标签里面的子元素,如果是 SQL 文本内容,判断是否包含 ${} 参数占位符,如果包含则创建 TextSqlNode 对象添加到 contents 列表里,当然了,还需要将 isDynamic 设置为 true 来表示这是动态 SQL;否则创建 StaticTextSqlNode 对象。

isDynamic 就是用来判断创建 DynamicSqlSource 对象还是 RawSqlSource 对象;接着如果是为 、 等标签,则使用对应的 NodeHandler 处理。

大家可能都好奇什么是 NodeHandler,其实我们可以看到 XMLScriptBuilder 里面的 nodeHandlerMap 属性就会记录着全部的 NodeHandler,其中 key 是标签名,value 就是对应的 NodeHandler了;并且在创建 XMLScriptBuilder 时,会调用 initNodeHandlerMap 方法来初始化 nodeHandlerMap 属性。

public class XMLScriptBuilder extends BaseBuilder {

  private final XNode context;
  private boolean isDynamic;
  private final Class parameterType;
  private final Map nodeHandlerMap = new HashMap();

   // .....
  private void initNodeHandlerMap() {
    nodeHandlerMap.put("trim", new TrimHandler());
    nodeHandlerMap.put("where", new WhereHandler());
    nodeHandlerMap.put("set", new SetHandler());
    nodeHandlerMap.put("foreach", new ForEachHandler());
    nodeHandlerMap.put("if", new IfHandler());
    nodeHandlerMap.put("choose", new ChooseHandler());
    nodeHandlerMap.put("when", new IfHandler());
    nodeHandlerMap.put("otherwise", new OtherwiseHandler());
    nodeHandlerMap.put("bind", new BindHandler());
  }
  // .....
}

从上面代码我们可以看到,每种动态 SQL 的标签都有对应的 NodeHandler,这些 NodeHandler 会将标签转换为对应的 SqlNode。例如 标签会转换为 IfSqlNode, 标签会转换为 ChooseSqlNode。

最后会用 MixedSqlNode 整理 SQL 转换的所有 SqlNode。

我们可以看看 MixedSqlNode 的代码:

public class MixedSqlNode implements SqlNode {
  private final List contents;

  public MixedSqlNode(List contents) {
    this.contents = contents;
  }

  @Override
  public boolean apply(DynamicContext context) {
    for (SqlNode sqlNode : contents) {
      sqlNode.apply(context);
    }
    return true;
  }
}

MixedSqlNode 的作用其实是非常的简单,就是用 contents 属性来保存 SQL 配置的所有子元素对应的 SqlNode 列表,也就是 XMLScriptBuilder#parseDynamicTags() 方法中的局部列表变量 contents,里面包含着 SQL 配置解析后的一个或多个 SqlNode 对象。

上面我们也讲到:当将 SQL 配置 都转换为 SqlNode 后,还会根据 isDynamic 来判断创建 DynamicSqlSource 还是 RawSqlSource 对象。

Ⅲ . DynamicSqlSource 和 RawSqlSource

创建 DynamicSqlSource 非常简单,将 Configuration 和 MixedSqlNode 封装起来即可,因为当执行 Mappper 接口的方法时,会根据入参来调用 DynamicSqlSource 的 getBoundSql 方法来解析动态 SQL。

而 RawSqlSource 的创建会稍微麻烦一点,因为他还需要将 #{} 参数占位符转换为 ? ,并保存参数映射关系。

备注:DynamicSqlSource 也会处理 #{} 参数占位符,只不过是在执行 getBoundSql() 方法时才会进行处理。

7、SqlSourceBuilder 协助创建 RawSqlSource

首先在 RawSqlSource 的构造函数里面,会创建 SqlSourceBuilder 对象,接着会调用 SqlSourceBuilder#parse() 方法,在 parse() 方法里面会会创建 GenericTokenParser 和 ParameterMappingTokenHandler。

  • GenericTokenParse 是 Token解析器,用于解析#{}参数。

  • ParameterMappingTokenHandler为Mybatis参数映射处理器,用于处理SQL中的#{}参数占位符,将 #{} 参数占位符转为 ? 。并且 ParameterMappingTokenHandler 的 parameterMappings 属性保存着参数的映射关系。

代码如下所示:

public class SqlSourceBuilder extends BaseBuilder {

    private static final String parameterProperties = "javaType,jdbcType,mode,numericScale,resultMap,typeHandler,jdbcTypeName";
    
    public SqlSourceBuilder(Configuration configuration) {
        super(configuration);
    }
    public SqlSource parse(String originalSql, Class parameterType, Map additionalParameters) {
        // ParameterMappingTokenHandler为Mybatis参数映射处理器,用于处理SQL中的#{}参数占位符
        ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
        // Token解析器,用于解析#{}参数
        GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
        // 调用GenericTokenParser对象的parse()方法將#{}参数占位符转换为?
        String sql = parser.parse(originalSql);
        return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
    }
  ....
}

好了,到这一步,已经将 SQL 配置解析成对应的 SqlSource 对象,其实就等于解析完成了,而动态 SQL ( 等标签)只能等待执行 Mapper 时,根据入参来继续解析了。

8、将 SQL 配置解析后的信息组装成 MappedStatement

解析完 SQL 配置后,我们需要回到 XMLStatementBuilder#parseStatementNode() 方法中,需要研究的就是最后的

MapperBuilderAssistant#addMappedStatement() 方法。

这个方法会根据上面 SQL 配置解析后的所有信息,封装成对应的 MappedStatement 对象,然后注册到 Configuration 的 mappedStatements 属性中。mappedStatements 为 Map 结构,其中 Key 为 Mapper Id,Value 为 MappedStatement 对象。

此时 Mapper 接口对应的 XML 文件里面的所有配置都已经解析完成了,但是我们可以发现,还没有为 Mapper 接口创建对应的代理类。

9、将 Mapper 接口注册到 Configuration 中

我们此时可以重新回到 XMLMapperBuilder#parse() 方法中。接着里面的 configurationElement() 方法继续往下走。

configuration.addLoadedResource(resource) 就不用讲了,就是将 XML 文件的路径添加到 Configuration 的 loadedResources 属性中,借此判断 XML 文件是否已经被解析过了。

Ⅰ . XMLMapperBuilder#bindMapperForNameSpace()

接着就是下一个重点了,就是 XMLMapperBuilder#bindMapperForNamespace() 方法。

我们先直接看代码:

private void bindMapperForNamespace() {
    String namespace = builderAssistant.getCurrentNamespace();
    if (namespace != null) {
      Class boundType = null;
      try {
        boundType = Resources.classForName(namespace);
      } catch (ClassNotFoundException e) {
        //ignore, bound type is not required
      }
      if (boundType != null) {
        if (!configuration.hasMapper(boundType)) {
         
          configuration.addLoadedResource("namespace:" + namespace);
          configuration.addMapper(boundType);
        }
      }
    }
  }

我们可以看到,先调用 Configuration#hasMapper() 方法来判断 Mapper 接口是否注册过了,如果没注册过就调用 Configuration#addMapper() 方法来注册 Mapper 接口,所以说,生成动态代理类的重点在这个方法里面。

Ⅱ . Configuration#addMapper()

Configuration#addMapper() 方法是调用属性 MapperRegistery#addMapper() 方法,代码如下:

public  void addMapper(Class type) {
    if (type.isInterface()) {
      if (hasMapper(type)) {
        throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
      }
      boolean loadCompleted = false;
      try {
        knownMappers.put(type, new MapperProxyFactory(type));
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        parser.parse();
        loadCompleted = true;
      } finally {
        if (!loadCompleted) {
          knownMappers.remove(type);
        }
      }
    }
  }

我们可以看到方法里面会调用 Configuration 属性 knownMappers 的 put 方法,将 key 为 Mapper 接口对应 Class 对象,value 为 Mapper 接口的 代理工厂类 MapperProxyFactory。

备注:当执行 Mapper 时,MapperProxyFactory会根据 SqlSession 为 Mapper 接口创建一个 MapperProxy 代理实例。

我们还可以看到,下面会根据当前 Configuration 配置和 Mapper 接口的 Class 对象创建一个 MapperAnnotationBuilder 对象,然后调用 MapperAnnotationBuilder#parse() 方法。

这里我就先不给大家详解了,因为后面会讲解到,但是可以告诉大家,这里主要就是为了解析 Mapper 接口那些使用 SQL 注解的方法,例如 @Select 系列注解和 @SelectProvider 系列注解。解析后也是会生成对应的 MappedStatement 注册到 Configuration 的 mappedStatements 属性中。

至此,关于指定 XML 文件解析和加载 Mapper 接口的整个流程已经完毕。在这里我还是简单的给大家总结一下流程吧。

  1. 根据 XML 文件的输入流创建 XMLMapperBuilder 对象,调用 parse() 方法作为解析 标签的入口。

  2. XMLMapperBuilder#configurationElement() 方法解析 、、、、 和 等所有标签,而 标签的解析入口为 XMLMapperBuilder#buildStatementFromContext() 方法。

  3. XMLMapperBuilder#buildStatementFromContext() 方法中会遍历所有 标签,然后创建对应的 XMLStatementBuilder 对象,进行标签解析。

  4. XMLStatementBuilder#parseStatementNode() 方法里面首先会解析 标签里的所有属性,然后利用 LanguageDriver 来解析 SQL 配置,将 SQL 的所有片段(包括静态SQL和动态SQL)解析成对应的 SqlNode 对象,然后使用 MixedSqlNode 来保存起来,最后根据是否为动态 SQL 来创建 DynamicSqlSource 对象或者 RawSqlSource 对象。

  5. 当 标签解析完后,会利用工具类 MapperBuilderAssistant 的 addMappedStatement() 方法来将解析的所有信息封装为对应的 MappedStatement 对象,然后注册到 Configuration 中。

  6. XML 文件解析完后,XMLMapperBuilder#parse() 方法会调用 Configuration#addLoadedResource() 方法将 XML 文件的资源路径注册到Configuration 中。

  7. 最后,在 XMLMapperBuilder#parse() 方法中还会调用 XMLMapperBuilder#bindMapperForNamespace() 方法,将 Mapper 接口注册到 Configuration 中。接口注册底层是使用 MapperRegistry 类,这个类会保存着所有 Mapper 接口的注册信息。在注册时,会为 Mapper 接口创建对应的 MapperFactoryBean;当执行 Mapper 时,可以根据当前 SqlSession 创建 Mapper 接口对应的 MapperProxy 代理实例。

  8. XMLMapperBuilder#bindMapperForNamespace() 方法在 Mapper 接口注册后,还会创建 MapperAnnotationBuilder 对象来解析 Mapper 接口带 SQL 注解方法,也是生成对应的 MappedStatement 然后注册到 Configuration 中。

接着看看指定 Mapper 接口是如何解析与加载 Mapper 的

10、MapperRegistry 将 Mapper 接口注册到 Configuration 中

我们再回顾一下 XMLConfigBuilder#mapperElement() 方法:

private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        // 通过标签指定包名
        if ("package".equals(child.getName())) {
          String mapperPackage = child.getStringAttribute("name");
          configuration.addMappers(mapperPackage);
        } else {
          // .... 省略
          } else if (resource == null && url == null && mapperClass != null) {
            // 通过class属性指定接口的完全限定名
            Class mapperInterface = Resources.classForName(mapperClass);
            configuration.addMapper(mapperInterface);
          } else {
            throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
          }
        }
      }
    }
  }

从上面代码可以看到,指定 Mapper 接口来解析与加载,都只是简单地调用了 Configuration#addMapper() 方法。而上面我们也已经讲解到了,Mapper 接口的注册底层是利用 MapperRegistry 。

但是此时我们会有一个疑问:Mapper 接口的 XML 文件不用解析吗?

所以到这里,我们需要继续讲解的就是上面省略掉的 MapperAnnotationBuilder。其实它不但是可以解析 Mapper 接口使用 SQL 注解的方法,还会尝试加载 Mapper 接口对应的 XML 文件,如果不为空,则会使用 XMLMapperBuilder 来解析 XML 文件。当然了,XMLMapperBuilder 解析 XMl 文件的流程和上面介绍的是一致的。

Ⅰ . MapperAnnotationBuilder#parse()

下面先看看 MapperAnnotationBuilder#parse() 方法,代码如下:

public void parse() {
    String resource = type.toString();
    if (!configuration.isResourceLoaded(resource)) {
      // 尝试加载和解析 XML 文件
      loadXmlResource();
      configuration.addLoadedResource(resource);
      assistant.setCurrentNamespace(type.getName());
      parseCache();
      parseCacheRef();
      Method[] methods = type.getMethods();
      for (Method method : methods) {
        try {
          // issue #237
          if (!method.isBridge()) {
            // 处理 SQL 注解
            parseStatement(method);
          }
        } catch (IncompleteElementException e) {
          configuration.addIncompleteMethod(new MethodResolver(this, method));
        }
      }
    }
    parsePendingMethods();
  }

MapperAnnotationBuilder#parse() 方法首先会调用 loadXmlResource() 方法来尝试加载并解析 Mapper 接口的 XMl 文件。在 loadXmlResource() 方法中,会根据 Mapper 接口的 name 来拼接 XML 的文件的名字,然后尝试获取文件输入流;如果文件输入流不为空,则表示 Mapper 接口有对应的 XML 文件,此时会创建一个 XMLMapperBuilder 对象,然后对 XML 文件进行解析。

到这里,我们就可以把 XML 文件也解析到了。

Ⅱ . MapperAnnotationBuilder#parseStatement()

当然了,Mapper 接口的方法可以直接使用像 @Select 这种 SQL 注解,所以 MapperAnnotationBuilder 也会尝试加载并解析方法上的注解。

在 MapperAnnotationBuilder#parse() 方法中,会遍历 Mapper 接口的所有方法(Method),然后调用 MapperAnnotationBuilder#parseStatement() 方法来解析。

代码如下:

for (Method method : methods) {
    try {
      // issue #237
      if (!method.isBridge()) {
        parseStatement(method);
      }
    } catch (IncompleteElementException e) {
      configuration.addIncompleteMethod(new MethodResolver(this, method));
    }
}

  void parseStatement(Method method) {
    Class parameterTypeClass = getParameterType(method);
    LanguageDriver languageDriver = getLanguageDriver(method);
    SqlSource sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver);
    if (sqlSource != null) {
      Options options = method.getAnnotation(Options.class);
      final String mappedStatementId = type.getName() + "." + method.getName();
      Integer fetchSize = null;
      Integer timeout = null;
      StatementType statementType = StatementType.PREPARED;
      ResultSetType resultSetType = ResultSetType.FORWARD_ONLY;
      SqlCommandType sqlCommandType = getSqlCommandType(method);
      boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
      boolean flushCache = !isSelect;
      boolean useCache = isSelect;

      KeyGenerator keyGenerator;
      String keyProperty = null;
      String keyColumn = null;
      if (SqlCommandType.INSERT.equals(sqlCommandType) || SqlCommandType.UPDATE.equals(sqlCommandType)) {
        // first check for SelectKey annotation - that overrides everything else
        SelectKey selectKey = method.getAnnotation(SelectKey.class);
        if (selectKey != null) {
          keyGenerator = handleSelectKeyAnnotation(selectKey, mappedStatementId, getParameterType(method), languageDriver);
          keyProperty = selectKey.keyProperty();
        } else if (options == null) {
          keyGenerator = configuration.isUseGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
        } else {
          keyGenerator = options.useGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
          keyProperty = options.keyProperty();
          keyColumn = options.keyColumn();
        }
      } else {
        keyGenerator = NoKeyGenerator.INSTANCE;
      }

      if (options != null) {
        if (FlushCachePolicy.TRUE.equals(options.flushCache())) {
          flushCache = true;
        } else if (FlushCachePolicy.FALSE.equals(options.flushCache())) {
          flushCache = false;
        }
        useCache = options.useCache();
        fetchSize = options.fetchSize() > -1 || options.fetchSize() == Integer.MIN_VALUE ? options.fetchSize() : null; //issue #348
        timeout = options.timeout() > -1 ? options.timeout() : null;
        statementType = options.statementType();
        resultSetType = options.resultSetType();
      }

      String resultMapId = null;
      ResultMap resultMapAnnotation = method.getAnnotation(ResultMap.class);
      if (resultMapAnnotation != null) {
        String[] resultMaps = resultMapAnnotation.value();
        StringBuilder sb = new StringBuilder();
        for (String resultMap : resultMaps) {
          if (sb.length() > 0) {
            sb.append(",");
          }
          sb.append(resultMap);
        }
        resultMapId = sb.toString();
      } else if (isSelect) {
        resultMapId = parseResultMap(method);
      }

      assistant.addMappedStatement(
          mappedStatementId,
          sqlSource,
          statementType,
          sqlCommandType,
          fetchSize,
          timeout,
          // ParameterMapID
          null,
          parameterTypeClass,
          resultMapId,
          getReturnType(method),
          resultSetType,
          flushCache,
          useCache,
          // TODO gcode issue #577
          false,
          keyGenerator,
          keyProperty,
          keyColumn,
          // DatabaseID
          null,
          languageDriver,
          // ResultSets
          options != null ? nullOrEmpty(options.resultSets()) : null);
    }
  }

在 MapperAnnotationBuilder#parseStatement() 方法中会调用 getSqlSourceFromAnnotations() 方法,而方法中会分别调用 getSqlAnnotationType() 和 getSqlProviderAnnotationType() 方法来判断 Method 是否带有 @Select 或 @SelectProvider 等系列注解。

如果有的话,会利用 LanguageDriver 来解析 SQL 注解,也就是利用 XMLLanguageDriver 的第二个 createSqlSource() 重载方法,代码如下:

@Override
  public SqlSource createSqlSource(Configuration configuration, String script, Class parameterType) {
    // 该方法用于解析Java注解中配置的SQL信息
    // 字符串以标签开头,则以XML方式解析
    if (script.startsWith("")) {
      XPathParser parser = new XPathParser(script, false, configuration.getVariables(), new XMLMapperEntityResolver());
      return createSqlSource(configuration, parser.evalNode("/script"), parameterType);
    } else {
      // 解析SQL配置中的全局变量
      script = PropertyParser.parse(script, configuration.getVariables());
      TextSqlNode textSqlNode = new TextSqlNode(script);
      // 如果SQL中是否仍包含${}参数占位符,则返回DynamicSqlSource实例,否则返回RawSqlSource
      if (textSqlNode.isDynamic()) {
        return new DynamicSqlSource(configuration, textSqlNode);
      } else {
        return new RawSqlSource(configuration, script, parameterType);
      }
    }
  }

上面的代码,首先判断是否有 注解,如果有则使用第一个 createSqlSource 重载方法以XML方式解析。如果不带有