MyBatis source code parsing -- MyBatis initialization process parsing

Keywords: Mybatis xml SQL Attribute

1. Preparation

To see the whole initialization process of MyBatis, first create a simple Java project. The directory structure is shown in the following figure:

 

1.1 Product entity class

 

public class Product {
    private long id;
    private String productName;
    private String productContent;
    private String price;
    private int sort;
    private int falseSales;
    private long category_id;
    private byte type;
    private byte state;
    // PS: omitting setter and getter functions
}

1.2 product mapper persistent interface

 

public interface ProductMapper {
    /**
     * Query all products
     * @return
     */
    List<Product> selectProductList();
}

1.3 ProductMapper.xml product mapping file

 

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >

<mapper namespace="team.njupt.mapper.ProductMapper">
    <select id="selectProductList" resultType="team.njupt.entity.Product">
        select * from product
    </select>
</mapper>

1.4 db.properties database configuration file

 

driver=com.mysql.jdbc.Driver
url=jdbc:mysql://127.0.0.1:3306/waimai?useUnicode=true&characterEncoding=utf8
username=root
password=xxxxxx

1.5 configuration file for mybatis.xml mybatis

 

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <properties resource="db.properties">
        <!--<property name="username" value="dev_user"/>-->
        <!--<property name="password" value="F2Fa3!33TYyg"/>-->
    </properties>

    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="${driver}"/>
                <property name="url" value="${url}"/>
                <property name="username" value="${username}"/>
                <property name="password" value="${password}"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource="team/njupt/mapper/ProductMapper.xml"/>
    </mappers>
</configuration>

1.6 Main main function

 

public class Main {
    public static void main(String[] args) throws IOException {

        String resource = "mybatis.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        SqlSession sqlSession = sqlSessionFactory.openSession();
        try {
            ProductMapper productMapper = sqlSession.getMapper(ProductMapper.class);
            List<Product> productList = productMapper.selectProductList();
            for (Product product : productList) {
                System.out.printf(product.toString());
            }
        } finally {
            sqlSession.close();
        }
    }
}

2. MyBatis initialization process

2.1 get configuration file

When the system is initialized, the configuration file is read first and parsed into an InputStream

 

String resource = "mybatis.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);

2.2 create SqlSessionFactoryBuilder object

As can be seen from the name of SqlSessionFactoryBuilder, SqlSessionFactoryBuilder is used to create SqlSessionFactory objects.
Take a look at the SqlSessionFactoryBuilder source code:


There are only some overloaded build functions in SqlSessionFactoryBuilder. The input parameters of these build functions are the input stream of MyBatis configuration file, and the return values are SqlSessionFactory. Thus, SqlSessionFactoryBuilder is purely used to create SqlSessionFactory objects through configuration files.

 

2.3 SqlSessionFactory creation process

Let's see how the build function creates SqlSessionFactory objects.

 

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
  try {
    XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
    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.
    }
  }
}

2.3.1 construct XMLConfigBuilder object

The build function first constructs an XMLConfigBuilder object, which is used to parse XML configuration files, as you can roughly guess from its name. Let's take a look at the architecture of XMLConfigBuilder.

 

  • Xmlxxbuilder is used to parse XML configuration files. Different types of xmlxxbuilder are used to parse different parts of MyBatis configuration files. For example, XMLConfigBuilder is used to parse the configuration file of MyBatis, xmlmaperbuilder is used to parse the mapping file (such as ProductMapper.xml mentioned above) in MyBatis, and XMLStatementBuilder is used to parse the SQL statement in the mapping file.

  • These xmlxxbuilders all have a common parent class, BaseBuilder. This parent class maintains a global Configuration object, which is stored as a Configuration object after the Configuration file of MyBatis is parsed.

  • When the XMLConfigBuilder object is created, the Configuration object is initialized, and when the Configuration object is initialized, some aliases are registered in the typeAliasRegistry container of Configuration.

    private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
    super(new Configuration());
    ErrorContext.instance().resource("SQL Mapper Configuration");
    this.configuration.setVariables(props);
    this.parsed = false;
    this.environment = environment;
    this.parser = parser;
    }
    
    public Configuration() {
    typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
    typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class);
    
    typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class);
    typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
    typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class);
    
    typeAliasRegistry.registerAlias("PERPETUAL", PerpetualCache.class);
    typeAliasRegistry.registerAlias("FIFO", FifoCache.class);
    typeAliasRegistry.registerAlias("LRU", LruCache.class);
    typeAliasRegistry.registerAlias("SOFT", SoftCache.class);
    typeAliasRegistry.registerAlias("WEAK", WeakCache.class);
    ......
    }
    

