Mybatis parses SQL source code analysis one

Mybatis parses SQL source code analysis one

TSMYK Java technology programming

related articles

Mybatis Mapper interface source code analysis
Mybatis database connection pool source code analysis
Mybatis type conversion source code analysis
Mybatis analysis configuration file source code analysis

Preface

When using Mybatis, we write SQL in the Mapper.xml configuration file; the corresponding dao is also configured in the file, and some advanced features such as for loop and if judgment can also be used in SQL. When database columns and JavaBean properties The resultMap, etc. defined when they are inconsistent, let’s see how Mybatis parses the SQL from the configuration file and binds the parameters passed by the user;

When Mybatis parses SQL, it can be divided into two parts. One is to parse the SQL from the Mapper.xml configuration file, and the other is to parse the SQL into the original SQL that the database can execute, and replace the placeholders with ?, etc.

This article will first look at the first part, how Mybatis parses SQL from the Mapper.xml configuration file.

The analysis of the configuration file uses a lot of builder mode (builder)

mybatis-config.xml

Mybatis has two configuration files, mybaits-config.xml configures some global configuration information of mybatis, and mapper.xml configures SQL information. When Mybatis is initialized, these two files will be parsed, mybatis-config The parsing of the .xml configuration file is relatively simple, no longer go into details, the use of XMLConfigBuilder class to parse the mybatis-config.xml file.


 1  public Configuration parse() {
 2    // 如果已经解析过,则抛异常
 3    if (parsed) {
 4      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
 5    }
 6    parsed = true;
 7    parseConfiguration(parser.evalNode("/configuration"));
 8    return configuration;
 9  }
10  // 解析 mybatis-config.xml 文件下的所有节点
11  private void parseConfiguration(XNode root) {
12      propertiesElement(root.evalNode("properties"));
13      Properties settings = settingsAsProperties(root.evalNode("settings"));
14      // .... 其他的节点........
15      // 解析 mapper.xml 文件
16      mapperElement(root.evalNode("mappers"));
17  }
18
19 // 解析 mapper.xml 文件
20 private void mapperElement(XNode parent) throws Exception {
21    // ......
22    InputStream inputStream = Resources.getUrlAsStream(url);
23    XMLMapperBuilder mapperParser = 
24           new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
25    mapperParser.parse();
26 }

As you can see from the above code, parsing the Mapper.xml configuration file is parsed by XMLMapperBuilder. Next, let's look at the implementation of this class:

XMLMapperBuilder

The XMLMapperBuilder class is used to parse the Mapper.xml file. It inherits BaseBuilder, a builder base class of the BaseBuilder class, which contains the Mybatis global configuration information Configuration, alias processor, type processor, etc., as shown below:


 1public abstract class BaseBuilder {
 2  protected final Configuration configuration;
 3  protected final TypeAliasRegistry typeAliasRegistry;
 4  protected final TypeHandlerRegistry typeHandlerRegistry;
 5
 6  public BaseBuilder(Configuration configuration) {
 7    this.configuration = configuration;
 8    this.typeAliasRegistry = this.configuration.getTypeAliasRegistry();
 9    this.typeHandlerRegistry = this.configuration.getTypeHandlerRegistry();
10  }
11}

For TypeAliasRegistry, TypeHandlerRegistry, please refer to Mybatis type conversion source code analysis

