preface

I haven’t seen you for a long time. I have been resting for two months since my last article, and during the special period of the epidemic, I stayed at home and died every day. It really testifies to how lazy a person can be. Well, no more nonsense, today will share with you our common persistent layer framework – MyBatis working principle and source code analysis.

To tell the truth, MyBatis is my first persistence layer framework, and I have never used Hibernate before. I directly transferred to this framework after the Java native Jdbc operation database. At that time, my first feeling was that it was too convenient to have a framework. For object encapsulation, we need to obtain the value through resultSetxxx (index), and then manually inject it through the setXXX() method of the object. This repetitive and non-technical work has always been despised by our program. MyBatis can directly map our SQL query data to the object and then directly return a wrapped object, which saves most of the program time, of course, JdbcTemplate can also do it, but let’s not say here. MyBatis has a lot of advantages, of course, only after using Jdbc and MyBatis at the same time, there will be a huge difference, but this is not the focus of today’s discussion, today’s focus is on how MyBatis does it.

For MyBatis, to my personal experience, the workflow is actually divided into two parts: first, build, that is, parse the XML configuration we write and turn it into the object it needs. The second is to execute, on the basis of the completion of the construction, to execute our SQL, complete the interaction with Jdbc. The focus of this post will be on construction.

Xml configuration file

Mybatis -config. XML and mapper. XML can be seen directly on the official website. Of course, for convenience, I will directly copy my XML configuration.

<! -- mybatis-config.xml -->
<?xml version="1.0" encoding="UTF-8" ? >

      
<configuration>
    <! -- Environments will be abolished after integration with Spring
    <environments default="development">
        <environment id="development">
            <! -- Use JDBC transaction management -->
            <transactionManager type="JDBC" />
            <! -- Database connection pool -->
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver" />
                <property name="url"
                          value="jdbc:mysql://xxxxxxx:3306/test? characterEncoding=utf8"/>
                <property name="username" value="username" />
                <property name="password" value="password" />
            </dataSource>
        </environment>
    </environments>

    <! Mapper.xml -->
     <mappers>
     	<! -- <package name=""> -->
         <mapper resource="mapper/DemoMapper.xml"  ></mapper>
     </mappers>
</configuration>
Copy the code
<! -- DemoMapper.xml -->
<?xml version="1.0" encoding="UTF-8"? >

      
<mapper  namespace="com.DemoMapper">
    <select  id="queryTest"   parameterType="Map" resultType="Map">
        select * from test WHERE id =#{id}
    </select>
</mapper>
Copy the code

The mybatis-config. XML file is used to configure data sources, configure aliases, and load mapper. XML, and we can see that the
node of this file contains a
, The path that the Mapper points to is another XML file: Demomapper.xml, and this file contains the SQL we use to query the database.

MyBatis actually parses these two XML files into configuration objects and uses them in execution.

