Beauty of mybatis source code: 2.2. Convert DOM corresponding to mybatis global configuration file to XNODE object

Posted by lupus2k5 on Fri, 05 Jun 2020 08:15:43 +0200

Convert DOM corresponding to mybatis global configuration file to XNODE object

In the above, we have completed the construction of XmlConfigBuilder object and prepared the basic environment for parsing XML files.

So the next step is to call the parse() method exposed by XmlConfigBuilder to complete the parsing of mybatis configuration file.

public Configuration parse() {
    if (parsed) {
        // The second call to XMLConfigBuilder
        throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    // Reset the parsing flag of XMLConfigBuilder to prevent duplicate parsing
    parsed = true;
    // Start the parsing process of Mybatis configuration file here
    // Parse the configuration configuration file and read the content under the [configuration] node
    parseConfiguration(parser.evalNode("/configuration"));
    // Return configuration instance of Mybatis
    return configuration;
}

Without resolution, mybatis will call the parseConfiguration(XNode root) method to complete the construction of the Configuration object.

The input parameter of parseConfiguration(XNode root) method is an object instance of XNode type. The generation of this object is completed by calling the XNode evalNode(String expression) method of XPathParser created above.

parseConfiguration(parser.evalNode("/configuration"));

The evalNode method receives an XPath address expression. The / in the string "/ configuration" means to get the element from the root node, so "/ configuration" means to get the root element configuration of the configuration file

  • Configuration is the root node of the Mybaits main configuration file. We usually use this method:
    <!--?xml version="1.0" encoding="UTF-8"?-->
    
    
    <configuration>
    ...
    </configuration>
    
/**
  * Resolving the Document object according to the expression to get the corresponding node
  *
  * @param expression Xpath Address expression
  * @return XNode
  */
 public XNode evalNode(String expression) {
     // Resolves the specified node from the document object
     return evalNode(document, expression);
 }

In the evalNode method, take the class property document of the XPathParser obtained above as a parameter and pass it to its overloaded method:

/**
 *  Get nodes from expressions
 * @param root Root node
 * @param expression xpath expression
 * @return XNode
 */
public XNode evalNode(Object root, String expression) {
    // Get DOM node
    Node node = (Node) evaluate(expression, root, XPathConstants.NODE);
    if (node == null) {
        return null;
    }
    // Package as XNode node
    return new XNode(this, node, variables);
}

In the overloaded evalNode method, the work of getting the DOM node corresponding to the expression is delegated to the evaluate method. If the corresponding DOM node is parsed, an instance of XNode object will be constructed and returned to the caller of the method with the XPathParser object itself, the parsed DOM node object and the variables passed in by the user as parameters.

The delegate evaluate method uses the XPath parser to parse the expression into the specified object.

private Object evaluate(String expression, Object root, QName returnType) {
     try {
         // Evaluates the XPath expression in the specified context and returns the result as the specified type.
         return xpath.evaluate(expression, root, returnType);
     } catch (Exception e) {
         throw new BuilderException("Error evaluating XPath.  Cause: " + e, e);
     }
 }

Six constants are defined in the XNode class, and the initialization and assignment of these six constants are completed in the construction method of XNode node.

/**
 * XNode
 *
 * @param xpathParser XPath Resolver
 * @param node        Packed nodes
 * @param variables   User passed in variables
 */
public XNode(XPathParser xpathParser, Node node, Properties variables) {
    // Initialize the resolver corresponding to the node
    this.xpathParser = xpathParser;
    // Initialize DOM node
    this.node = node;
    // Initialization node name
    this.name = node.getNodeName();
    // Initialize user-defined variables
    this.variables = variables;
    // Resolving attribute configuration in a node
    this.attributes = parseAttributes(node);
    // Content contained in resolution node
    this.body = parseBody(node);
}

The value operation of attributes and body attributes needs to be completed by the parseAttributes and parseBody methods respectively.

/**
 * Resolving attribute values in nodes
 *
 * @param n node
 * @return Property collection
 */
private Properties parseAttributes(Node n) {
    // Define Properties object
    Properties attributes = new Properties();
    // Get attribute node
    NamedNodeMap attributeNodes = n.getAttributes();
    if (attributeNodes != null) {
        for (int i = 0; i &lt; attributeNodes.getLength(); i++) {
            Node attribute = attributeNodes.item(i);
            // Perform a placeholder resolution replacement for the value of each property
            String value = PropertyParser.parse(attribute.getNodeValue(), variables);
            // preservation
            attributes.put(attribute.getNodeName(), value);
        }
    }
    return attributes;
}

/**
 * Parsing content in nodes
 *
 * @param node node
 * @return Content in node
 */
private String parseBody(Node node) {
    String data = getBodyData(node);
    if (data == null) {
        NodeList children = node.getChildNodes();
        for (int i = 0; i &lt; children.getLength(); i++) {
            Node child = children.item(i);
            data = getBodyData(child);
            if (data != null) {
                break;
            }
        }
    }
    return data;
}

/**
 * Get the contents of CDATA node and TEXT node
 *
 * @param child node
 */
private String getBodyData(Node child) {
    if (child.getNodeType() == Node.CDATA_SECTION_NODE
            || child.getNodeType() == Node.TEXT_NODE) {
        // Get content from CDATA and TEXT nodes
        String data = ((CharacterData) child).getData();
        // Perform placeholder resolution
        data = PropertyParser.parse(data, variables);
        return data;
    }
    return null;
}

These two methods are relatively simple. The only thing to notice is that when processing the property value and body content, the PropertyParser.parse The (string string, properties variables) method replaces the property values and placeholders in the body.

  • About PropertyParser >The propertyparser acts as a replacement variable placeholder in mybatis. The main function is to replace the placeholder of type ${variable name} with the corresponding actual value.

    PropertyParser only exposes a String parse(String string, Properties variables) method. The function of this method is to replace the specified placeholder with the corresponding value in the variable context. This method has two parameters: one is that the String type may contain the text content of the placeholder; the other is that the property type variable context.

    /**
     * Replace placeholder
     * @param string    Text content
     * @param variables Variable context
     */
    public static String parse(String string, Properties variables) {
        // Placeholder variable handler
        VariableTokenHandler handler = new VariableTokenHandler(variables);
        // Placeholder parser
        GenericTokenParser parser = new GenericTokenParser("${", "}", handler);
        // Return contents of closed label
        return parser.parse(string);
    }
    

    There are two classes involved in the parse method, VariableTokenHandler and GenericTokenParser

    VariableTokenHandler is an implementation class of the TokenHandler interface. TokenHandler defines a String handleToken(String content); method, which is mainly used to carry out some additional processing on the content passed in by the client.

    TokenHandler is also an embodiment of the policy pattern. It defines an interface for unified text processing, and its subclasses are responsible for providing different processing strategies.

    Specifically in the VariableTokenHandler, the function of this method is to replace the placeholders in the incoming text content.

    The construction method of the VariableTokenHandler requires a variables parameter of type Properties, in which the variables defined will be used to replace the placeholders.

    The placeholder resolution operation of the VariableTokenHandler allows the user to${ key:defaultValue }The default value is provided for the specified key in the form of. That is, if there is no variable value matching the key in the variable context, the DefaultValue is used as the key value.

    The separator is used when taking the default value in the placeholder. The default value is:, if you need to modify it, you can add it in the variables parameter org.apache.ibatis . parsing.PropertyParser.default -Value separator = "custom separator" for configuration.

    Using the default value in the placeholder is off by default. If you need to turn it on, you can add it in the variables parameter org.apache.ibatis . parsing.PropertyParser.enable -Default value = true for configuration.

    The following is the construction method of VariableTokenHandler:

    private VariableTokenHandler(Properties variables) {
              this.variables = variables;
              // Whether to allow default values such as${ key:aaaa }
              this.enableDefaultValue = Boolean.parseBoolean(getPropertyValue(KEY_ENABLE_DEFAULT_VALUE, ENABLE_DEFAULT_VALUE));
              // Default value separator
              this.defaultValueSeparator = getPropertyValue(KEY_DEFAULT_VALUE_SEPARATOR, DEFAULT_VALUE_SEPARATOR);
          }
    

    GenericTokenParser is a general placeholder parser. Its construction method has three input parameters: the start tag, the end tag of the placeholder, and the processing policy object for the placeholder content.

    /**
      * GenericTokenParser
      * @param openToken Start tag
      * @param closeToken End tag
      * @param handler Content processor
      */
     public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) {
         this.openToken = openToken;
         this.closeToken = closeToken;
         this.handler = handler;
     }
    

    GenericTokenParser provides a parse(String text) method. The method will find the content matching the placeholder and call TokenHandler to process it. If it does not match the content corresponding to the placeholder, the original content will be returned.