Next look at the attribute definition of the XMLMapperBuilder class:


 1public class XMLMapperBuilder extends BaseBuilder {
 2  // xpath 包装类
 3  private XPathParser parser;
 4  // MapperBuilder 构建助手
 5  private MapperBuilderAssistant builderAssistant;
 6  // 用来存放sql片段的哈希表
 7  private Map<String, XNode> sqlFragments;
 8  // 对应的 mapper 文件
 9  private String resource;
10
11  private XMLMapperBuilder(XPathParser parser, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
12    super(configuration);
13    this.builderAssistant = new MapperBuilderAssistant(configuration, resource);
14    this.parser = parser;
15    this.sqlFragments = sqlFragments;
16    this.resource = resource;
17  }
18  // 解析文件
19  public void parse() {
20    // 判断是否已经加载过该配置文件
21    if (!configuration.isResourceLoaded(resource)) {
22      // 解析 mapper 节点
23      configurationElement(parser.evalNode("/mapper"));
24      // 将 resource 添加到 configuration 的 addLoadedResource 集合中保存,该集合中记录了已经加载过的配置文件
25      configuration.addLoadedResource(resource);
26      // 注册 Mapper 接口
27      bindMapperForNamespace();
28    }
29    // 处理解析失败的 <resultMap> 节点
30    parsePendingResultMaps();
31    // 处理解析失败的 <cache-ref> 节点
32    parsePendingChacheRefs();
33    // 处理解析失败的 SQL 节点
34    parsePendingStatements();
35  }

From the above code, the MapperBuilderAssistant auxiliary class is used. There are many auxiliary methods in this class. The currentNamespace attribute is used to indicate the namespace of the current Mapper.xml configuration file. When the Mapper.xml configuration file is parsed, , Will call bindMapperForNamespace to register the Mapper interface, indicating the Mapper interface corresponding to the configuration file. For the registration of Mapper, please refer to Mybatis Mapper interface source code analysis


 1  private void bindMapperForNamespace() {
 2    // 获取当前的命名空间
 3    String namespace = builderAssistant.getCurrentNamespace();
 4    if (namespace != null) {
 5      Class<?> boundType = Resources.classForName(namespace);
 6      if (boundType != null) {
 7        // 如果还没有注册过该 Mapper 接口,则注册
 8        if (!configuration.hasMapper(boundType)) {
 9          configuration.addLoadedResource("namespace:" + namespace);
10          // 注册
11          configuration.addMapper(boundType);
12        }
13     }
14  }

Now let's parse each node of the Mapper.xml file. The analysis of each node is encapsulated into a method, which is easy to understand:


 1  private void configurationElement(XNode context) {
 2      // 命名空间
 3      String namespace = context.getStringAttribute("namespace");
 4      // 设置命名空间
 5      builderAssistant.setCurrentNamespace(namespace);
 6      // 解析 <cache-ref namespace=""/> 节点
 7      cacheRefElement(context.evalNode("cache-ref"));
 8      // 解析 <cache /> 节点
 9      cacheElement(context.evalNode("cache"));
10      // 已废弃,忽略
11      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
12      // 解析 <resultMap /> 节点
13      resultMapElements(context.evalNodes("/mapper/resultMap"));
14      // 解析 <sql> 节点
15      sqlElement(context.evalNodes("/mapper/sql"));
16      // 解析 select|insert|update|delete 这几个节点
17      buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
18  }

Parse the <cache> node

Mybatis does not enable the second level cache by default, except for the local session cache. If you want to enable the second-level cache for a certain namespace, you need to add the <cache> tag in the SQL mapping file to tell Mybatis that you need to enable the second-level cache. First, let's take a look at the instructions for using the <cache> tag:


1<cache eviction="LRU" flushInterval="1000" size="1024" readOnly="true" type="MyCache" blocking="true"/>

<cache> There are 6 attributes in total, which can be used to change the default behavior of Mybatis cache:

  1. eviction: The expiration strategy of the cache, which can take 4 values:
  • LRU-Least Recently Used: Remove objects that have not been used for the longest time. (default)
  • FIFO-First In First Out: Remove objects in the order they enter the cache.
  • SOFT-Soft Reference: Remove objects based on garbage collector status and soft reference rules.
  • WEAK-Weak references: More aggressively remove objects based on garbage collector status and weak reference rules.
    2.flushInterval: The time interval for refreshing the cache, the default is not set, that is, there is no refresh interval, the cache is only refreshed when the statement is called
    3.size: the size of the cache
    4.readOnly: whether it is read-only
    5.type: custom cache achieve
    6.blocking: whether the blocking
    of such method is mainly used to resolve cacheElement <cache> node:

 1  // 解析 <cache> 节点
 2  private void cacheElement(XNode context) throws Exception {
 3    if (context != null) {
 4      // 获取 type 属性,默认为 PERPETUAL
 5      String type = context.getStringAttribute("type", "PERPETUAL");
 6      Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
 7      // 获取过期策略 eviction 属性
 8      String eviction = context.getStringAttribute("eviction", "LRU");
 9      Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
10      Long flushInterval = context.getLongAttribute("flushInterval");
11      Integer size = context.getIntAttribute("size");
12      boolean readWrite = !context.getBooleanAttribute("readOnly", false);
13      boolean blocking = context.getBooleanAttribute("blocking", false);
14      // 获取 <cache> 节点下的子节点,将用于初始化二级缓存
15      Properties props = context.getChildrenAsProperties();
16      // 创建 Cache 对象,并添加到 configuration.caches 集合中保存
17      builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
18    }
19  }

Next, let's see how the MapperBuilderAssistant auxiliary class creates a cache and adds it to the configuration.caches collection:


 1  public Cache useNewCache(Class<? extends Cache> typeClass, Class<? extends Cache> evictionClass,
 2      Long flushInterval, Integer size, boolean readWrite, boolean blocking, Properties props) {
 3    // 创建缓存,使用构造者模式设置对应的属性
 4    Cache cache = new CacheBuilder(currentNamespace)
 5        .implementation(valueOrDefault(typeClass, PerpetualCache.class))
 6        .addDecorator(valueOrDefault(evictionClass, LruCache.class))
 7        .clearInterval(flushInterval)
 8        .size(size)
 9        .readWrite(readWrite)
10        .blocking(blocking)
11        .properties(props)
12        .build();
13    // 进入缓存集合
14    configuration.addCache(cache);
15    // 当前缓存
16    currentCache = cache;
17    return cache;
18  }

Let's look at what CacheBuilder is. It is the builder of Cache, as shown below:


 1public class CacheBuilder {
 2  // Cache 对象的唯一标识,对应配置文件中的 namespace
 3  private String id;
 4  // Cache 的实现类
 5  private Class<? extends Cache> implementation;
 6  // 装饰器集合
 7  private List<Class<? extends Cache>> decorators;
 8  private Integer size;
 9  private Long clearInterval;
10  private boolean readWrite;
11  // 其他配置信息
12  private Properties properties;
13  // 是否阻塞
14  private boolean blocking;
15
16  // 创建 Cache 对象
17  public Cache build() {
18    // 设置 implementation 的默认值为 PerpetualCache ,decorators 的默认值为 LruCache
19    setDefaultImplementations();
20    // 创建 Cache
21    Cache cache = newBaseCacheInstance(implementation, id);
22    // 设置 <properties> 节点信息
23    setCacheProperties(cache);
24    if (PerpetualCache.class.equals(cache.getClass())) {
25      for (Class<? extends Cache> decorator : decorators) {
26        cache = newCacheDecoratorInstance(decorator, cache);
27        setCacheProperties(cache);
28      }
29      cache = setStandardDecorators(cache);
30    } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
31      cache = new LoggingCache(cache);
32    }
33    return cache;
34  }
35}

Parse the <cache-ref> node

After using <cache> to configure the corresponding cache, multiple namespaces can refer to the same cache, use <cache-ref> to specify


1<cache-ref namespace="com.someone.application.data.SomeMapper"/>
2
3cacheRefElement(context.evalNode("cache-ref"));

The parsed source code is as follows, which is relatively simple:


 1  private void cacheRefElement(XNode context) {
 2      // 当前文件的namespace
 3      String currentNamespace = builderAssistant.getCurrentNamespace();
 4      // ref 属性所指向引用的 namespace
 5      String refNamespace = context.getStringAttribute("namespace");
 6      // 会存入到 configuration 的一个 map 中, cacheRefMap.put(namespace, referencedNamespace);
 7      configuration.addCacheRef(currentNamespace , refNamespace );
 8      CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, refNamespace);
 9      // 实际上调用 构建助手 builderAssistant 的 useCacheRef 方法进行解析
10      cacheRefResolver.resolveCacheRef();
11    }
12  }

