“Hand tearing Mybatis source code” 02 – Load configuration file

Load configuration file

Get input stream

  1. MyBatis configuration document hierarchy
  2. First, start by reading in to see how the configuration file is loaded, and now make a breakpoint from here
public class MybatisTest {<!-- -->
  @Test
  public void test1() throws IOException {<!-- -->
     // 1. Load the configuration file through the class loader, load it into a byte input stream, and store it in memory
    // Note: the configuration file is not parsed
    InputStream resourceAsStream = Resources.getResourceAsStream("SqlMapConfig.xml");
    ...
  }
}
  1. Then it is found that a classLoaderWrapper: ClassLoader wrapper will read the data source and return the input stream
public class Resources {<!-- -->

  private static ClassLoaderWrapper classLoaderWrapper = new ClassLoaderWrapper();
  ...
  public static InputStream getResourceAsStream(ClassLoader loader, String resource) throws IOException {<!-- -->
InputStream in = classLoaderWrapper.getResourceAsStream(resource, loader);
    ...
  }
}
  1. If you continue to enter, you will find that the corresponding loader will be returned through getClassLoaders(classLoader) before reading. The current classLoader is null
public class ClassLoaderWrapper {<!-- -->
  // default class loader
  ClassLoader defaultClassLoader;
  // system class loader
  ClassLoader systemClassLoader;
  ...
  public InputStream getResourceAsStream(String resource, ClassLoader classLoader) {<!-- -->
    return getResourceAsStream(resource, getClassLoaders(classLoader));
  }
}
  1. The getClassLoaders(classLoader) code is as follows
public class ClassLoaderWrapper {<!-- -->
  ...
  ClassLoader[] getClassLoaders(ClassLoader classLoader) {<!-- -->
    return new ClassLoader[]{<!-- -->
        // The class loader specified by the parameter
        classLoader,
        // The default loader specified by the system
        defaultClassLoader,
        // The class loader bound by the current thread
        Thread.currentThread().getContextClassLoader(),
        // The class loader used by the current class
        getClass().getClassLoader(),
        systemClassLoader};
  }
}
  1. After getting the loader set, the read source method is being called, and finally the input stream is returned. The judgment method is as follows
public class ClassLoaderWrapper {<!-- -->
  ...
  InputStream getResourceAsStream(String resource, ClassLoader[] classLoader) {<!-- -->
    // loop ClassLoader, read the file through the specified or default ClassLoader
    for (ClassLoader cl : classLoader) {<!-- -->
      // if the loader is not empty
      if (null != cl) {<!-- -->
        // try to read with this loader first
        InputStream returnValue = cl. getResourceAsStream(resource);

        // If there is no result returned, it may be due to the lack of a "/", read it after adding it
        if (null == returnValue) {<!-- -->
          returnValue = cl. getResourceAsStream("/" + resource);
        }
        
        if (null != returnValue) {<!-- -->
          // If read, terminate the loop and return the result
          return returnValue;
        }
      }
    }
    return null;
  }
}
  1. When the input stream is successfully read, it returns
public class Resources {<!-- -->
  ...
  public static InputStream getResourceAsStream(ClassLoader loader, String resource) throws IOException {<!-- -->
InputStream in = classLoaderWrapper.getResourceAsStream(resource, loader);
// After finding the input stream, it returns
    if (in == null) {<!-- -->
      throw new IOException("Could not find resource " + resource);
    }
    return in;
  }
}
  1. Question: Why is the ClassLoaderWrapper wrapper required instead of using ClassLoader directly?
  • Because it needs to be used to determine whether the resource path is empty
  • Whether the input stream loaded according to the path is empty
  • Is the used class loader empty
  • exception handling
  1. Summary
  • Get input stream

Parse configuration file

  1. First, build sqlSessionFactory through SqlSessionFactoryBuilder, we can know that sqlSessionFactory must contain all configuration file information
public class MybatisTest {<!-- -->

