Mybatis source code reading (3): mybatis initialization (below) mapper analysis

The real power of MyBatis is its mapping statement, and that's where its magic lies. Because of how powerful it is, the mapper's XML file is relatively simple. If you compare it to JDBC code with the same functionality, you'll immediately notice that you've saved nearly 95% of the code. MyBatis is built for SQL and does it better than the normal way.

The SQL map file has a few top-level elements (in the order they should be defined):

  • cache – cache configuration for the given namespace.
  • cache-ref - A reference to other namespace cache configurations.
  • resultMap – is the most complex and powerful element that describes how to load objects from a database result set.
  • parameterMap  - Obsolete! Old-fashioned parameter mapping. Inline parameters are preferred, this element may be removed in the future and is not documented here.
  • sql – A block of reusable statements that can be referenced by other statements.
  • insert – map insert statement
  • update – map update statement
  • delete – map delete statement
  • select – map query statement

The attributes and functions of each tag are not explained here. You can refer to the official documentation: http://www.mybatis.org/mybatis-3/zh/sqlmap-xml.html

The previous article introduced the following statement in the source code of mybatis configuration file parsing mappers node, which is parsed by XMLMapperBuilder when the mapper mapping file is obtained from here.

一、XMLMapperBuilder

//mapper mapping files are parsed through XMLMapperBuilder
          XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
          mapperParser.parse();
//parse mapper file
public void parse() {
  // Determine if the mapping file has been loaded
  if (!configuration.isResourceLoaded(resource)) {
    // process mapper node
    configurationElement(parser.evalNode("/mapper"));
    // Add the resource to the configuration's loadedResources collection and save it as a HashSet<String>
    configuration.addLoadedResource(resource);
    //Register mapper interface
    bindMapperForNamespace ();
  }
  // Process the resultMap node that failed to parse
  parsePendingResultMaps ();
  // Handle cache-ref nodes that failed to parse
  parsePendingCacheRefs ();
  // Process the sql node that failed to parse
  parsePendingStatements ();
}
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");
    }
    // record the current namespace
    builderAssistant.setCurrentNamespace(namespace);
    // Parse the cache-ref node
    cacheRefElement(context.evalNode("cache-ref"));
    // Parse the cache node
    cacheElement(context.evalNode("cache"));
    // Parse the parameterMap node, this has been abandoned and is not recommended to use
    parameterMapElement(context.evalNodes("/mapper/parameterMap"));
    // Parse the resultMap node
    resultMapElements(context.evalNodes("/mapper/resultMap"));
    // Parse the sql node
    sqlElement(context.evalNodes("/mapper/sql"));
    // Parse the statement
    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);
  }
}

1.cache node

        It completes the creation of the cache by calling the corresponding method of CacheBuilder. Each cache has a unique ID inside, and the value of this id is the namespace. The created cache object is stored in the configured cache cache (the cache is keyed by the ID attribute of the cache, that is, the namespace, which once again reflects the powerful use of the namespace of mybatis).

/**
 * cache - configure the cache for this namespace.
 * type- cache implementation class, the default is PERPETUAL, you can use a custom cache implementation class (alias or full class name)
 * eviction- recovery algorithm, the default is LRU, the optional algorithms are:
 * LRU - Least Recently Used: Remove objects that have not been used for the longest time.
 * FIFO - first in, first out: remove objects in the order in which they entered the cache.
 * SOFT - Soft Reference: Remove objects based on garbage collector state and soft reference rules.
 * WEAK – Weak References: More aggressive removal of objects based on garbage collector state and weak reference rules.
 * flushInterval- flush interval, the default is 1 hour, in milliseconds
 * size- cache size, the default size is 1024, the unit is the number of references
 * readOnly - read only
 * @param context
 * @throws Exception
 */