The useCacheRef method of builderAssistant:


 1  public Cache useCacheRef(String namespace) {
 2      // 标识未成功解析的 Cache 引用
 3      unresolvedCacheRef = true;
 4      // 根据 namespace 中 configuration 的缓存集合中获取缓存
 5      Cache cache = configuration.getCache(namespace);
 6      if (cache == null) {
 7        throw new IncompleteElementException("....");
 8      }
 9      // 当前使用的缓存
10      currentCache = cache;
11      // 已成功解析 Cache 引用
12      unresolvedCacheRef = false;
13      return cache;
14  }

Parse the <resultMap> node

The resultMap node is very powerful and complex, so I will write a separate article to introduce it.

Parse the <sql> node

The <sql> node can be used to define reused SQ fragments,


1    <sql id="commSQL" databaseId="" lang="">
2        id, name, job, age
3    </sql>
4
5    sqlElement(context.evalNodes("/mapper/sql"));

The sqlElement method is as follows, a Mapper.xml file can have multiple sql nodes:


 1  private void sqlElement(List<XNode> list, String requiredDatabaseId) throws Exception {
 2    // 遍历,处理每个 sql 节点
 3    for (XNode context : list) {
 4      // 数据库ID
 5      String databaseId = context.getStringAttribute("databaseId");
 6      // 获取 id 属性
 7      String id = context.getStringAttribute("id");
 8      // 为 id 加上 namespace 前缀,如原来 id 为 commSQL,加上前缀就变为了 com.aa.bb.cc.commSQL
 9      id = builderAssistant.applyCurrentNamespace(id, false);
10      if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) {
11        // 如果 SQL 片段匹配对应的数据库,则把该节点加入到缓存中,是一个 map
12        // Map<String, XNode> sqlFragments
13        sqlFragments.put(id, context);
14      }
15    }
16  }