  @Test
  public void test1() throws IOException {<!-- -->
    ...
    // 2. (1) Parsed the configuration file and encapsulated the configuration object
    // (2) Create the DefaultSqlSessionFactory factory object
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
    ...
  }
}
  1. The first is to call the build() method and find that our environment input is null
public class SqlSessionFactoryBuilder {<!-- -->
  ...
  public SqlSessionFactory build(InputStream inputStream) {<!-- -->
    return build(inputStream, null, null);
  }
  // environment The id value of the running environment information
  public SqlSessionFactory build(InputStream inputStream, String environment) {<!-- -->
    return build(inputStream, environment, null);
  }
  ...
}
  • In fact, normally, we can configure the environment parameter by ourselves, because we know that Mybatis supports multi-environment configuration, and the definition is in sqlMapConfig.xml
<configuration>
  ...
    <environment id="development">
...
    </environment>
  ...
</configuration>
  1. Then you need to use XMLConfigBuilder to create the input stream into a document object
public class SqlSessionFactoryBuilder {<!-- -->
  ...
  public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {<!-- -->
    try {<!-- -->
      // XMLConfigBuilder: used to parse XML configuration files
      // Use the builder pattern:
      // - benefits: reduce coupling, separate creation of complex objects
      // 1. Create an XPathParser parser object, and parse it into a document object according to the inputStream
      // 2. Create a global configuration object Configuration object
      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
      ...
}
}

Note: For objects that return at least 4 parameters, it is best to use the builder pattern

  1. In the constructor of XMLConfigBuilder, an XPathParser object will be created, which will contain a document object, which is XPathParser< /strong> In fact, it will only be responsible for parsing configuration files
public class XMLConfigBuilder extends BaseBuilder {<!-- -->
  ...
  protected final Configuration configuration; // This is the attribute in BaseBuilder
  private boolean parsed;
  private final XPathParser parser;
  private String environment;
  ...
  public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {<!-- -->
    // XPathParser is based on Java XPath parser for parsing configuration files in MyBatis
    this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
  }
}
  • createDocument() is too detailed, so let’s not pay attention to it. Anyway, in the end, get the Document object through dom analysis, and then assign it to the member variable
public class XPathParser {<!-- -->
  ...
  private final Document document;
  ...
  public XPathParser(InputStream inputStream, boolean validation, Properties variables, EntityResolver entityResolver) {<!-- -->
    commonConstructor(validation, variables, entityResolver);
    // parse the XML document into a Document object
    this.document = createDocument(new InputSource(inputStream));
  }
}
  1. Then start to initialize XMLConfigBuilder, first create a Configuration object
public class XMLConfigBuilder extends BaseBuilder {<!-- -->
  private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {<!-- -->
    // Create a global Configuration object and register some aliases of Mybatis internal related classes through TypeAliasRegistry
    super(new Configuration());
    ...
  }
}
  1. Take a brief look at the construction method of Configuration, and you can see that a bunch of aliases have been registered
public class Configuration {<!-- -->
  ...
  protected environment environment;
  // Type register, used for the mapping of input and output parameters in the execution of sql statements and various configurations in the mybatis-config file
  // For example, use shorthand for <transactionManager type="JDBC"/><dataSource type="POOLED">
  protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();
  ...
  // Why use JDBC alias to use jdbc transaction management? sqlConfig.xml, the reason is here
  public Configuration() {<!-- -->
   
    // TypeAliasRegistry (Type Alias Registry)
    // Register the alias of the transaction factory
    typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
    typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class);
    ...
  }
}
  • Question: Why can jdbc transaction management be used with JDBC aliases?
    • The reason is here, because in sqlMapConfig.xml , we specified which factory to use for assignment, and the type field in it is
<configuration>
...
    <environment id="development">
      <!-- Use jdbc transaction management -->
      <transactionManager type="JDBC" />
      <!-- database connection pool -->
      <dataSource type="POOLED">
...
      </dataSource>
    </environment>
...
</configuration>
  1. After returning to build() to get XMLConfigBuilder, the analysis is actually performed
