Mybatis analiza el análisis de código fuente SQL uno

Mybatis analiza el análisis de código fuente SQL uno

Programación de tecnología Java TSMYK

Artículos relacionados

Análisis del código fuente de la interfaz Mybatis Mapper Análisis del código fuente del
grupo de conexiones de la base de datos
Mybatis Conversión del tipo
Mybatis Análisis del código fuente Análisis de Mybatis Archivo de configuración Análisis del código fuente

Prefacio

Cuando usamos Mybatis, escribimos SQL en el archivo de configuración Mapper.xml; el dao correspondiente también está configurado en el archivo, y algunas características avanzadas como el bucle for y el juicio if también se pueden usar en SQL. Cuando las columnas de la base de datos y las propiedades de JavaBean resultMap, etc. definido cuando es inconsistente, veamos cómo Mybatis analiza el SQL del archivo de configuración y vincula los parámetros pasados ​​por el usuario;

Cuando Mybatis analiza SQL, se puede dividir en dos partes. Una es analizar el SQL del archivo de configuración Mapper.xml y la otra es analizar el SQL en el SQL original que la base de datos puede ejecutar y reemplazar los marcadores de posición con ?, etc.

Este artículo analizará primero la primera parte, cómo Mybatis analiza SQL desde el archivo de configuración Mapper.xml.

El análisis del archivo de configuración utiliza mucho modo de constructor (constructor)

mybatis-config.xml

Mybatis tiene dos archivos de configuración, mybaits-config.xml configura alguna información de configuración global de mybatis y mapper.xml configura la información SQL. Cuando se inicializa Mybatis, estos dos archivos serán analizados, mybatis-config El análisis del archivo de configuración .xml es relativamente simple, ya no entre en detalles, el uso de la clase XMLConfigBuilder para analizar el archivo 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 }

Como puede ver en el código anterior, XMLMapperBuilder analiza el archivo de configuración Mapper.xml. A continuación, veamos la implementación de esta clase:

XMLMapperBuilder

La clase XMLMapperBuilder se utiliza para analizar el archivo Mapper.xml. Hereda BaseBuilder, una clase base del constructor de la clase BaseBuilder, que contiene la información de configuración global de Mybatis Configuración, procesador de alias, procesador de tipo, etc., como se muestra a continuación:


 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}

Para TypeAliasRegistry, TypeHandlerRegistry, consulte el análisis del código fuente de conversión de tipos de Mybatis

A continuación, observe la definición de atributo de la clase 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  }

A partir del código anterior, se usa la clase auxiliar MapperBuilderAssistant. Hay muchos métodos auxiliares en esta clase. Entre ellos, se usa un atributo currentNamespace para indicar el espacio de nombres del archivo de configuración Mapper.xml actual. Cuando el archivo de configuración Mapper.xml es parsed,, Llamará a bindMapperForNamespace para registrar la interfaz Mapper, indicando la interfaz Mapper correspondiente al archivo de configuración. Para el registro de Mapper, consulte Análisis del código fuente de la interfaz Mybatis Mapper


 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  }

Ahora analicemos cada nodo del archivo Mapper.xml. El análisis de cada nodo está encapsulado en un método, que es fácil de entender:


 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  }

Analizar el nodo <caché>

Mybatis no habilita la caché de segundo nivel de forma predeterminada, excepto la caché de sesión local. Si desea habilitar la caché de segundo nivel para un determinado espacio de nombres, debe agregar la etiqueta <cache> en el archivo de mapeo SQL para decirle a Mybatis que necesita habilitar la caché de segundo nivel. Primero, echemos un vistazo a las instrucciones para usar la etiqueta <caché>:


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