2.3.2 parsing configuration file

After you have the XMLConfigBuilder object, you can use it to parse the configuration file.

 

  private void parseConfiguration(XNode root) {
  try {
    // Resolving < Properties > nodes
    propertiesElement(root.evalNode("properties"));
    // Analyze < Settings > nodes
    Properties settings = settingsAsProperties(root.evalNode("settings"));
    loadCustomVfs(settings);
    // Analyze < typealiases > nodes
    typeAliasesElement(root.evalNode("typeAliases"));
    // Parsing < plugins > nodes
    pluginElement(root.evalNode("plugins"));
    // Analyze < objectfactory > nodes
    objectFactoryElement(root.evalNode("objectFactory"));
    objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
    // Parsing < reflectorfactory > nodes
    reflectorFactoryElement(root.evalNode("reflectorFactory"));
    settingsElement(settings);
    // Analyze < environments > nodes
    environmentsElement(root.evalNode("environments"));
    databaseIdProviderElement(root.evalNode("databaseIdProvider"));
    typeHandlerElement(root.evalNode("typeHandlers"));
    // Parsing < mappers > nodes
    mapperElement(root.evalNode("mappers"));
  } catch (Exception e) {
    throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
  }
}

As you can see from the above code, XMLConfigBuilder will successively parse the properties in the configuration file, such as < Properties >, < Settings >, < environments >, < typealiases >, < plugins >, < mappers >. The following describes the parsing process of the following important attributes.

2.3.2.1 resolution process of < Properties > node

  • The < Properties > node is defined as follows:

    <properties resource="org/mybatis/example/config.properties">
      <property name="username" value="dev_user"/>
      <property name="password" value="F2Fa3!33TYyg"/>
    </properties>
    

 

  • Resolution process of < Properties > node:

    /**
      * @Param context <properties>node
      */
    private void propertiesElement(XNode context) throws Exception {
      if (context != null) {
        // Get all children of < Properties > node
        Properties defaults = context.getChildrenAsProperties();
        // Get the resource property on the < Properties > node
        String resource = context.getStringAttribute("resource");
        // Get the url property on the < Properties > node
        String url = context.getStringAttribute("url");
        // resource and url cannot exist at the same time
        if (resource != null && url != null) {
          throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference.  Please specify one or the other.");
        }
        if (resource != null) {
          // Get the key value pair in the properties file corresponding to the resource property value and add it to the defaults container        
          defaults.putAll(Resources.getResourceAsProperties(resource));
        } else if (url != null) {
          // Get the key value pair in the properties file corresponding to the url property value and add it to the defaults container
          defaults.putAll(Resources.getUrlAsProperties(url));
        }
        // Get the original properties in the configuration and add them to the defaults container
        Properties vars = configuration.getVariables();
        if (vars != null) {
          defaults.putAll(vars);
        }
        parser.setVariables(defaults);
        // Add the defaults container to configuration
        configuration.setVariables(defaults);
      }
    }
    

    If the properties are configured in more than one place, MyBatis will load in the following order:

  • The properties specified in the properties element body are read first.
  • Then read the property file under the class path according to the resource property in the properties element or according to the path specified by the url property, and overwrite the read property with the same name.

Therefore, the properties passed through the method parameters have the highest priority, followed by the configuration file specified in the resource/url property, and the properties specified in the properties property have the lowest priority.

  • Finally, the Properties object that carries all the Properties is stored in the Configuration object.