public class SqlSessionFactoryBuilder {<!-- -->
  ...
  public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {<!-- -->
   
      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);

      // parser.parse(): Use XPATH to parse the XML configuration file, and encapsulate the configuration file into the Configuration object
      // Return the DefaultSqlSessionFactory object, which owns the Configuration object (encapsulating configuration file information)
      // parse(): The configuration file is parsed
      return build(parser. parse());
  }
}

XMLConfigBuilder parses the Document to get the configuration and store it in configuration

  1. Continue to enter from parser.parse() and you will see that parse() will only parse once
public class XMLConfigBuilder extends BaseBuilder {<!-- -->
  ...
  public Configuration parse() {<!-- -->
    // Initially false, interest will only be applied once
    if (parsed) {<!-- -->
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;

    // parser.evalNode("/configuration"): parse the configuration root node through the XPATH parser
    // Start parsing from the configuration root node, and finally encapsulate the parsed content into the Configuration object
    parseConfiguration(parser.evalNode("/configuration"));

    return configuration;
  }
}
  1. Then use evalNode() to parse all the content under /configuration into Xnode
public class XPathParser {<!-- -->
  ...
  public XNode evalNode(String expression) {<!-- -->
    // According to the XPATH syntax, get the specified node
    return evalNode(document, expression);
  }

