MybatisはSQLソースコード分析を1つ解析します

MybatisはSQLソースコード分析を1つ解析します

TSMYKJavaテクノロジープログラミング

関連記事

MybatisMapperインターフェースのソースコード分析
Mybatisデータベース接続プールのソースコード分析
Mybatisタイプ変換ソースコード分析
Mybatis分析構成ファイルソースコード分析

序文

Mybatisを使用する場合、Mapper.xml構成ファイルにSQLを記述します。対応するdaoもファイルに構成され、forループや判断などのいくつかの高度な機能もSQLで使用できます。データベース列とJavaBeanプロパティの場合一貫性がない場合に定義されたresultMapなどで、Mybatisが構成ファイルからSQLを解析し、ユーザーから渡されたパラメーターをバインドする方法を見てみましょう。

MybatisがSQLを解析する場合、2つの部分に分けることができます.1つはMapper.xml構成ファイルからSQLを解析することであり、もう1つはデータベースが実行できる元のSQLにSQLを解析し、プレースホルダーを次のように置き換えることです。 ?など

この記事では、最初に、MybatisがMapper.xml構成ファイルからSQLを解析する方法について説明します。

構成ファイルの分析では、多くのビルダーモード(ビルダー)を使用します

mybatis-config.xml

Mybatisには2つの構成ファイルがあります。mybaits-config.xmlはmybatisのグローバル構成情報を構成し、mapper.xmlはSQL情報を構成します。Mybatisが初期化されると、これら2つのファイルが解析されます。mybatis-config.xml構成ファイルの解析は比較的単純で、詳細には触れません。XMLConfigBuilderクラスを使用してmybatis-config.xmlファイルを解析します。


 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 }

上記のコードからわかるように、Mapper.xml構成ファイルの解析はXMLMapperBuilderによって解析されます。次に、このクラスの実装を見てみましょう。

XMLMapperBuilder

XMLMapperBuilderクラスは、Mapper.xmlファイルの解析に使用されます。BaseBuilderは、BaseBuilderクラスのビルダー基本クラスであり、以下に示すように、Mybatisグローバル構成情報構成、エイリアスプロセッサー、タイププロセッサーなどが含まれます。


 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}

TypeAliasRegistry、TypeHandlerRegistryについては、Mybatis型変換ソースコード分析を参照してください。

次に、XMLMapperBuilderクラスの属性定義を見てください。


 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  }

上記のコードから、MapperBuilderAssistant補助クラスが使用されます。このクラスには多くの補助メソッドがあります。その中で、currentNamespace属性は、現在のMapper.xml構成ファイルの名前空間を示すために使用されます。Mapper.xml構成ファイルがparsed 、、 bindMapperForNamespaceを呼び出してMapperインターフェースを登録し、構成ファイルに対応するMapperインターフェースを示します。Mapperの登録については、MybatisMapperインターフェースのソースコード分析を参照してください。


 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  }

次に、Mapper.xmlファイルの各ノードを解析してみましょう。各ノードの分析は、理解しやすいメソッドにカプセル化されています。


 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  }

<cache>ノードを解析します

Mybatisは、ローカルセッションキャッシュを除いて、デフォルトで第2レベルのキャッシュを有効にしません。特定の名前空間に対して第2レベルのキャッシュを有効にする場合は、SQLマッピングファイルに<cache>タグを追加して、第2レベルのキャッシュを有効にする必要があることをMybatisに通知する必要があります。まず、を見てみましょう。 <cache>タグの使用手順:


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

<cache>合計6つの属性があり、Mybatisキャッシュのデフォルトの動作を変更するために使用できます。

  1. エビクション:キャッシュの有効期限戦略。4つの値を取ることができます。
  • LRU-最近使用されていないもの:最も長く使用されていないオブジェクトを削除します。(デフォルト)
  • FIFO-先入れ先出し:オブジェクトがキャッシュに入る順序でオブジェクトを削除します。
  • ソフト-ソフトリファレンス:ガベージコレクタのステータスとソフトリファレンスルールに基づいてオブジェクトを削除します。
  • 弱-弱参照:ガベージコレクターのステータスと弱参照ルールに基づいて、オブジェクトをより積極的に削除します。
    2.flushInterval:キャッシュを更新する時間間隔。デフォルトは設定されていません。つまり、更新間隔はありません。キャッシュは、ステートメントが呼び出されたときにのみ更新されます
    。3.size:キャッシュサイズ
    4.readOnly:かどうか読み取り専用
    5.type:カスタムキャッシュは
    6.blockingを実現します:
    そのようなメソッドのブロッキングが主にcacheElement <cache>ノードの解決に使用されるかどうか

 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  }

次に、MapperBuilderAssistant補助クラスがキャッシュを作成し、それをconfiguration.cachesコレクションに追加する方法を見てみましょう。


 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  }

CacheBuilderとは何かを見てみましょう。これは、以下に示すように、Cacheのビルダーです。


 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}

<cache-ref>ノードを解析します

<cache>を使用して対応するキャッシュを構成した後、複数の名前空間が同じキャッシュを参照できます。<cache-ref>を使用して指定します


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

解析されたソースコードは次のとおりですが、これは比較的単純です。


 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  }

builderAssistantのuseCacheRefメソッド:


 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  }

<resultMap>ノードを解析します

resultMapノードは非常に強力で複雑なので、別の記事で紹介します。

<sql>ノードを解析します

<sql>ノードを使用して、再利用されたSQフラグメントを定義できます。


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

sqlElementメソッドは次のとおりです。Mapper.xmlファイルには複数のSQLノードを含めることができます。


 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  }

IDの前に名前空間を付ける方法は次のとおりです。


 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  }

挿入|更新|削除|ノード分析の選択

データベースの操作に関連するこれらのSQLの分析は、主にXMLStatementBuilderクラスによって実行されます。SqlSourceはMybatisでSQLステートメントを表すために使用されますが、これらのSQLステートメントはデータベースで直接実行することはできず、動的なSQLステートメントとプレースホルダーが存在する可能性があります。

次に、そのようなノードの分析を見てみましょう。

 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}

selelct | insert | update | deleteノードがXMLStatementBuilderクラスのparseStatementNode()メソッドを使用して解析されていることがわかります。次に、このメソッドの実装を見てみましょう。


 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}

この方法は主にいくつかの部分に分かれています。

  1. 属性の解析
  2. インクルードノードを解析します
  3. selectKeyノードを解決します
  4. MapperedStatmentオブジェクトを作成し、それを構成に対応するコレクションに追加して
    属性解決するの比較的簡単です。次に、次の部分を確認します。

解析には子ノードが含まれます

インクルードノードの解析では、インクルードノードに含まれるSQLフラグメントを<sql>ノードで定義されたSQLフラグメントに置き換え、$ {xxx}プレースホルダーを実際のパラメーターに置き換えます。

XMLIncludeTransformerクラスのapplyIncludesメソッドを使用して解析されます。


 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は主キーを生成するためのものであり、それを見る必要はありません。

この時点で、mapper.xml構成ファイル内のノードが解析されています。resultMapノードを除いて、記事の冒頭でノードを解析するときに、エラーが発生して例外がスローされたり、例外がスローされたりする場合があります。各解析で、その時点で、分析は再び分析のために対応するセットに入れられるため、分析が完了した後、次の3行のコードがあります。


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

障害が発生したノードを再分析するために使用されます。

この時点で、Mapper.xml構成ファイルが解析されます。

おすすめ

転載: blog.51cto.com/15077536/2608603