<caché> Hay 6 atributos en total, que se pueden usar para cambiar el comportamiento predeterminado de la caché de Mybatis:

  1. desalojo: La estrategia de caducidad de la caché, que puede tomar 4 valores:
  • LRU-Least Recent Used: Elimina los objetos que no se han utilizado durante más tiempo. (defecto)
  • FIFO-First In First Out: Elimina los objetos en el orden en que ingresan al caché.
  • Referencia SOFT-Soft: Elimina objetos según el estado del recolector de basura y las reglas de referencia suave.
  • DÉBIL-Referencias débiles: elimine objetos de manera más agresiva según el estado del recolector de basura y las reglas de referencia débiles.
    2.flushInterval: el intervalo de tiempo para actualizar la caché, el valor predeterminado no está establecido, es decir, no hay intervalo de actualización, la caché solo se actualiza cuando la declaración se llama
    3.size: tamaño de la caché
    4.readOnly: si es
    5.tipo de solo lectura: caché personalizado lograr
    6.bloqueo: si el bloqueo
    de dicho método se utiliza principalmente para resolver el nodo cacheElement <caché>:

 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  }

A continuación, veamos cómo la clase auxiliar MapperBuilderAssistant crea un caché y lo agrega a la colección 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  }

Veamos qué es CacheBuilder. Es el constructor de Cache, como se muestra a continuación:


 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}

Analizar el nodo <cache-ref>

Después de usar <caché> para configurar la caché correspondiente, varios espacios de nombres pueden hacer referencia a la misma caché, use <cache-ref> para especificar


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

El código fuente analizado es el siguiente, que es relativamente 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  }

El método useCacheRef de 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  }

Analizar el nodo <resultMap>

El nodo resultMap es muy poderoso y complejo, así que escribiré un artículo separado para presentarlo.

Analizar el nodo <sql>

El nodo <sql> se puede utilizar para definir fragmentos SQ reutilizados,


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

El método sqlElement es el siguiente, un archivo Mapper.xml puede tener varios nodos 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  }

El método de prefijar el ID con el espacio de nombres es el siguiente:


 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  }

insertar | actualizar | eliminar | seleccionar análisis de nodo

El análisis de estos SQL relacionados con el funcionamiento de la base de datos se realiza principalmente mediante la clase XMLStatementBuilder. SqlSource se utiliza en Mybatis para representar sentencias SQL, pero estas sentencias SQL no se pueden ejecutar directamente en la base de datos y puede haber sentencias SQL dinámicas y marcadores de posición.

A continuación, veamos el análisis de dichos nodos:

 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}

Puede ver que los nodos selelct | insert | update | delete se analizan utilizando el método parseStatementNode () de la clase XMLStatementBuilder. A continuación, veamos la implementación de este método:


 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}

El método se divide principalmente en varias partes:

  1. Analizar atributos
  2. Analizar el nodo de inclusión
  3. Resolver el nodo selectKey
  4. Es
    relativamente sencillo crear un objeto MapperedStatment y agregarlo a la colección correspondiente a la configuración para resolver los atributos. A continuación, observe las siguientes partes:

El análisis incluye nodos secundarios

Analizar el nodo de inclusión es reemplazar el fragmento SQL que contiene con el fragmento SQL definido por el nodo <sql> y reemplazar los marcadores de posición $ {xxx} con parámetros reales:

Se analiza mediante el método applyIncludes de la clase XMLIncludeTransformer:


 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 es generar la clave principal, no es necesario que la mire.

En este punto, los nodos en el archivo de configuración mapper.xml han sido analizados. Excepto por el nodo resultMap, al principio del artículo, al analizar los nodos, a veces pueden ocurrir errores y pueden lanzarse excepciones, y pueden lanzarse excepciones. en cada análisis.En ese momento, el análisis se colocará en el conjunto correspondiente para su análisis nuevamente, por lo que una vez completado el análisis, hay tres líneas de código de la siguiente manera:


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

Se utiliza para volver a analizar los nodos fallidos.

En este punto, se analiza el archivo de configuración Mapper.xml.

Supongo que te gusta

Origin blog.51cto.com/15077536/2608603
Recomendado
Clasificación