private void cacheElement(XNode context) throws Exception {
  if (context != null) {
    String type = context.getStringAttribute("type", "PERPETUAL");
    Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
    String eviction = context.getStringAttribute("eviction", "LRU");
    Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
    Long flushInterval = context.getLongAttribute("flushInterval");
    Integer size = context.getIntAttribute("size");
    boolean readWrite = !context.getBooleanAttribute("readOnly", false);
    boolean blocking = context.getBooleanAttribute("blocking", false);
    Properties props = context.getChildrenAsProperties();
    builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
  }
}
public Cache useNewCache(Class<? extends Cache> typeClass,
    Class<? extends Cache> evictionClass,
    Long flushInterval,
    Integer size,
    boolean readWrite,
    boolean blocking,
    Properties props) {
  Cache cache = new CacheBuilder(currentNamespace)
      .implementation(valueOrDefault(typeClass, PerpetualCache.class))
      .addDecorator(valueOrDefault(evictionClass, LruCache.class))
      .clearInterval(flushInterval)
      .size(size)
      .readWrite(readWrite)
      .blocking(blocking)
      .properties(props)
      .build();
  configuration.addCache(cache);
  currentCache = cache;
  return cache;
}

2.cache-ref node

        The cacheRefElement method is responsible for parsing the cache-ref element, and it completes the reference of the cache by calling the corresponding method of the CacheRefResolver. The created cache-ref reference relationship is stored in the configured cacheRefMap cache.

/**
 * cache-ref – reference cache configuration from other namespaces.
 * If you don't want to define your own cache, you can use cache-ref to refer to other caches.
 * Because each cache has namespace as id,
 * So cache-ref only needs to configure a namespace attribute.
 * It should be noted that if both cache-ref and cache are configured, the cache shall prevail.
 * @param context
 */
private void cacheRefElement(XNode context) {
  if (context != null) {
    configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace"));
    CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace"));
    try {
      cacheRefResolver.resolveCacheRef();
    } catch (IncompleteElementException e) {
      configuration.addIncompleteCacheRef(cacheRefResolver);
    }
  }
}

3. resultMap node

        The resultMapElement method is responsible for parsing the resultMap element, and it completes the parsing of the resultMap by calling the corresponding method of the ResultMapResolver. The other child nodes except the discriminator child node under the resultMap node will be parsed into the corresponding ResultMapping object, and each <resultMap> node will be parsed into a ResultMap object, and the created resultMap will be stored in the resultMaps cache of the configuration (the cache is named as namespace The id of +resultMap is the key, which once again reflects the powerful usefulness of the namespace of mybatis).

private void resultMapElements(List<XNode> list) throws Exception {
  for (XNode resultMapNode : list) {
    try {
      resultMapElement(resultMapNode);
    } catch (IncompleteElementException e) {
      // ignore, it will be retried
    }
  }
}

private ResultMap resultMapElement(XNode resultMapNode) throws Exception {
  return resultMapElement(resultMapNode, Collections.<ResultMapping> emptyList());
}

private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings) throws Exception {
  ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
  String id = resultMapNode.getStringAttribute("id",
      resultMapNode.getValueBasedIdentifier());
  String type = resultMapNode.getStringAttribute("type",
      resultMapNode.getStringAttribute("ofType",
          resultMapNode.getStringAttribute("resultType",
              resultMapNode.getStringAttribute("javaType"))));
  String extend = resultMapNode.getStringAttribute("extends");
  Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
  Class<?> typeClass = resolveClass(type);
  Discriminator discriminator = null;
  List<ResultMapping> resultMappings = new ArrayList<ResultMapping>();
  resultMappings.addAll(additionalResultMappings);
  List<XNode> resultChildren = resultMapNode.getChildren();
  for (XNode resultChild : resultChildren) {
    if ("constructor".equals(resultChild.getName())) {
      processConstructorElement(resultChild, typeClass, resultMappings);
    } else if ("discriminator".equals(resultChild.getName())) {
      discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
    } else {
      List<ResultFlag> flags = new ArrayList<ResultFlag>();
      if ("id".equals(resultChild.getName())) {
        flags.add(ResultFlag.ID);
      }
      resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
    }
  }
  ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
  try {
    return resultMapResolver.resolve();
  } catch (IncompleteElementException  e) {
    configuration.addIncompleteResultMap(resultMapResolver);
    throw e;
  }
}

4. sql node analysis

The sql node is used to define reusable sql statement fragments, and the sqlElement method is responsible for parsing sql elements. The id attribute is used to distinguish different sql elements, and multiple sql elements can be configured in the same mapper configuration file.

private void sqlElement(List<XNode> list) throws Exception {
  if (configuration.getDatabaseId() != null) {
    sqlElement(list, configuration.getDatabaseId());
  }
  sqlElement(list, null);
}