2.3.2.2 resolution process of < Settings > nodes

  • The < Settings > node is defined as follows:
    <settings>
      <setting name="cacheEnabled" value="true"/>
      <setting name="lazyLoadingEnabled" value="true"/>
      <setting name="multipleResultSetsEnabled" value="true"/>
    </settings>
    
  • The resolution process of < Settings > node:
    The resolution process of < Settings > property is very similar to that of < Properties > property, which will not be covered here. Finally, all the setting properties are stored in the Configuration object.

2.3.2.3 analysis process of < typealiases > attribute

There are two ways to define < typealiases > attributes:

  • Mode 1:
    <typeAliases>
      <typeAlias alias="Author" type="domain.blog.Author"/>
      <typeAlias alias="Blog" type="domain.blog.Blog"/>
    </typeAliases>
    
  • Mode 2:
    <typeAliases>
      <package name="domain.blog"/>
    </typeAliases>
    
    In this way, MyBatis will create an alias for all classes under the specified package, which is the first lowercase class name.

The resolution process of < typealiases > node is as follows:

 

  private void typeAliasesElement(XNode parent) {
  if (parent != null) {
    // Traverse all child nodes under < typealiases >
    for (XNode child : parent.getChildren()) {
      // If the current node is < package >
      if ("package".equals(child.getName())) {
        // Get the name attribute (package name) on < package >
        String typeAliasPackage = child.getStringAttribute("name");
        // Alias all classes under the package and register them in the typeAliasRegistry of configuration          
        configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
      } 
      // If the current node is < typealias >
      else {
        // Get alias and type attributes
        String alias = child.getStringAttribute("alias");
        String type = child.getStringAttribute("type");
        // Register in typeAliasRegistry of configuration
        try {
          Class<?> clazz = Resources.classForName(type);
          if (alias == null) {
            typeAliasRegistry.registerAlias(clazz);
          } else {
            typeAliasRegistry.registerAlias(alias, clazz);
          }
        } catch (ClassNotFoundException e) {
          throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e);
        }
      }
    }
  }
}
  • If the < package > node is defined under the < typealiases > node, MyBatis will give an alias to all classes under the package (with the first letter lowercase as the alias)
  • If the < typealias > node is defined under the < typealiases > node, MyBatis will give the specified alias to the specified class.
  • These aliases are stored in the typeAliasRegistry container of configuration.

2.3.2.4 analysis process of < mappers > nodes

There are four ways to define < mappers > nodes:

  • Mode 1:

 

<mappers>
  <package name="org.mybatis.builder"/>
</mappers>
  • Mode 2:

 

<mappers>
  <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
</mappers>
  • Mode 3:

 

<mappers>
  <mapper url="file:///var/mappers/AuthorMapper.xml"/>
</mappers>
  • Mode 4:

 

<mappers>
  <mapper class="org.mybatis.builder.AuthorMapper"/>
</mappers>