The method of prefixing the ID with namespace is as follows:


 1  public String applyCurrentNamespace(String base, boolean isReference) {
 2    if (base == null) {
 3      return null;
 4    }
 5     // 是否已经包含 namespace 了
 6    if (isReference) {
 7      if (base.contains(".")) {
 8        return base;
 9      }
10    } else {
11      // 是否是一 namespace. 开头
12      if (base.startsWith(currentNamespace + ".")) {
13        return base;
14      }
15    }
16    // 返回 namespace.id,即 com.aa.bb.cc.commSQL
17    return currentNamespace + "." + base;
18  }

insert | update | delete | select node analysis

The analysis of these SQLs related to operating the database is mainly performed by the XMLStatementBuilder class. SqlSource is used to represent SQL statements in Mybatis, but these SQL statements cannot be executed directly in the database. There may be dynamic SQL statements and placeholders.

Next, let's look at the analysis of such nodes:

 1buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
 2
 3private void buildStatementFromContext(List<XNode> list) {
 4// 匹配对应的数据库
 5if (configuration.getDatabaseId() != null) {
 6  buildStatementFromContext(list, configuration.getDatabaseId());
 7}
 8buildStatementFromContext(list, null);
 9}
10
11private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
12for (XNode context : list) {
13  // 为 XMLStatementBuilder 对应的属性赋值
14  final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
15  // 解析每个节点
16  statementParser.parseStatementNode();
17}

You can see that selelct | insert | update | delete nodes are parsed using the parseStatementNode() method of the XMLStatementBuilder class. Next, let's look at the implementation of this method:


 1  public void parseStatementNode() {
 2    // id 属性和数据库标识
 3    String id = context.getStringAttribute("id");
 4    String databaseId = context.getStringAttribute("databaseId");
 5    // 如果数据库不匹配则不加载
 6    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
 7      return;
 8    }
 9    // 获取节点的属性和对应属性的类型
10    Integer fetchSize = context.getIntAttribute("fetchSize");
11    Integer timeout = context.getIntAttribute("timeout");
12    Integer fetchSize = context.getIntAttribute("fetchSize");
13    Integer timeout = context.getIntAttribute("timeout");
14    String parameterMap = context.getStringAttribute("parameterMap");
15    String parameterType = context.getStringAttribute("parameterType");
16    // 从注册的类型里面查找参数类型
17    Class<?> parameterTypeClass = resolveClass(parameterType);
18    String resultMap = context.getStringAttribute("resultMap");
19    String resultType = context.getStringAttribute("resultType");
20    String lang = context.getStringAttribute("lang");
21    LanguageDriver langDriver = getLanguageDriver(lang);
22    // 从注册的类型里面查找返回值类型
23    Class<?> resultTypeClass = resolveClass(resultType);
24    String resultSetType = context.getStringAttribute("resultSetType");
25    StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
26    ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
27
28    // 获取节点的名称
29    String nodeName = context.getNode().getNodeName();
30    // 根据节点的名称来获取节点的类型,枚举:UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH;
31    SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
32    // 下面这三行代码,如果是select语句,则不会刷新缓存和需要使用缓存
33    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
34    boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
35    boolean useCache = context.getBooleanAttribute("useCache", isSelect);
36    boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
37
38    // 解析 <include> 节点
39    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
40    includeParser.applyIncludes(context.getNode());
41
42    // 解析 selectKey 节点
43    processSelectKeyNodes(id, parameterTypeClass, langDriver);
44    // 创建 sqlSource 
45    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
46    // 处理 resultSets keyProperty keyColumn  属性
47    String resultSets = context.getStringAttribute("resultSets");
48    String keyProperty = context.getStringAttribute("keyProperty");
49    String keyColumn = context.getStringAttribute("keyColumn");
50    // 处理 keyGenerator
51    KeyGenerator keyGenerator;
52    String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
53    keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
54    if (configuration.hasKeyGenerator(keyStatementId)) {
55      keyGenerator = configuration.getKeyGenerator(keyStatementId);
56    } else {
57      keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
58          configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
59          ? new Jdbc3KeyGenerator() : new NoKeyGenerator();
60    }
61    // 创建 MapperedStatement 对象,添加到 configuration 中
62    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
63        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
64        resultSetTypeEnum, flushCache, useCache, resultOrdered, 
65        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
66}