  public XNode evalNode(Object root, String expression) {<!-- --> // Get the content to be selected and convert it to xnode
    Node node = (Node) evaluate(expression, root, XPathConstants.NODE);
    if (node == null) {<!-- -->
      return null;
    }
    return new XNode(this, node, variables);
  }
  ...
}
  • The content contained in Xnode is all the content under sqlMapConfig.xml tag
  1. After entering parseConfiguration(), you will find a lot of **Element() functions, which are very important. These are all under the /configuration node The content is parsed and stored in the configuration object
public class XMLConfigBuilder extends BaseBuilder {<!-- -->
...
private void parseConfiguration(XNode root) {<!-- -->
    try {<!-- -->
      // parse the </properties> tag
      propertiesElement(root.evalNode("properties"));
      // parse the </settings> tag
      Properties settings = settingsAsProperties(root. evalNode("settings"));
      loadCustomVfs(settings);
      loadCustomLogImpl(settings);
      // parse the </typeAliases> tag
      typeAliasesElement(root. evalNode("typeAliases"));
      // parse the </plugins> tag
      pluginElement(root.evalNode("plugins"));
      // parse the </objectFactory> tag
      objectFactoryElement(root. evalNode("objectFactory"));
      // parse the </objectWrapperFactory> tag
      objectWrapperFactoryElement(root. evalNode("objectWrapperFactory"));
      // parse the </reflectorFactory> tag
      reflectorFactoryElement(root. evalNode("reflectorFactory"));
      settingsElement(settings);
      // parse the </environments> tag
      environmentsElement(root. evalNode("environments"));
      // parse the </databaseIdProvider> tag
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      // parse the </typeHandlers> tag
      typeHandlerElement(root. evalNode("typeHandlers"))
      // Parse the </mappers> tag to load the main entry of the mapping file process
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {<!-- -->
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }
}
  1. Skip to environmentsElement() to see
  • First it will extract the /environments element from the Xnode node, and then determine whether there is content
  • Then judge whether the environment parameter has been passed in, if not, get the default attribute environment to be activated from /environments
  • After getting the environment, start to create each factory class according to the type attribute defined under each tag, and finally encapsulate it into configuration
public class XMLConfigBuilder extends BaseBuilder {<!-- -->
  ...
  private void environmentsElement(XNode context) throws Exception {<!-- -->
    // If environments are defined
    if (context != null) {<!-- -->
      // If the method parameter does not pass the environment, then parse the sqlMapConfig.xml
      if (environment == null) {<!-- -->
        // <environments default="development">
        environment = context. getStringAttribute("default");
      }

      // Traversing and parsing the environment nodes to find the effective environment
      for (XNode child : context.getChildren()) {<!-- -->
        // Get the id attribute value
        String id = child. getStringAttribute("id");
        // Determine whether the id and environment values are equal
        if (isSpecifiedEnvironment(id)) {<!-- -->

          // <transactionManager type="JDBC" /> Create a transaction factory object
          TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));

          // <dataSource type="POOLED"> Create a data source object
          DataSourceFactory dsFactory = dataSourceElement(child. evalNode("dataSource"));
          DataSource dataSource = dsFactory. getDataSource();
          Environment.Builder environmentBuilder = new Environment.Builder(id)
              .transactionFactory(txFactory)
              .dataSource(dataSource);

          // Save Environment to configuraion
          configuration.setEnvironment(environmentBuilder.build());
          break;
        }
      }
    }
  }
}
  • Like transactionManagerElement(), the function of the type attribute is that there is already a corresponding factory class in the alias, and it can be initialized directly
public class XMLConfigBuilder extends BaseBuilder {<!-- -->
  ...
  private TransactionFactory transactionManagerElement(XNode context) throws Exception {<!-- -->
    if (context != null) {<!-- -->
      String type = context. getStringAttribute("type");
      Properties props = context. getChildrenAsProperties();
      TransactionFactory factory = (TransactionFactory) resolveClass(type). getDeclaredConstructor(). newInstance();
      factory. setProperties(props);
      return factory;
    }
    throw new BuilderException("Environment declaration requires a TransactionFactory.");
  }
}
  1. Let’s take a look at mapperElement() parsing /mappers
  • It will be processed according to the sub-tag /package or /mapper, let’s take a brief look at the analysis of /mapper
  • It will parse out resource attribute, url attribute, class attribute from /mapper, these three are mutual Rejected, only one of them can be used
  • For parsing like resource attribute, you also need to use XMLMapperBuilder to specifically parse
public class XMLConfigBuilder extends BaseBuilder {<!-- -->
  ...
  private void mapperElement(XNode parent) throws Exception {<!-- -->
    if (parent != null) {<!-- -->
      // Get the subtags of the <mappers> tag
      for (XNode child : parent. getChildren()) {<!-- -->
        // <package> subtag
        if ("package".equals(child.getName())) {<!-- -->
...
        } else {<!-- -->// <mapper> subtag
          // Get the resource attribute of the <mapper> subtag
          String resource = child. getStringAttribute("resource");
          // Get the url attribute of the <mapper> subtag
          String url = child. getStringAttribute("url");
          // Get the class attribute of the <mapper> subtag
          String mapperClass = child. getStringAttribute("class");
          // they are mutually exclusive
          if (resource != null & amp; & amp; url == null & amp; & amp; mapperClass == null) {<!-- -->
            ErrorContext.instance().resource(resource);
            try(InputStream inputStream = Resources. getResourceAsStream(resource)) {<!-- -->
              // Specially used to parse mapper mapping files
              XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration. getSqlFragments());
              // Parse the mapper mapping file through XMLMapperBuilder
              mapperParser. parse();
            }
          } else if (resource == null & amp; & amp; url != null & amp; & amp; mapperClass == null) {<!-- -->
            ErrorContext.instance().resource(url);
            try(InputStream inputStream = Resources. getUrlAsStream(url)){<!-- -->
              XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration. getSqlFragments());
              // Parse the mapper mapping file through XMLMapperBuilder
              mapperParser. parse();
            }
          } else if (resource == null & amp; & amp; url == null & amp; & amp; mapperClass != null) {<!-- -->
            Class<?> mapperInterface = Resources. classForName(mapperClass);
            // Store the specified mapper interface and its proxy object in a Map collection, the key is the mapper interface type, and the value is the proxy object factory
            configuration.addMapper(mapperInterface);
          } else {<!-- -->
            throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
          }
        }
      }
    }
  }
  ...
}
  1. Summary
  • Parsing configuration files
  • Get SqlSessionFactory
syntaxbug.com © 2021 All Rights Reserved.