After creating the XNode object, you can use the object to call the parseConfiguration(XNode root) method to perform the real configuration file resolution operation:

parseConfiguration(parser.evalNode("/configuration"));

Before parsing the configuration file, let's briefly understand the DTD definition of mybatis global configuration file:

<!--ELEMENT configuration (
properties?
, settings?
, typeAliases?
, typeHandlers?
, objectFactory?
, objectWrapperFactory?
, reflectorFactory?
, plugins?
, environments?
, databaseIdProvider?
, mappers?
)-->

Referring to the DTD file above, we can find that 11 types of child nodes are allowed under the configuration node. These nodes are optional, which means that the global configuration file of Mybatis can not configure any child nodes (refer to unit test: org.apache.ibatis.builder.XmlConfigBuilderTest#shouldSuccessfullyLoadMinimalXMLConfigFile).

Go back to the method parseConfiguration. In this method, corresponding to the sub node of configuration, the work of parsing configuration is divided into several sub methods to complete:

/**
 * Resolve Configuration node
 */
private void parseConfiguration(XNode root) {
    try {
        //issue #117 read properties first
        // Load the resource configuration file and override the corresponding properties [properties node]
        propertiesElement(root.evalNode("properties"));
        // Convert the content in the settings tab to Property and verify.
        Properties settings = settingsAsProperties(root.evalNode("settings"));
        // Determine how to access the resource file according to the settings configuration
        loadCustomVfs(settings);
        // Determine the way of log processing according to the configuration of settings
        loadCustomLogImpl(settings);
        // Alias resolution
        typeAliasesElement(root.evalNode("typeAliases"));
        // Plug in configuration
        pluginElement(root.evalNode("plugins"));
        // Configure object creation factory
        objectFactoryElement(root.evalNode("objectFactory"));
        // Configure object packaging factory
        objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
        // Configure reflection factory
        reflectorFactoryElement(root.evalNode("reflectorFactory"));
        // Initialize global configuration through settings configuration
        settingsElement(settings);
        // read it after objectFactory and objectWrapperFactory issue #631
        // Load the multi environment source configuration to find the transaction manager and data source corresponding to the current environment (default)
        environmentsElement(root.evalNode("environments"));
        // The database type flag creates a class. Mybatis will load all statements without databaseId and the databaseId property of the current database
        // Statement priority greater than statement without databaseId
        databaseIdProviderElement(root.evalNode("databaseIdProvider"));
        // Register type converter
        typeHandlerElement(root.evalNode("typeHandlers"));
        // !! Register to parse the MapperXml file corresponding to Dao
        mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
        throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
}

Multiple times in the above code root.evalNode(String) method, which is used to get the corresponding XNode node object according to the passed in expression.

public XNode evalNode(String expression) {
      return xpathParser.evalNode(node, expression);
  }

The specific implementation is actually delegated to the XNode evalNode(Object root, String expression) method of the xpathParser parser parser.

/**
 *  Get nodes from expressions
 * @param root Root node
 * @param expression xpath expression
 * @return XNode
 */
public XNode evalNode(Object root, String expression) {
    // Get DOM node
    Node node = (Node) evaluate(expression, root, XPathConstants.NODE);
    if (node == null) {
        return null;
    }
    // Package as XNode node
    return new XNode(this, node, variables);
}

We have seen this method in the above, and will not repeat it here. Now we use propertieselement( root.evalNode ("properties"); for example, explain the function of the xpath expression "properties": This expression represents getting the properties element and all its children.

Pay attention to me and learn more together

Topics: Programming Mybatis Attribute Apache xml