The method is mainly divided into several parts:

  1. Parsing attributes
  2. Parse the include node
  3. Resolve selectKey node
  4. It is
    relatively simple to create a MapperedStatment object and add it to the collection corresponding to the configuration to resolve the attributes. Next, look at the following parts:

Parsing include child nodes

Parsing the include node is to replace the SQL fragment it contains with the SQL fragment defined by the <sql> node, and replace the ${xxx} placeholders with real parameters:

It is parsed using the applyIncludes method of the XMLIncludeTransformer class:


 1  public void applyIncludes(Node source) {
 2    // 获取参数
 3    Properties variablesContext = new Properties();
 4    Properties configurationVariables = configuration.getVariables();
 5    if (configurationVariables != null) {
 6      variablesContext.putAll(configurationVariables);
 7    }
 8    // 解析
 9    applyIncludes(source, variablesContext, false);
10  }
11
12  private void applyIncludes(Node source, final Properties variablesContext, boolean included) {
13    if (source.getNodeName().equals("include")) {
14      // 这里是根据 ref 属性对应的值去 <sql> 节点对应的集合查找对应的SQL片段,
15      // 在解析 <sql> 节点的时候,把它放到了一个map中,key为namespace+id,value为对应的节点,
16      // 现在要拿 ref 属性去这个集合里面获取对应的SQL片段
17      Node toInclude = findSqlFragment(getStringAttribute(source, "refid"), variablesContext);
18      // 解析include的子节点<properties>
19      Properties toIncludeContext = getVariablesContext(source, variablesContext);
20      // 递归处理<include>节点
21      applyIncludes(toInclude, toIncludeContext, true);
22      if (toInclude.getOwnerDocument() != source.getOwnerDocument()) {
23        toInclude = source.getOwnerDocument().importNode(toInclude, true);
24      }
25      // 将 include 节点替换为 sql 节点
26      source.getParentNode().replaceChild(toInclude, source);
27      while (toInclude.hasChildNodes()) {
28        toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);
29      }
30      toInclude.getParentNode().removeChild(toInclude);
31    } else if (source.getNodeType() == Node.ELEMENT_NODE) {
32      // 处理当前SQL节点的子节点
33      NodeList children = source.getChildNodes();
34      for (int i = 0; i < children.getLength(); i++) {
35        applyIncludes(children.item(i), variablesContext, included);
36      }
37    } else if (included && source.getNodeType() == Node.TEXT_NODE
38        && !variablesContext.isEmpty()) {
39      // 绑定参数
40      source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
41    }
42  }

selectKey is to generate the primary key, you don't need to look at it.

At this point, the nodes in the mapper.xml configuration file have been parsed. Except for the resultMap node, at the beginning of the article, when parsing the nodes, sometimes errors may occur and exceptions may be thrown. When parsing each parsing, exceptions At that time, the analysis will be put into the corresponding set for analysis again, so after the analysis is completed, there are three lines of code as follows:


1    // 处理解析失败的 <resultMap> 节点
2    parsePendingResultMaps();
3    // 处理解析失败的 <cache-ref> 节点
4    parsePendingChacheRefs();
5    // 处理解析失败的 SQL 节点
6    parsePendingStatements();

It is used to re-analyze the failed nodes.

At this point, the Mapper.xml configuration file is parsed.

Guess you like

Origin blog.51cto.com/15077536/2608603