MyBatis - loading mechanism for global configuration files

Keywords: Database MySQL Mybatis Redis

For code debugging, we can use any test code in the previous chapter as the Debug carrier. In this chapter, we actually study these two codes:

    InputStream xml = Resources.getResourceAsStream("mybatis-config.xml");
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(xml);

That is, how to load the MyBatis global configuration file and how to build it from the global configuration file   SqlSessionFactory  .

1. Loading of global configuration file

First, let's look at the loading of the configuration file   Resources.getResourceAsStream   The method can only be guessed from the method name. It should be with the help of class loader. Let's take a quick look at the source code:

public static InputStream getResourceAsStream(String resource) throws IOException {
    return getResourceAsStream(null, resource);

public static InputStream getResourceAsStream(ClassLoader loader, String resource) throws IOException {
    InputStream in = classLoaderWrapper.getResourceAsStream(resource, loader);
    if (in == null) {
        throw new IOException("Could not find resource " + resource);
    return in;

This place doesn't seem to come in   ClassLoader  , Actually take   ClassLoader   In another location:

ClassLoader[] getClassLoaders(ClassLoader classLoader) {
    return new ClassLoader[]{

Take so much at once   ClassLoader  , What's the picture? Obviously, it wants to go one by one   ClassLoader   Try it all. As long as you can get the resources, it's OK  . The following is the actual use   ClassLoader   Load the underlying source code of the global configuration file:

InputStream getResourceAsStream(String resource, ClassLoader[] classLoader) {
    for (ClassLoader cl : classLoader) {
        if (null != cl) {
            // try to find the resource as passed
            InputStream returnValue = cl.getResourceAsStream(resource);
            // now, some class loaders want this leading "/", so we'll add it and try again if we didn't find the resource
            if (null == returnValue) {
                returnValue = cl.getResourceAsStream("/" + resource);
            if (null != returnValue) {
                return returnValue;
    return null;

The logic is very clear, so using this method, you can easily obtain the binary stream of the global configuration file.

2. Parse configuration file

The following is the parsing process. Our test code directly creates a new one   SqlSessionFactoryBuilder  , Subsequent adjustment   build   Method to construct   SqlSessionFactory  :

SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(xml);

and   build   The method finally comes to an overloaded method with three parameters:

public SqlSessionFactory build(InputStream inputStream) {
    return build(inputStream, null, null);

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
        XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
        return build(parser.parse());
    } // catch finally ......

It can be seen that the first underlying core component used here is   XMLConfigBuilder  , xml based configuration Builder (embodiment of builder pattern). And this   XMLConfigBuilder  , First inherited a family called   BaseBuilder   What you need:

public class XMLConfigBuilder extends BaseBuilder {
    // ......

Let's first study the construction of these two classes.

2.1 BaseBuilder

BaseBuilder   As its name implies, it is a basic constructor. Its initialization needs to pass in the global configuration object of MyBatis   Configuration  :

public abstract class BaseBuilder {
    protected final Configuration configuration;
    protected final TypeAliasRegistry typeAliasRegistry;
    protected final TypeHandlerRegistry typeHandlerRegistry;

    public BaseBuilder(Configuration configuration) {
        this.configuration = configuration;
        this.typeAliasRegistry = this.configuration.getTypeAliasRegistry();
        this.typeHandlerRegistry = this.configuration.getTypeHandlerRegistry();

this   Configuration   As we all know, after MyBatis is initialized, all configuration items, Mapper and statement will be stored here. It's OK if you can remember.

Take a general look at the defined methods. Most of them are methods such as parsing and obtaining, which looks more like providing basic tool class method support: (the following is a reference)   BaseBuilder   Two methods defined in)

protected Boolean booleanValueOf(String value, Boolean defaultValue) {
    return value == null ? defaultValue : Boolean.valueOf(value);

protected JdbcType resolveJdbcType(String alias) {
    if (alias == null) {
        return null;
    try {
        return JdbcType.valueOf(alias);
    } catch (IllegalArgumentException e) {
        throw new BuilderException("Error resolving JdbcType. Cause: " + e, e);

In this way, the core processing logic is not   BaseBuilder   In, we return to the implementation class   XMLConfigBuilder   Yes.

2.2 XMLConfigBuilder

As usual, let's take a look at the internal members and the definition of the construction method.

2.2.1 definition of construction method

public class XMLConfigBuilder extends BaseBuilder {

    private boolean parsed;
    private final XPathParser parser;
    private String environment;
    private final ReflectorFactory localReflectorFactory = new DefaultReflectorFactory();
    public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
        // Notice that an XPath parser is added here
        this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);

    private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
        super(new Configuration());
        ErrorContext.instance().resource("SQL Mapper Configuration");
        this.parsed = false;
        this.environment = environment;
        this.parser = parser;

There are many overloaded construction methods in the source code, and the above is   SqlSessionFactoryBuilder   Invoked in   build   Method, and finally the three parameter constructor in the above code is called. This constructor calls the overloaded constructor below. Note that in the source code, its overloaded construction method is no longer required   InputStream  , But constructed a   XPathParser  , Although we haven't seen this guy, we can probably guess that it is a parser for parsing xml global configuration files. We don't need to study this thing first. Just look by when we use it below.

In addition, in the lowest construction method, it can be found that   Configuration   The object is generated by new here, and it is very simple. It is generated by using the null parameter construction method new, so you can first understand one thing: if you really want us to operate by ourselves, initialize MyBatis   Configuration  , It's not that we can't. It doesn't matter if we operate by ourselves.

The rest, in the lowest construction method, are ordinary assignment operations, and there is nothing to say.

2.2.2 core parse method

The next code is   return build(parser.parse());   Well, this line of code is actually two methods. First, it calls   XMLConfigBuilder   of   parse   Methods, generating   Configuration  , After that   SqlSessionFactoryBuilder   of   build   method. Let's look at it first   XMLConfigBuilder   How the configuration file is parsed.

public Configuration parse() {
    if (parsed) {
        throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    parsed = true;
    return configuration;

The core method is still in the middle   parseConfiguration   Method, but before that, let's focus on   parser.evalNode("/configuration")   This action. XPathParser#evalNode

XPathParser   The function of is to convert the xml configuration file to   Document   Object and provide the corresponding xml tag node. its   evalNode   Method is used to obtain the tag specified in xml:

public XNode evalNode(String expression) {
    return evalNode(document, expression);

public XNode evalNode(Object root, String expression) {
    Node node = (Node) evaluate(expression, root, XPathConstants.NODE);
    if (node == null) {
        return null;
    return new XNode(this, node, variables);

private Object evaluate(String expression, Object root, QName returnType) {
    try {
        // Parsing xml using javax's XPath
        return xpath.evaluate(expression, root, returnType);
    } catch (Exception e) {
        throw new BuilderException("Error evaluating XPath.  Cause: " + e, e);

as for   XPath   How to do it internally is the mechanism of xml parsing. We don't care. The only thing we should care about is the middle   evalNode   Method analysis   Node   After that, the return is encapsulated into a   XNode  . What is it? intent of node encapsulation as XNode

Watch   XNode   The construction method of it is passed into a   variables   Object, and this   variables   In fact, they are those defined in the global configuration file  < properties>   Labels, and introduced  . properties   File. But why are these configuration attribute values involved? Let's recall that we wrote such code in the global configuration file before:

<environments default="development">
    <environment id="development">
        <transactionManager type="JDBC"/>
        <dataSource type="POOLED">
            <property name="driver" value="${jdbc.driverClassName}"/>
            <property name="url" value="${jdbc.url}"/>
            <property name="username" value="${jdbc.username}"/>
            <property name="password" value="${jdbc.password}"/>

If you really want to take the driver attribute, you must not  $ {jdbc.driverClassName}   Take it out. You have to dynamically replace the value of the configuration attribute. But javax is native   Node   I can't do this, so MyBatis is based on javax   Node   Encapsulates a   XNode  , Combined   XPathParser  , You can achieve the effect of dynamically parsing the configuration attribute value.

If it is explained in this way, many small partners will look at a loss. For example, the booklet.

Take the above xml as an example, when we get  < dataSource>   After the tag is, the   driver  , url   And other attributes, and the values of these attributes are placeholders. When parsing, MyBatis will first parse them as usual  < property>   Tag, and then get the value attribute (for example, parsing)   driver   Property, so the returned value is  $ {jdbc.driverClassName}  ), Then! It will use a placeholder parser to parse the placeholder and   properties>   Replace the configuration attribute defined / loaded in the tag with the corresponding attribute value. Through this step, ${JDBC. Driverclassname}   Is replaced by   com.mysql.jdbc.Driver  , In this way, the resolution of dynamic configuration attribute value is realized.

Is it easier to understand after this explanation? Let's look at the source code:

// XNode
public String evalString(String expression) {
    // If you don't parse it yourself, delegate XPathParser to parse it
    return xpathParser.evalString(node, expression);

// XPathParser
public String evalString(Object root, String expression) {
    // First take the plaintext attribute value from the label
    String result = (String) evaluate(expression, root, XPathConstants.STRING);
    // The placeholder is handled by the placeholder service parser and replaced with the real configuration value
    result = PropertyParser.parse(result, variables);
    return result;

The processing idea is clear at a glance, so we can understand why MyBatis chose to seal an additional layer   XNode  , Not the one that uses javax directly   Node   Let's go.

2.2.3 parseConfiguration

Go back to the top   parse   Method, parse   The essence of method is analysis  < configuration>   The content of the tag, so let's enter   parseConfiguration   Method:

private void parseConfiguration(XNode root) {
    try {
        // issue #117 read properties first
        Properties settings = settingsAsProperties(root.evalNode("settings"));
        // read it after objectFactory and objectWrapperFactory issue #631
    } catch (Exception e) {
        throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);

Ah, ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah ah? This is too simple! OK, now that we have excavated this place, we can take a look one by one. propertiesElement - parsing properties

The first thing to analyze is  < properties>   Tag, which will parse the internal definition  < property>  , And configured   resource  , url   Attributes: (key comments have been marked in the source code)

private void propertiesElement(XNode context) throws Exception {
    if (context != null) {
        // Load the internally defined < property > first
        Properties defaults = context.getChildrenAsProperties();
        String resource = context.getStringAttribute("resource");
        String url = context.getStringAttribute("url");
        if (resource != null && url != null) {
            // You can't have both
            // throw ex ......
        if (resource != null) {
        } else if (url != null) {
        // Configuration property values loaded by the programmer
        Properties vars = configuration.getVariables();
        if (vars != null) {
        // Put the configuration attribute value into the parser and global configuration

After reading the whole source code, you can find the whole loading process, which is the process we explained in the previous chapter. Moreover, through the daily reading of the source code, we can understand why the priority of configuration is the highest in programming, followed by the properties file, and the lowest defined in the configuration file.

This method is very simple. Notes are also marked in the source code, so there is no more explanation in the booklet. settingsAsProperties - add in configuration item

Here is the analysis  < settings>   Tag. The parsing of this tag involves 3 lines of code:

    Properties settings = settingsAsProperties(root.evalNode("settings"));

It is not difficult to understand that this operation is obviously  < settings>   The label is configured line by line and encapsulated as one   Properties  , Then deal with the VFS and Log components in addition. The contents of VFS will not be mentioned for the time being. Let's take a look at how the underlying layer handles the configuration of Log components:

private void loadCustomLogImpl(Properties props) {
    Class<? extends Log> logImpl = resolveClass(props.getProperty("logImpl"));

Oh, such a simple logic? It is taken directly from the configuration   logImpl   And then set to   Configuration   It's over in, and this   resolveClass   Method is actually resolved by alias:

// BaseBuilder
protected <T> Class<? extends T> resolveClass(String alias) {
    if (alias == null) {
        return null;
    try {
        return resolveAlias(alias);
    } // catch ......

protected <T> Class<? extends T> resolveAlias(String alias) {
    return typeAliasRegistry.resolveAlias(alias);

And the registration of these aliases, as early as   Configuration   When it is created, it is all initialized:

public Configuration() {
    typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
    typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class);

    // ......

    typeAliasRegistry.registerAlias("SLF4J", Slf4jImpl.class);
    typeAliasRegistry.registerAlias("COMMONS_LOGGING", JakartaCommonsLoggingImpl.class);
    typeAliasRegistry.registerAlias("LOG4J", Log4jImpl.class);
    typeAliasRegistry.registerAlias("LOG4J2", Log4j2Impl.class);
    typeAliasRegistry.registerAlias("JDK_LOGGING", Jdk14LoggingImpl.class);
    typeAliasRegistry.registerAlias("STDOUT_LOGGING", StdOutImpl.class);
    typeAliasRegistry.registerAlias("NO_LOGGING", NoLoggingImpl.class);

    // ......

so when we configure those setting configuration items, the content that can be filled in actually refers to the alias predetermined by MyBatis here (which can also explain why we configure them in settings   log4j   It doesn't work well. It must be configured   LOG4J   Can). typeAliasesElement - register type alias

OK, the next one to parse is   typeAliases   Label, we know it can be used  < package>   Direct scanning can also be used  < typeAlias>   Directly declare an alias for the specified type. The bottom layer deals with these two situations respectively:

private void typeAliasesElement(XNode parent) {
    if (parent != null) {
        for (XNode child : parent.getChildren()) {
            // Specify alias for package scan processing package
            if ("package".equals(child.getName())) {
                String typeAliasPackage = child.getStringAttribute("name");
                // Note that registerAliases is called to register a group
            } else {
                // Handles the typeAlias label definition on a case by case basis
                String alias = child.getStringAttribute("alias");
                String type = child.getStringAttribute("type");
                try {
                    Class<?> clazz = Resources.classForName(type);
                    if (alias == null) {
                    } else {
                        typeAliasRegistry.registerAlias(alias, clazz);
                } catch (ClassNotFoundException e) {
                    throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e);

Note that in the source code, if it is processing  < package>   The method called here is also different for the package scanning declared by the tag! Enter into   registerAliases   In the method, we will find that one will be used here   ResolverUtil   Tool class to scan all classes (the parent class is   Object  ), After the scanning is completed, in the following for loop, judge whether these classes are ordinary classes (non interface, non anonymous inner class, non inner class). If so, register the alias.

public void registerAliases(String packageName) {
    registerAliases(packageName, Object.class);

public void registerAliases(String packageName, Class<?> superType) {
    ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
    // The above passed in is Object
    // Note that this scanning action is a full-level scanning, which will scan the sub packages
    resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
    Set<Class<? extends Class<?>>> typeSet = resolverUtil.getClasses();
    for (Class<?> type : typeSet) {
        // Ignore inner classes and interfaces (including
        // Skip also inner classes. See issue #6
        if (!type.isAnonymousClass() && !type.isInterface() && !type.isMemberClass()) {

Note! There is a full-level package scanning action! Therefore, the configured package actually scans all classes under the specified package and its sub packages, and registers them with alias aliases. pluginElement - registered plug-in

The next thing to register is   plugins   Plug in. The code logic is simple:

private void pluginElement(XNode parent) throws Exception {
    if (parent != null) {
        for (XNode child : parent.getChildren()) {
            String interceptor = child.getStringAttribute("interceptor");
            Properties properties = child.getChildrenAsProperties();
            // Create interceptor object directly
            Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
            // Attribute assignment of interceptor

On the whole, it simply creates interceptors and registers them in the global Configuration. Of course, one thing we should be aware of: interceptors are created by MyBatis. If we want to integrate MyBatis with Spring and want Spring to manage MyBatis interceptors, it seems unrealistic. register a bunch of factories

The following three lines of code register some factories:


The registration as like as two peas of the 3 Factory is almost the same at the bottom.   ObjectFactory   For example:

private void objectFactoryElement(XNode context) throws Exception {
    if (context != null) {
        String type = context.getStringAttribute("type");
        Properties properties = context.getChildrenAsProperties();
        ObjectFactory factory = (ObjectFactory) resolveClass(type).getDeclaredConstructor().newInstance();

As like as two peas, the logic of interceptor is almost the same as that above, so we know all these logic OK. settingsElement - application configuration item

The next action is to apply the configuration items initialized before. This method is long and wide (each line of code is so long! I really don't want to paste them all), so we only intercept a few lines of code for a symbolic look:

private void settingsElement(Properties props) {
    // ......
    configuration.setCacheEnabled(booleanValueOf(props.getProperty("cacheEnabled"), true));
    configuration.setProxyFactory((ProxyFactory) createInstance(props.getProperty("proxyFactory")));
    configuration.setLazyLoadingEnabled(booleanValueOf(props.getProperty("lazyLoadingEnabled"), false));
    // ......

emmmm, come on, isn't that what's in the global configuration file  < settings>   All applied to the global   Configuration   Among the objects, there's no mystery. Just scan it quickly. environmentsElement - data source environment configuration

The following is the part of parsing the database environment configuration, because there are nested tags  < transactionManager>   And  < dataSource>  , So the source code here will be a little more complicated (just a little):

private void environmentsElement(XNode context) throws Exception {
    if (context != null) {
        if (environment == null) {
            // Get the default database environment configuration ID from default
            environment = context.getStringAttribute("default");
        for (XNode child : context.getChildren()) {
            String id = child.getStringAttribute("id");
            // Only the default database environment configuration will be constructed
            if (isSpecifiedEnvironment(id)) {
                // Resolve the transactionManager tag to generate TransactionFactory
                TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
                // Parse datasource tag to generate datasource
                DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
                DataSource dataSource = dsFactory.getDataSource();
                // Simple builder to construct Environment object
                Environment.Builder environmentBuilder = new Environment.Builder(id)

Reading the whole source code, the idea is still very clear. What it does is to load the transaction manager and the configuration of the data source and construct it   Environment   Object, and resolve when it's done   transactionManager   Labels, and   dataSource   The logic of the label is almost identical to the pile of factories analyzed above, so the source code of the booklet will not be pasted repeatedly. Just follow the IDE to turn over the source code.

In addition, Environment   The structure of is just a combination of the above   TransactionFactory   And   DataSource  :

public final class Environment {
    private final String id;
    private final TransactionFactory transactionFactory;
    private final DataSource dataSource;
    // ......
} databaseIdProviderElement - database vendor identification resolution

Next is  < databaseIdProvider>   For tag parsing, we say that if you want to use it, you can declare "DB_VENDOR", and then define the alias according to different database manufacturers. It is not difficult to understand this logic reflected in the source code:

private void databaseIdProviderElement(XNode context) throws Exception {
    DatabaseIdProvider databaseIdProvider = null;
    if (context != null) {
        String type = context.getStringAttribute("type");
        // awful patch to keep backward compatibility
        // Write VENDOR and write DB_ Like VENDOR
        if ("VENDOR".equals(type)) {
            type = "DB_VENDOR";
        Properties properties = context.getChildrenAsProperties();
        databaseIdProvider = (DatabaseIdProvider) resolveClass(type).getDeclaredConstructor().newInstance();
    Environment environment = configuration.getEnvironment();
    if (environment != null && databaseIdProvider != null) {
        String databaseId = databaseIdProvider.getDatabaseId(environment.getDataSource());

The first half is normal initialization   DatabaseIdProvider   The key is that there is another one below   configuration.setDatabaseId(databaseId);   What does it do?

Think about it carefully. We mentioned in the previous chapter, databaseidprovider   It's cooperation   mapper.xml   It is used to define the statement in, and when the source code comes to this position, it is not yet its turn   mapper.xml   Analysis, if, as we did in the previous chapter, in a   mapper.xml   Two identical statements are defined in, and then it's your turn   mapper.xml   MyBatis as like as two peas, two statement are exactly the same as id. Isn't that equivalent to a crash? No, I have to hang him up! Therefore, an exception with the same id as the statement will be thrown. But! This logic is wrong. Although the IDs of the two statements are the same, the databaseId is different. One   SqlSessionFactory   Only one data source can be connected, and the database manufacturer of this data source is determined. Therefore, only one of the two statements can be used, so it is necessary to parse   mapper.xml  , Compare the databaseId when reading the statement! Where does this databaseId come from? Obviously, it can be seen from the overall situation   Configuration   Get it. This one up there   setDatabaseId   You can understand my action! This action is to determine the database manufacturer corresponding to the data source in advance and prepare for later parsing mapper.xml. typeHandlerElement - register type processor

The following one resolves   TypeHandler   Yes, the logic is also relatively simple. You can either scan the package or register one by one, but in the end, you are registered to   typeHandlerRegistry   Medium:

private void typeHandlerElement(XNode parent) {
    if (parent != null) {
        for (XNode child : parent.getChildren()) {
            // Packet scanning
            if ("package".equals(child.getName())) {
                String typeHandlerPackage = child.getStringAttribute("name");
            } else {
                // Register typehandlers one by one
                String javaTypeName = child.getStringAttribute("javaType");
                String jdbcTypeName = child.getStringAttribute("jdbcType");
                String handlerTypeName = child.getStringAttribute("handler");
                Class<?> javaTypeClass = resolveClass(javaTypeName);
                JdbcType jdbcType = resolveJdbcType(jdbcTypeName);
                Class<?> typeHandlerClass = resolveClass(handlerTypeName);
                if (javaTypeClass != null) {
                    if (jdbcType == null) {
                        typeHandlerRegistry.register(javaTypeClass, typeHandlerClass);
                    } else {
                        typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);
                } else {

The logic is very simple. Just turn over the source code and you'll be OK. The booklet is no longer wordy. mapperElement - parse mapper.xml

The resolution of the last node is mapper. At first glance, it doesn't seem very complicated:

private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
        for (XNode child : parent.getChildren()) {
            // Package scan Mapper interface
            if ("package".equals(child.getName())) {
                String mapperPackage = child.getStringAttribute("name");
            } else {
                String resource = child.getStringAttribute("resource");
                String url = child.getStringAttribute("url");
                String mapperClass = child.getStringAttribute("class");
                // Process mapper.xml loaded by resource
                if (resource != null && url == null && mapperClass == null) {
                    InputStream inputStream = Resources.getResourceAsStream(resource);
                    XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
                } else if (resource == null && url != null && mapperClass == null) {
                    // Process mapper.xml loaded by url
                    InputStream inputStream = Resources.getUrlAsStream(url);
                    XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
                } else if (resource == null && url == null && mapperClass != null) {
                    // Register a single Mapper interface
                    Class<?> mapperInterface = Resources.classForName(mapperClass);
                } else {
                    throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");

But please don't forget! After this method is completed, the initialization of the whole MyBatis is completed! But at this time   mapper.xml   Not loaded yet! So this link is also very important! In the source code, it can be found that except for the package scanning Mapper interface and a single registered Mapper interface, the other two are parsing   mapper.xml   File. As for analysis   mapper.xml   How to deal with the bottom layer of the mapping file. We will explain it after the explanation of the mapping file. Here we can know one thing first: mapper.xml   The resolution of is using   XMLMapperBuilder   Completed.

So far, the loading of the entire MyBatis global configuration file has been completed, and the processing flow is relatively uncomplicated

Posted by BETA on Sat, 20 Nov 2021 15:46:12 -0800