The resolution process of < mappers > node is as follows:

 

  private void mapperElement(XNode parent) throws Exception {
  if (parent != null) {
    // Traverse all child nodes under < mappers >
    for (XNode child : parent.getChildren()) {
      // If the current node is < package >
      if ("package".equals(child.getName())) {
        // Get the name attribute of < package > (the value of the attribute is the package name of the mapper class)
        String mapperPackage = child.getStringAttribute("name");
        // Register all mapper classes under the package into the mapperRegistry container of configuration
        configuration.addMappers(mapperPackage);
      } 
      // If the current node is < mapper >
      else {
        // Get the resource, url and class attributes in turn
        String resource = child.getStringAttribute("resource");
        String url = child.getStringAttribute("url");
        String mapperClass = child.getStringAttribute("class");
        // Parsing resource attribute (path to Mapper.xml file)
        if (resource != null && url == null && mapperClass == null) {
          ErrorContext.instance().resource(resource);
          // Parse Mapper.xml file into input stream
          InputStream inputStream = Resources.getResourceAsStream(resource);
          // Use XMLMapperBuilder to parse Mapper.xml and register Mapper Class in mapperRegistry container of configuration object
          XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
          mapperParser.parse();
        } 
        // Parse url attribute (path to Mapper.xml file)
        else if (resource == null && url != null && mapperClass == null) {
          ErrorContext.instance().resource(url);
          InputStream inputStream = Resources.getUrlAsStream(url);
          XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
          mapperParser.parse();
        } 
        // Resolve class attribute (Mapper Class's fully qualified name)
        else if (resource == null && url == null && mapperClass != null) {
          // Convert the permission name of Mapper Class to Class object
          Class<?> mapperInterface = Resources.classForName(mapperClass);
          // Register in the mapperRegistry container of the configuration object
          configuration.addMapper(mapperInterface);
        } else {
          throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
        }
      }
    }
  }
}
  • MyBatis will traverse all the child nodes under < mappers >. If the currently traversed node is < package >, MyBatis will register all mapper classes under the package into the maperregistry container of configuration.
  • If the current node is < mapper >, the resource, url and class attributes will be obtained in turn, the mapping file will be parsed, and the Mapper Class corresponding to the mapping file will be registered in the maperregistry container of configuration.

The resolution process of < mapper > node is as follows:

 

XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
  • Before parsing, you need to create XMLMapperBuilder first. The creation process is as follows:

    private XMLMapperBuilder(XPathParser parser, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
      // Assign configuration to BaseBuilder
      super(configuration);
      // Create MapperBuilder assistant object (the object is the helper of MapperBuilder)
      this.builderAssistant = new  MapperBuilderAssistant(configuration, resource);
      this.parser = parser;
      this.sqlFragments = sqlFragments;
      this.resource = resource;
    }
    
    • First, the parent class BaseBuilder will be initialized, and the configuration will be assigned to BaseBuilder;
    • Then create mapperbuilder assistant object, which is the helper of XMLMapperBuilder, to assist XMLMapperBuilder to complete some actions of parsing mapping files.
  • When you have XMLMapperBuilder, you can enter the process of parsing < mapper >

    public void parse() {
      // If the current Mapper.xml has not been parsed, start parsing
      // PS: if there is the same < mapper > node under the < mappers > node, there is no need to parse it again
      if (!configuration.isResourceLoaded(resource)) {
        // Analyze < mapper > nodes
        configurationElement(parser.evalNode("/mapper"));
        // Add the Mapper.xml to the LoadedResource container of the configuration, and there is no need to parse it next time
        configuration.addLoadedResource(resource);
        // Register the Mapper Class corresponding to Mapper.xml into the mapperRegistry container of configuration
        bindMapperForNamespace();
      }
    
      parsePendingResultMaps();
      parsePendingCacheRefs();
      parsePendingStatements();
    }
    
  • configurationElement function

    private void configurationElement(XNode context) {
    try {
      // Get the namespace attribute on the < mapper > node, which must exist to indicate who the Mapper Class corresponding to the current mapping file is
      String namespace = context.getStringAttribute("namespace");
      if (namespace == null || namespace.equals("")) {
        throw new BuilderException("Mapper's namespace cannot be empty");
      }
      // Assign the namespace property value to builder assistant
      builderAssistant.setCurrentNamespace(namespace);
      // Parsing < cache ref > nodes
      cacheRefElement(context.evalNode("cache-ref"));
      // Parsing < cache > nodes
      cacheElement(context.evalNode("cache"));
      // Parsing < parametermap > nodes
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      // Parsing < resultmap > nodes
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      // Parsing < SQL > nodes
      sqlElement(context.evalNodes("/mapper/sql"));
      // Parsing sql statements      
      buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
    }
    }
    
  • resultMapElements function
    This function is used to parse all < ResultMap > nodes in the mapping file. These nodes will be parsed into ResultMap objects and stored in the resultMaps container of the Configuration object.

    • The < resultmap > node is defined as follows:
     <resultMap id="userResultMap" type="User">
      <constructor>
         <idArg column="id" javaType="int"/>
         <arg column="username" javaType="String"/>
      </constructor>
      <result property="username" column="user_name"/>
      <result property="password" column="hashed_password"/>
    </resultMap>
    
    • Resolution process of < resultmap > node:
    private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings) throws Exception {
      ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
      // Get id attribute on < resultmap >
      String id = resultMapNode.getStringAttribute("id",
        resultMapNode.getValueBasedIdentifier());
      // Get the type attribute on < resultMap > (that is, the return value type of resultMap)
      String type = resultMapNode.getStringAttribute("type",
        resultMapNode.getStringAttribute("ofType",
            resultMapNode.getStringAttribute("resultType",
                resultMapNode.getStringAttribute("javaType"))));
      // Get extensions property
      String extend = resultMapNode.getStringAttribute("extends");
      // Get autoMapping property
      Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
      // Convert the return value type of resultMap to Class object
      Class<?> typeClass = resolveClass(type);
      Discriminator discriminator = null;
      // Resultmaps are used to store all child nodes under < resultmap >
      List<ResultMapping> resultMappings = new ArrayList<ResultMapping>();
      resultMappings.addAll(additionalResultMappings);
      // Get and traverse all child nodes under < resultmap >
      List<XNode> resultChildren = resultMapNode.getChildren();
      for (XNode resultChild : resultChildren) {
        // If the current node is < constructor >, add its children to resultMappings
        if ("constructor".equals(resultChild.getName())) {
          processConstructorElement(resultChild, typeClass, resultMappings);
        }
        // If the current node is < discriminator >, make condition judgment and add the hit child node to resultMappings
        else if ("discriminator".equals(resultChild.getName())) {
          discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
        }
        // If the current node is < result >, < Association >, < Collection >, add it to resultMappings
        else {
          // PS:flags is only used to distinguish whether the current node is < ID > or < idarg >, because the property name of these two nodes is name, while the property name of other nodes is property
          List<ResultFlag> flags = new ArrayList<ResultFlag>();
          if ("id".equals(resultChild.getName())) {
            flags.add(ResultFlag.ID);
          }
          resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
        }
      }
      // The function of ResultMapResolver is to generate a ResultMap object and add it to the resultMaps container of the Configuration object (see the following for the specific process)
      ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
      try {
        return resultMapResolver.resolve();
      } catch (IncompleteElementException  e) {
        configuration.addIncompleteResultMap(resultMapResolver);
        throw e;
      }
    }
    

    ResultMapResolver is a pure class with only one function resolve, which is used to construct the ResultMap object and store it in the resultMaps container of the Configuration object. This process is completed by MapperBuilderAssistant.addResultMap.

    public ResultMap resolve() {
      return assistant.addResultMap(this.id, this.type, this.extend,  this.discriminator, this.resultMappings, this.autoMapping);
    }
    
  • sqlElement function
    This function is used to parse all < sql > nodes in the mapping file, and store these nodes in the sqlFragments container of the XMLMapperBuilder object corresponding to the current mapping file, which is used for parsing sql statements.

    <sql id="userColumns"> ${alias}.id,${alias}.username,${alias}.password </sql>
    
  • buildStatementFromContext function
    This function parses the sql statements in the mapping file into MappedStatement objects, and there is mapedstatements for configuration.

2.3.3 create SqlSessionFactory object

 

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
  try {
    XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
    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.
    }
  }
}

Look back at the build function of SqlSessionFactory. I just talked about it for a long time. I introduced the process of XMLConfigBuilder parsing the mapping file. After parsing, the parser.parse() function will return a configuration object containing the parsing result of the mapping file. Then, this object will be passed as a parameter to another build function, as follows:

 

  public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
  }

This function takes configuration as an argument and creates a DefaultSqlSessionFactory object.
DefaultSqlSessionFactory is an implementation class of the interface SqlSessionFactory. The architecture of SqlSessionFactory is shown in the following figure:

 

At this time, SqlSessionFactory is created!

Published 31 original articles, won praise 18, visited 10000+
Private letter follow

Posted by youngsei on Sat, 29 Feb 2020 23:25:01 -0800