parsing

  • What configuration objects are required for MyBatis?

    We haven’t read the source code here, but as programmers, we can make an assumption based on our daily development experience. Assuming it comes from a question, the question is: why split configuration and SQL statements into two configuration files instead of writing them directly together?

    Does this mean that MyBatis will parse these two configuration files into two different Java objects?

    Might as well put the problem aside first, read the source code.

  • Environment set up

    First we can write a most basic use of MyBatis code, I have written here.

    public static void main(String[] args) throws Exception {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        / / create SqlSessionFacory
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        / * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * line * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /
        SqlSession sqlSession = sqlSessionFactory.openSession();
        / / get the Mapper
        DemoMapper mapper = sqlSession.getMapper(DemoMapper.class);
        Map<String,Object> map = new HashMap<>();
        map.put("id"."123");
        System.out.println(mapper.selectAll(map));
        sqlSession.close();
        sqlSession.commit();
      }
    Copy the code

    The important thing about looking at the source code is to find the entry to the source code, and we can start with these lines to see where the build actually starts.

    First of all, it’s not hard to see, this program displays the bytes read the mybatis – config. XML file, and then through the SqlSessionFactoryBuilder is the build () method, MyBatis builds the configuration object from the XML configuration file we wrote, so the configuration file must be where the build started, which is the build method.

  • Build start

    If we enter the Build method, we can see that parsing is really going on here. This method returns an SqlSessionFactory object that was created using the constructor pattern.

      public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
        try {
          / / parsing mybatis - config. XML
          / / XMLConfigBuilder initializer
          XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
          //parse(): parse the nodes in mybatis-config.xml
          return build(parser.parse());
        } catch (Exception e) {
          throw ExceptionFactory.wrapException("Error building SqlSession.", e);
        } finally {
          ErrorContext.instance().reset();
          try {
            inputStream.close();
          } catch (IOException e) {
            // Intentionally ignore. Prefer previous error.}}}Copy the code

    Enter the parse () :

    public Configuration parse(a) {
    	// Check whether the file has been parsed
        if (parsed) {
          throw new BuilderException("Each XMLConfigBuilder can only be used once.");
        }
        // If not, continue parsing and set the identifier to true
        parsed = true;
        // Parse the < Configuration > node
        parseConfiguration(parser.evalNode("/configuration"));
        return configuration;
      }
    Copy the code

    The < Configuration > node in mybatis-config. XML is used to parse the < Configuration > node.

    The answer is yes, and we can move on.

    As you can see here, although the amount of code is not very large, at least now we can get a general mainline diagram in our brain, as shown below:

    Following this thread, we enter the parseConfiguration(XNode) method and move on.

     private void parseConfiguration(XNode root) {
        try {
          // Parse the nodes under 
            
          //issue #117 read properties first
          //<properties>
          propertiesElement(root.evalNode("properties"));
          //<settings>
          Properties settings = settingsAsProperties(root.evalNode("settings"));
          loadCustomVfs(settings);
          loadCustomLogImpl(settings);
          // Alias 
            
              parsing
            
          // An alias is a Map that stores the class corresponding to the specified alias
          typeAliasesElement(root.evalNode("typeAliases"));
          / / the plugin < plugins >
          pluginElement(root.evalNode("plugins"));
          // Custom instantiated object behavior 
            
          objectFactoryElement(root.evalNode("objectFactory"));
          //MateObject facilitates reflection on objects of the entity class
          objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
          reflectorFactoryElement(root.evalNode("reflectorFactory"));
          settingsElement(settings);
          // read it after objectFactory and objectWrapperFactory issue #631
          //<environments>
          environmentsElement(root.evalNode("environments"));
          databaseIdProviderElement(root.evalNode("databaseIdProvider"));
          // typeHandlers
          typeHandlerElement(root.evalNode("typeHandlers"));
          // Main 
            
              points to the xxxxmapper.xml file where we store the SQL
            
          mapperElement(root.evalNode("mappers"));
        } catch (Exception e) {
          throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: "+ e, e); }}Copy the code

    You can see that this method already resolves nodes under < Configuration >, such as < Settings >,

    ,

    , and
    .

    Each method that parses different tags carries out set or other similar operations on the Configuration object. After these operations, a Configuration object is built. Here, due to the large amount of code, and most of the construction is details. Probably know how to use it, it is not explained in the article, I will pick a major said, of course, interested students can go to pull MyBatis source to see.

  • Mappers

    As mentioned above, the mybatis-config. XML file must have a tag called
    . The
    node in this tag holds the SQL statements that we use to manipulate the database, so the construction of this tag will be the focus of today’s analysis.

    Before we look at the source code, let’s recall how we usually configure the mapper tag. There are several common configurations.

    <mappers>
        <! -- From config file path -->
      <mapper resource="mapper/DemoMapper.xml" ></mapper>
        <! -- Fully qualified class name via Java -->
      <mapper class="com.mybatistest.TestMapper"/>
       <! -- pass url usually used when mapper is not local -->
      <mapper url=""/>
        <! -- pass package name -->
      <package name="com.mybatistest"/>
        <! Mapper = mapper = resource/url/class
    </mappers>
    Copy the code

    These are several ways in which the
    tag can be configured to make it easier to understand the parsing of Mappers.

    private void mapperElement(XNode parent) throws Exception {
      if(parent ! =null) {
          // Iterate over the nodes under mappers to parse
          for (XNode child : parent.getChildren()) {
          // First parse the package node
          if ("package".equals(child.getName())) {
            // Get the package name
            String mapperPackage = child.getStringAttribute("name");
            configuration.addMappers(mapperPackage);
          } else {
            // If no package node exists, scan mapper node
            / / resource/url/mapperClass three values can only have one value has a value
            String resource = child.getStringAttribute("resource");
            String url = child.getStringAttribute("url");
            String mapperClass = child.getStringAttribute("class");
            // Priority resource> URL >mapperClass
            if(resource ! =null && url == null && mapperClass == null) {
                // If the resource in mapper node is not empty
              ErrorContext.instance().resource(resource);
               // Then load the xxxmapper. XML file pointed to by resource directly as a byte stream
              InputStream inputStream = Resources.getResourceAsStream(resource);
              // XMLMapperBuilder parses xxxmapper. XML and you can see that XMLMapperBuilde is also passed in the Configuration object, so mapper must be wrapped in the Configuration object later.
              XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
              / / parsing
              mapperParser.parse();
            } else if (resource == null&& url ! =null && mapperClass == null) {
              / / if the url! =null, then the url is resolved
              ErrorContext.instance().resource(url);
              InputStream inputStream = Resources.getUrlAsStream(url);
              XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
              mapperParser.parse();
            } else if (resource == null && url == null&& mapperClass ! =null) {
                / / if mapperClass! =null, then construct the Configuration by loading the classClass<? > mapperInterface = Resources.classForName(mapperClass); configuration.addMapper(mapperInterface); }else {
          	  If no, throw exceptions directly. If two or three exceptions are configured
              throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
            }
          }
        }
      }
    }
    Copy the code

    Mapper. XML is loaded with resource, so XMLMapperBuilder is used to parse mapper. XML.

    public void parse(a) {
        // Determine if the file was previously parsed
        if(! configuration.isResourceLoaded(resource)) {// Parse the mapper file node (main)
          configurationElement(parser.evalNode("/mapper"));
          configuration.addLoadedResource(resource);
          // Bind the Class object in the Namespace
          bindMapperForNamespace();
        }
        // Reparse the nodes that could not be resolved before, and then fill the pit.
        parsePendingResultMaps();
        parsePendingCacheRefs();
        parsePendingStatements();
      }
    
    
    // Parse nodes in mapper files
    // Get the configuration items inside the configuration and finally encapsulate a MapperedStatemanet
    private void configurationElement(XNode context) {
      try {
      	// Get namespace, this is very important, mybatis will dynamically proxy our Mapper interface through this
        String namespace = context.getStringAttribute("namespace");
        if (namespace == null || namespace.equals("")) {
            // Throw an exception if the namespace is empty
          throw new BuilderException("Mapper's namespace cannot be empty");
        }
        builderAssistant.setCurrentNamespace(namespace);
        // Parse the cache node
        cacheRefElement(context.evalNode("cache-ref"));
        cacheElement(context.evalNode("cache"));
          
        // Parse parameterMap (obsolete) and resultMap 
            
        parameterMapElement(context.evalNodes("/mapper/parameterMap"));
        resultMapElements(context.evalNodes("/mapper/resultMap"));
        // Parse < SQL > nodes
        //< SQL id="staticSql">select * from test
        //<select> <include refid="staticSql"></select>
        sqlElement(context.evalNodes("/mapper/sql"));
        
        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); }}Copy the code

    In this parse() method, a configuationElement code is called to parse the various nodes in the xxxmapper.xml file, This includes

    ,

    , (obsolete),

    , < SQL >, and add, delete, change, and query nodes.

    There is no doubt that we are XXXMapper. XML is essential to write SQL, interact with the database mainly depends on this, so emphatically about analytical method — add and delete node buildStatementFromContext ().

    Before Posting the code, the name gives you a sense of what this means. This method creates a Statement based on the number of nodes we add, delete, change, or query. As anyone who has used native Jdbc knows, a Statement is the object on which we operate our database.

    private void buildStatementFromContext(List<XNode> list) {
        if(configuration.getDatabaseId() ! =null) {
          buildStatementFromContext(list, configuration.getDatabaseId());
        }
        / / to parse the XML
        buildStatementFromContext(list, null);
    }
    
    private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
        for (XNode context : list) {
        final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
        try {
          // Parse the XML node
          statementParser.parseStatementNode();
        } catch (IncompleteElementException e) {
          // If there is a problem with the XML statement, it is stored in the collection and parsed againconfiguration.addIncompleteStatement(statementParser); }}}public void parseStatementNode(a) {
        // Get id in 
        String id = context.getStringAttribute("id");
        // Get databaseId for multiple databases, null here
        String databaseId = context.getStringAttribute("databaseId");
    
        if(! databaseIdMatchesCurrent(id, databaseId,this.requiredDatabaseId)) {
          return;
        }
    	Select update delete insert
        String nodeName = context.getNode().getNodeName();
        // Get the type of SQL operation based on the node name
        SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
        // Check whether it is a query
        boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
        // Cache refresh Default: Add, delete, modify refresh The query is not refreshed
        boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
        // Whether to use level 2 cache Default value: Add, delete, and change this parameter is not used
        boolean useCache = context.getBooleanAttribute("useCache", isSelect);
        // Whether to process nested query results group by
    
        // Three sets of data are divided into a nested query result
        boolean resultOrdered = context.getBooleanAttribute("resultOrdered".false);
    
        // Include Fragments before parsing
        XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
        // Replace the Includes tag with the value in the corresponding SQL tag
        includeParser.applyIncludes(context.getNode());
    
        // Get the name of parameterType
        String parameterType = context.getStringAttribute("parameterType");
        // Get the Class for parameterTypeClass<? > parameterTypeClass = resolveClass(parameterType);// Parse the configuration of the custom scripting language driver here is null
        String lang = context.getStringAttribute("lang");
        LanguageDriver langDriver = getLanguageDriver(lang);
    
        // Parse selectKey after includes and remove them.
        //解析selectKey
        processSelectKeyNodes(id, parameterTypeClass, langDriver);
    
        // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
        // Set the primary key increment rule
        KeyGenerator keyGenerator;
        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;
        }
    / * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /
        // Static parsing is used when there are no dynamic Sql statements and only #{}. Placeholder when ${} is not resolved
        SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
        // Get StatementType, which can be understood as Statement and PreparedStatement
        StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
        / / used
        Integer fetchSize = context.getIntAttribute("fetchSize");
        // The timeout period
        Integer timeout = context.getIntAttribute("timeout");
        / / are outdated
        String parameterMap = context.getStringAttribute("parameterMap");
        // Get the name of the return value type
        String resultType = context.getStringAttribute("resultType");
        // Get the Class of the intensity of the return valueClass<? > resultTypeClass = resolveClass(resultType);// Obtain the ID of the resultMap
        String resultMap = context.getStringAttribute("resultMap");
        // Get the result set type
        String resultSetType = context.getStringAttribute("resultSetType");
        ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
        if (resultSetTypeEnum == null) {
          resultSetTypeEnum = configuration.getDefaultResultSetType();
        }
        String keyProperty = context.getStringAttribute("keyProperty");
        String keyColumn = context.getStringAttribute("keyColumn");
        String resultSets = context.getStringAttribute("resultSets");
    
        // Wrap the property as an MappedStatement object (see code below)
        builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
            fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
            resultSetTypeEnum, flushCache, useCache, resultOrdered,
            keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
      }
    
    // Encapsulate the property as an MappedStatement object
      public MappedStatement addMappedStatement( String id, SqlSource sqlSource, StatementType statementType, SqlCommandType sqlCommandType, Integer fetchSize, Integer timeout, String parameterMap, Class<? > parameterType, String resultMap, Class<? > resultType, ResultSetType resultSetType,boolean flushCache,
          boolean useCache,
          boolean resultOrdered,
          KeyGenerator keyGenerator,
          String keyProperty,
          String keyColumn,
          String databaseId,
          LanguageDriver lang,
          String resultSets) {
    
        if (unresolvedCacheRef) {
          throw new IncompleteElementException("Cache-ref not yet resolved");
        }
    
        //id = namespace
        id = applyCurrentNamespace(id, false);
        boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    
          // Create a constructor for MappedStatement by using constructor mode + chain
        MappedStatement.Builder statementBuilder = newMappedStatement.Builder(configuration, id, sqlSource, sqlCommandType) .resource(resource) .fetchSize(fetchSize) .timeout(timeout) .statementType(statementType) .keyGenerator(keyGenerator) .keyProperty(keyProperty) .keyColumn(keyColumn) .databaseId(databaseId) .lang(lang) .resultOrdered(resultOrdered) .resultSets(resultSets) .resultMaps(getStatementResultMaps(resultMap, resultType, id)) .resultSetType(resultSetType) .flushCacheRequired(valueOrDefault(flushCache, ! isSelect)) .useCache(valueOrDefault(useCache, isSelect)) .cache(currentCache); ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);if(statementParameterMap ! =null) {
          statementBuilder.parameterMap(statementParameterMap);
        }
    
          // Construct the MappedStatement from the constructor
        MappedStatement statement = statementBuilder.build();
         // Encapsulate the MappedStatement object into a Configuration object
        configuration.addMappedStatement(statement);
        return statement;
    }
    Copy the code

    Although this code is very long, but in a word it is tedious but not complex, which is mainly to parse XML nodes. To take a simpler example, suppose we have a configuration like this:

    <select id="selectDemo" parameterType="java.lang.Integer" resultType='Map'>
        SELECT * FROM test
    </select>
    Copy the code

    ParameterType, resultType, and other attributes of this node are required to be wrapped into a MappedStatement object. So MyBatis uses the constructor pattern to construct this object. Finally, when the MappedStatement object is constructed, it is encapsulated in a Configuration object.

    MyBatis (MyBatis, MyBatis, MyBatis, MyBatis, MyBatis, MyBatis, MyBatis) And do two configuration files correspond to two objects? , seems to have the answer, here’s a summary:

    MyBatis will need to parse the Configuration file and eventually parse it into a Configuration object, but it is not wrong to say that two Configuration files correspond to two objects:

    1. Configuration object, which stores the Configuration information of mybatis-config. XML.
    2. MappedStatement, which stores xxxmapper. XML configuration information.

However, the MappedStatement object is encapsulated into the Configuration object and merged into a single object, that is, the Configuration object.

Finally, draw a flow chart of the construction process:

Fill in the pit

  • Where are SQL statements parsed?

    ${}, #{}, ${}, ${}, ${}, ${}, ${} So it would be appropriate to mention it separately.

    First of all, we can confirm that the entire process we just walked through involves the generation of SQL statements. The following code is quite convoluted and hard to read.

    // Static parsing is used when there are no dynamic Sql statements and only #{}. Placeholder when ${} is not resolved
        SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    Copy the code

    Here is the entry point to generate the Sql, and continue from a step debugging perspective.

    /* Enter the createSqlSource method */
    @Override
    public SqlSource createSqlSource(Configuration configuration, XNode script, Class
              parameterType) {
        // Enter the construct
        XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
        / / enter parseScriptNode
        return builder.parseScriptNode();
    }
    /** enter this method */
    public SqlSource parseScriptNode(a) {
        / / #
        // Will parse first
        MixedSqlNode rootSqlNode = parseDynamicTags(context);
        SqlSource sqlSource;
        if (isDynamic) {
          ${} = ${}
          sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
        } else {
          #{} -->?
          sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
        }
        return sqlSource;
    }
    protected MixedSqlNode parseDynamicTags(XNode node) {
        List<SqlNode> contents = new ArrayList<>();
        // Get the subtags under the select tag
        NodeList children = node.getNode().getChildNodes();
        for (int i = 0; i < children.getLength(); i++) {
          XNode child = node.newXNode(children.item(i));
          if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
        	  // If it is a query
        	Select * from test where id = #{id}
            String data = child.getStringBody("");
            TextSqlNode textSqlNode = new TextSqlNode(data);
            // check whether SQL is ${}
            if (textSqlNode.isDynamic()) {
                ${} = ${}
              contents.add(textSqlNode);
              isDynamic = true;
            } else {
            	// If not, generate static SQL directly
                / / # {} - >?
              contents.add(newStaticTextSqlNode(data)); }}else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
        	  // Add, delete, change
            String nodeName = child.getNode().getNodeName();
            NodeHandler handler = nodeHandlerMap.get(nodeName);
            if (handler == null) {
              throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
            }
            handler.handleNode(child, contents);
            isDynamic = true; }}return new MixedSqlNode(contents);
      }
    Copy the code
    / * from among the above code segment to this section takes a lot of code, and not a posted * /
    public SqlSource parse(String originalSql, Class
              parameterType, Map
             
               additionalParameters)
             ,> {
        ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
        // A GenericTokenParser is generated, which starts and ends with #{} and calls its parse method to replace #{} with?
        GenericTokenParser parser = new GenericTokenParser("# {"."}", handler);
        // We can parse #{} and replace it with?
        String sql = parser.parse(originalSql);
        return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
      }
    
    // Go through a complicated parsing process
    public String parse(String text) {
        if (text == null || text.isEmpty()) {
          return "";
        }
        // search open token
        int start = text.indexOf(openToken);
        if (start == -1) {
          return text;
        }
        char[] src = text.toCharArray();
        int offset = 0;
        final StringBuilder builder = new StringBuilder();
        StringBuilder expression = null;
        #{} select? ,#{id1} ${}
        while (start > -1) {
          if (start > 0 && src[start - 1] = ='\ \') {
            // this open token is escaped. remove the backslash and continue.
            builder.append(src, offset, start - offset - 1).append(openToken);
            offset = start + openToken.length();
          } else {
            // found open token. let's search close token.
            if (expression == null) {
              expression = new StringBuilder();
            } else {
              expression.setLength(0);
            }
            builder.append(src, offset, start - offset);
            offset = start + openToken.length();
            int end = text.indexOf(closeToken, offset);
            while (end > -1) {
              if (end > offset && src[end - 1] = ='\ \') {
                // this close token is escaped. remove the backslash and continue.
                expression.append(src, offset, end - offset - 1).append(closeToken);
                offset = end + closeToken.length();
                end = text.indexOf(closeToken, offset);
              } else {
                expression.append(src, offset, end - offset);
                break; }}if (end == -1) {
              // close token was not found.
              builder.append(src, start, src.length - start);
              offset = src.length;
            } else {
            	// Use placeholders?
                // Note the handler.handleToken() method, which is the core
              builder.append(handler.handleToken(expression.toString()));
              offset = end + closeToken.length();
            }
          }
          start = text.indexOf(openToken, offset);
        }
        if (offset < src.length) {
          builder.append(src, offset, src.length - offset);
        }
        return builder.toString();
    }
    
    / / BindingTokenParser handleToken
    // Call this method when ${} is scanned without resolving the value to be replaced at run time
    @Override
    public String handleToken(String content) {
      this.isDynamic = true;
      return null;
    }
    / / ParameterMappingTokenHandler handleToken
    // A global scan of the #{id} string replaces all #{} calls to handleToken with?
    @Override
    public String handleToken(String content) {
          parameterMappings.add(buildParameterMapping(content));
          return "?";
    }
    Copy the code

    This code is rather convoluted, and we should look at it from a macro perspective. So LET me just summarize it here:

    First we get our SQL statement from the

    If the string #{} is scanned, it will be replaced by? .

    So how does he make that judgment?

    GenericTokenParser is generated. This object can be passed an openToken and closeToken. If #{}, then openToken is #{, closeToken is}, This is then replaced by the handler.handleToken() method in the parse method.

    BindingTokenParser (${}, BindingTokenParser, BindingTokenParser); The handler is ParameterMappingTokenHandler instantiate the object.

    Process them separately.

  • What do I mean by unresolvable nodes mentioned above?

    As you can see from the code above, each node in the mapper. XML file is parsed sequentially.

    So suppose I write a number of nodes like this:

    <select id="demoselect" paramterType='java.lang.Integer' resultMap='demoResultMap'>
    </select>
    <resultMap id="demoResultMap" type="demo">
        <id column property>
        <result coulmn property>
    </resultMap>
    Copy the code

    The Select node needs to obtain a resultMap, but the resultMap is not parsed. Therefore, when the < SELECT > node is parsed, the resultMap information cannot be obtained.

    Here’s how MyBatis does it:

    private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
        for (XNode context : list) {
        final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
        try {
          // Parse the XML node
          statementParser.parseStatementNode();
        } catch (IncompleteElementException e) {
          // If there is a problem with the XML statement, it is stored in the collection and parsed againconfiguration.addIncompleteStatement(statementParser); }}}Copy the code

    If a node fails to be parsed, an exception is thrown and the addIncompleteStatement method on the Configuration is called. The parsed object is temporarily stored in the set. After all nodes are parsed, the parsed object in the set will continue to be parsed:

    public void parse(a) {
    	  // Determine if the file was previously parsed
        if(! configuration.isResourceLoaded(resource)) {// Parse the mapper file
          configurationElement(parser.evalNode("/mapper"));
          configuration.addLoadedResource(resource);
          // Bind the Class object in the Namespace
          bindMapperForNamespace();
        }
    
        // Reparse the previously unresolvable node
        parsePendingResultMaps();
        parsePendingCacheRefs();
        parsePendingStatements();
    }
    private void parsePendingResultMaps(a) {
        Collection<ResultMapResolver> incompleteResultMaps = configuration.getIncompleteResultMaps();
        synchronized (incompleteResultMaps) {
          Iterator<ResultMapResolver> iter = incompleteResultMaps.iterator();
          while (iter.hasNext()) {
            try {
                / / add resultMap
              iter.next().resolve();
              iter.remove();
            } catch (IncompleteElementException e) {
              // ResultMap is still missing a resource...}}}}public ResultMap resolve(a) {
        / / add resultMap
        return assistant.addResultMap(this.id, this.type, this.extend, this.discriminator, this.resultMappings, this.autoMapping);
      }
    Copy the code

conclusion

MyBatis will parse the configuration file into configuration objects before executing the query: The XML containing the SQL will be parsed into an MappedStatement object. However, this object will eventually be added to the Configuration object as well. And the execution of SQL, which I will share in the next article on SQL execution.

Welcome to visit my personal Blog: Object’s Blog