private void sqlElement(List<XNode> list, String requiredDatabaseId) throws Exception {
  for (XNode context : list) {
    String databaseId = context.getStringAttribute("databaseId");
    String id = context.getStringAttribute("id");
    id = builderAssistant.applyCurrentNamespace(id, false);
    if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) {
      // The record is saved in sqlFragments. In fact, in the constructor, you can see that this field points to the sqlFragments collection of the configuration
      sqlFragments.put(id, context);
    }
  }
}

private boolean databaseIdMatchesCurrent(String id, String databaseId, String requiredDatabaseId) {
  if (requiredDatabaseId != null) {
    if (!requiredDatabaseId.equals(databaseId)) {
      return false;
    }
  } else {
    if (databaseId != null) {
      return false;
    }
    // skip this fragment if there is a previous one with a not null databaseId
    if (this.sqlFragments.containsKey(id)) {
      XNode context = this.sqlFragments.get(id);
      if (context.getStringAttribute("databaseId") != null) {
        return false;
      }
    }
  }
  return true;
}

二、XMLStatementBuilder

There is another kind of important node in the mapping configuration file that needs to be parsed. In fact, it is the select|insert|update|delete node. These nodes are mainly used to define SQL statements. They are not parsed by XMLMapperBuilder, but by XMLStatementBuilder. A node will be parsed into a MappedStatement object and stored in the configuration object. There are several important steps within this method, and understanding them is helpful to properly configure the statement element.

1.MappedStatement

MappedStatement contains many properties of these nodes, the more important ones are as follows:

private String resource;//The id in the node includes the namespace
private SqlSource sqlSource;//SqlSource object, corresponding to a SQL statement
private SqlCommandType sqlCommandType;//SQL的类型,insert,delete,select,update

The parsing process code is as follows:

public void parseStatementNode() {
  // Get the id of the sql node and the databaseId if it does not match the current node and do not load the node,
  // If there is a node with the same id and the databaseId is not empty, the node is not loaded
  String id = context.getStringAttribute("id");
  String databaseId = context.getStringAttribute("databaseId");

  if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
    return;
  }

  // Get various attributes of the node
  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");
  String lang = context.getStringAttribute("lang");
  LanguageDriver langDriver = getLanguageDriver (lang);

  Class<?> resultTypeClass = resolveClass(resultType);
  String resultSetType = context.getStringAttribute("resultSetType");
  StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
  ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);

  // Set the type of sqlCommandType according to the name of the node
  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);

  // Process the include node before parsing the SQL statement
  XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
  includeParser.applyIncludes(context.getNode());

  // Process the selectKey node
  processSelectKeyNodes(id, parameterTypeClass, langDriver);
  
  // Complete the parsing of the node. This part is the core
  SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
  // Get three properties of resultSets keyProperty keyColumn
  String resultSets = context.getStringAttribute("resultSets");
  String keyProperty = context.getStringAttribute("keyProperty");
  String keyColumn = context.getStringAttribute("keyColumn");
  KeyGenerator keyGenerator;
  // Get the id of the selectKeyGenerator corresponding to the selectKey node
  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;
  }

  // Create MappedStatement object through MapperBuilderAssistant,
  // and add to the configuration.mappedStatements collection to save
  builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
      fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
      resultSetTypeEnum, flushCache, useCache, resultOrdered,
      keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}

2. Parse the include node

Before parsing the statement node, first parse the include node through XMLIncludeTransformer. The change process will replace the SQL fragment defined in the <sql> node with the include node, and replace the ${xx} placeholders with real parameters.

private void applyIncludes(Node source, final Properties variablesContext, boolean included) {
  if (source.getNodeName().equals("include")) { // ---(2) Process include nodes
    // Find the <sql> pointed to by the refid attribute, and return a deep cloned Node object
    Node toInclude = findSqlFragment(getStringAttribute(source, "refid"), variablesContext);
    Properties toIncludeContext = getVariablesContext(source, variablesContext);
    // Recursively process include nodes
    applyIncludes(toInclude, toIncludeContext, true);
    if (toInclude.getOwnerDocument() != source.getOwnerDocument()) {
      toInclude = source.getOwnerDocument().importNode(toInclude, true);
    }
    // replace the <sql> node with the <include> node
    source.getParentNode().replaceChild(toInclude, source);
    while (toInclude.hasChildNodes()) {
      // Add the child nodes of the <sql> node to the front of the <sql> node
      toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);
    }
    // delete <sql> node after replacement
    toInclude.getParentNode().removeChild(toInclude);
  } else if (source.getNodeType() == Node.ELEMENT_NODE) {// ---(1)
    if (included && !variablesContext.isEmpty()) {
      // replace variables in attribute values
      NamedNodeMap attributes = source.getAttributes();
      for (int i = 0; i < attributes.getLength(); i++) {// Traverse the child nodes of the current sql
        Node attr = attributes.item(i);
        attr.setNodeValue(PropertyParser.parse(attr.getNodeValue(), variablesContext));
      }
    }
    NodeList children = source.getChildNodes();
    for (int i = 0; i < children.getLength(); i++) {
      applyIncludes(children.item(i), variablesContext, included);
    }
  } else if (included && source.getNodeType() == Node.TEXT_NODE
      && !variablesContext.isEmpty()) {// ---(3)
    // replace variables in text node to replace the corresponding placeholders
    source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
  }
}

3. Parse the selectKey node

The selectKey node can be defined in the insert and update nodes to solve the problem of auto-increment of the primary key.

private void parseSelectKeyNode(String id, XNode nodeToHandle, Class<?> parameterTypeClass, LanguageDriver langDriver, String databaseId) {
  String resultType = nodeToHandle.getStringAttribute("resultType");
  Class<?> resultTypeClass = resolveClass(resultType);
  StatementType statementType = StatementType.valueOf(nodeToHandle.getStringAttribute("statementType", StatementType.PREPARED.toString()));
  String keyProperty = nodeToHandle.getStringAttribute("keyProperty");
  String keyColumn = nodeToHandle.getStringAttribute("keyColumn");
  boolean executeBefore = "BEFORE".equals(nodeToHandle.getStringAttribute("order", "AFTER"));

  //defaults
  boolean useCache = false;
  boolean resultOrdered = false;
  KeyGenerator keyGenerator = NoKeyGenerator.INSTANCE;
  Integer fetchSize = null;
  Integer timeout = null;
  boolean flushCache = false;
  String parameterMap = null;
  String resultMap = null;
  ResultSetType resultSetTypeEnum = null;

  // Generate SqlSource
  SqlSource sqlSource = langDriver.createSqlSource (configuration, nodeToHandle, parameterTypeClass);
  // Only select statements can be configured in the selectKey node
  SqlCommandType sqlCommandType = SqlCommandType.SELECT;

  // Create a MappedStatement object and add it to the mappedStatements collection of the configuration to save
  builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
      fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
      resultSetTypeEnum, flushCache, useCache, resultOrdered,
      keyGenerator, keyProperty, keyColumn, databaseId, langDriver, null);

  id = builderAssistant.applyCurrentNamespace(id, false);

  MappedStatement keyStatement = configuration.getMappedStatement(id, false);
  // Create the corresponding KeyGenerator (primary key auto-increment strategy) and add it to the configuration
  configuration.addKeyGenerator(id, new SelectKeyGenerator(keyStatement, executeBefore));
}

3. Bind the Mapper interface

The namespace of each mapping configuration file can be bound to a Mapper interface and registered in the MapperRegistry.

// bind mapper interface
private void bindMapperForNamespace() {
  //Get the namespace of the mapping file
  String namespace = builderAssistant.getCurrentNamespace();
  if (namespace != null) {
    Class<?> boundType = null;
    try {
      // Parse the type corresponding to the namespace, that is, dao
      boundType = Resources.classForName(namespace);
    } catch (ClassNotFoundException e) {
      //ignore, bound type is not required
    }
    if (boundType != null) {
      if (!configuration.hasMapper(boundType)) {// Has it been loaded
        // Spring may not know the real resource name so we set a flag
        // to prevent loading again this resource from the mapper interface
        // look at MapperAnnotationBuilder # loadXmlResource
        //register
        configuration.addLoadedResource("namespace:" + namespace);
        configuration.addMapper(boundType);
      }
    }
  }
}

Guess you like

Origin http://10.200.1.11:23101/article/api/json?id=326629024&siteId=291194637