MyBatis Source Code Analysis: Configuration Parsing Process

1. Configuration Parsing Process

MyBatis processes two types of configuration files during initialization. The first is the global configuration file (mybatis-config.xml), and the second comprises all Mapper.xml files, including annotations defined on Mapper interface classes.

How does this parsing work?

SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);

The SqlSessionFactoryBuilder follows the Builder pattern (a creational pattern designed to construct complex objects without concerning ourselves with internal details, representing an embodiment of encapsulation). The Builder pattern appears throughout MyBatis (there are 9 additional classes ending with "Builder").

The build() method in SqlSessionFactoryBuilder creates SqlSessionFactory instances. With 9 overloaded versions available, you can create SqlSessionFactory objects using different approaches (defaulting to singleton behavior).

XMLConfigBuilder

At this stage, an XMLConfigBuilder instance is created, along with the Configuration object that stores all parsed configuration information.

XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);

The Configuration object is instantiated here:

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;
}

XMLConfigBuilder extends the abstract BaseBuilder class and specializes in parsing global configuration files. Several other subclasses exist for different parsing targets:

XMLMapperBuilder: Parses Mapper definitions
XMLStatementBuilder: Parses CRUD operation tags
XMLScriptBuilder: Parses dynamic SQL statements

Based on the input stream and two null parameters, a parser is instantiated.

return build(parser.parse());

Two operations occur here. First, the parser's parse() method returns a Configuration instance.

All information from the configuration file gets stored within Configuration. Each child element of <configuration> maps directly to corresponding Configuration class properties.

parse() Method

This method first verifies whether the global configuration file has already been parsed, ensuring the config file requires parsing only once per application lifecycle. The resulting Configuration object persists throughout the entire application lifecycle.

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

parseConfiguration Method

MyBatis provides封装 for both DOM and SAX parsing approaches.

The following method handles all top-level tags in the configuration file.

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

Q&A: Can the tag order in MyBatis global configuration be rearranged? What if settings appears after plugins?

An error occurs, so the order must remain consistent.

propertiesElement() Method

The first operation parses the <properties> tag, loading external configuration files like db.properties.

Two approaches exist: relative paths for files in the classpath and absolute paths (URLs).

The final outcome populates a Properties object called defaults (a Hashtable-based key-value store), after which both XPathParser and Configuration's Properties attribute receive the completed Properties instance.

private void propertiesElement(XNode context) throws Exception {
    if (context != null) {
        Properties defaults = context.getChildrenAsProperties();
        String resource = context.getStringAttribute("resource");
        String url = context.getStringAttribute("url");
        if (resource != null && url != null) {
            throw new BuilderException("Properties element cannot specify both URL and resource.");
        }
        if (resource != null) {
            defaults.putAll(Resources.getResourceAsProperties(resource));
        } else if (url != null) {
            defaults.putAll(Resources.getUrlAsProperties(url));
        }
        Properties vars = configuration.getVariables();
        if (vars != null) {
            defaults.putAll(vars);
        }
        parser.setVariables(defaults);
        configuration.setVariables(defaults);
    }
}

settingsAsProperties() Method

Second, the <settings> tag gets parsed into a Properties object. Processing of <settings> child elements happens later (parse first, configure later).

String resource = context.getStringAttribute("resource");

In earlier versions, parsing and configuration occurred together. Parsing into a Properties object first serves later requirements.

loadCustomVfs(settings)

loadCustomVfs retrieves custom Virtual File System implementations, useful when reading local or FTP remote files.

Based on the <vfsimpl> tag within <settings>, an abstract VFS subclass gets instantiated. MyBatis provides JBoss6VFS and DefaultVFS implementations in the io package.

@SuppressWarnings("unchecked")
Class<? extends VFS> vfsImpl = (Class<? extends VFS>)Resources.classForName(clazz);
configuration.setVfsImpl(vfsImpl);

The result gets assigned to Configuration.

loadCustomLogImpl(settings)

loadCustomLogImpl obtains the logging implementation class from the <logimpl> tag, supporting numerous logging frameworks including LOG4J and SLF4J in the logging package.

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

This creates a Log interface implementation and assigns it to Configuration.

typeAliasesElement() Method

This step handles type alias definitions.

Two definition approaches exist: directly specifying a class alias (for example, mapping com.domain.User to user) or specifying a package, which makes all class names within that package aliases of their fully qualified names.

Class aliases and their corresponding classes get stored in a TypeAliasRegistry object.

Class<?> clazz = Resources.classForName(type);
if (alias == null) {
    typeAliasRegistry.registerAlias(clazz);
} else {
    typeAliasRegistry.registerAlias(alias, clazz);
}

pluginElement() Method

Parses <plugins> tags, such as pagination plugins like PageHelper or custom plugins. The <plugins> tag contains <plugin> tags, each with <property> tags.

Since all plugins implement the Interceptor interface, this step converts plugins into Interceptor instances, configures their properties, and adds them to Configuration's InterceptorChain (a List).

private void pluginElement(XNode parent) throws Exception {
    if (parent != null) {
        for (XNode child : parent.getChildren()) {
            String interceptor = child.getStringAttribute("interceptor");
            Properties properties = child.getChildrenAsProperties();
            Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
            interceptorInstance.setProperties(properties);
            configuration.addInterceptor(interceptorInstance);
        }
    }
}

public void addInterceptor(Interceptor interceptor) {
    interceptorChain.addInterceptor(interceptor);
}

public class InterceptorChain {
    private final List<Interceptor> interceptors = new ArrayList<>();
}

Plugin execution follows three phases: parsing, wrapping (proxy generation), and runtime interception. This step completes the first phase.

objectFactoryElement(), objectWrapperFactoryElement(), reflectorFactoryElement()

ObjectFactory instantiates returned objects.

ObjectWrapperFactory applies special processsing to objects.

For queries without alias configuration, a custom MapWrapper can transform underscore naming to camel case, returning results as a Map structure.

public class CustomMapWrapper extends MapWrapper {
    public CustomMapWrapper(MetaObject metaObject, Map<String, Object> map) {
        super(metaObject, map);
    }

    @Override
    public String findProperty(String name, boolean useCamelCaseMapping) {
        if (useCamelCaseMapping
            && ((name.charAt(0) >= 'A' && name.charAt(0) <= 'Z')
                || name.contains("_"))) {
            return convertUnderscoreToCamel(name);
        }
        return name;
    }

    public String convertUnderscoreToCamel(String input) {
        StringBuilder result = new StringBuilder();
        boolean capitalizeNext = false;
        for (int i = 0; i < input.length(); i++) {
            char currentChar = input.charAt(i);
            if (currentChar == '_') {
                if (result.length() > 0) {
                    capitalizeNext = true;
                }
            } else {
                if (capitalizeNext) {
                    result.append(Character.toUpperCase(currentChar));
                    capitalizeNext = false;
                } else {
                    result.append(Character.toLowerCase(currentChar));
                }
            }
        }
        return result.toString();
    }
}

ReflectorFactory serves as a reflection utility toolkit, encapsulating reflection operations.

All four objects above get created using resolveClass.

Interceptor instance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
ObjectFactory objFactory = (ObjectFactory) resolveClass(type).getDeclaredConstructor().newInstance();
ObjectWrapperFactory wrapperFactory = (ObjectWrapperFactory) resolveClass(type).getDeclaredConstructor().newInstance();
ReflectorFactory reflFactory = (ReflectorFactory) resolveClass(type).getDeclaredConstructor().newInstance();

settingsElement() Method

This handles all child elements of the <settings> tag. Since all child elemants were previously converted to a Properties object, processing the Properties object here is straightforward.

The settings configuration includes 26 options, such as secondary caching, lazy loading, and local cache scope.

All default values get assigned here. Refer to this location when unsure about default values.

All values ultimately get assigned to Configuration properties.

private void settingsElement(Properties props) {
    configuration.setAutoMappingBehavior(AutoMappingBehavior.valueOf(props.getProperty("autoMappingBehavior", "PARTIAL")));
    configuration.setAutoMappingUnknownColumnBehavior(AutoMappingUnknownColumnBehavior.valueOf(props.getProperty("autoMappingUnknownColumnBehavior", "NONE")));
    configuration.setCacheEnabled(booleanValueOf(props.getProperty("cacheEnabled"), true));
    configuration.setProxyFactory((ProxyFactory) createInstance(props.getProperty("proxyFactory")));
    configuration.setLazyLoadingEnabled(booleanValueOf(props.getProperty("lazyLoadingEnabled"), false));
}

environmentsElement() Method

Parses the <environments> tag. An environment corresponds to a data source, so here we create a transaction factory based on the configured <transactionManager> and a data source from the <dataSource> tag. Both objects get set as properties of an Environment object, which then gets stored in configuration.

private void environmentsElement(XNode context) throws Exception {
    if (context != null) {
        if (environment == null) {
            environment = context.getStringAttribute("default");
        }
        for (XNode child : context.getChildren()) {
            String id = child.getStringAttribute("id");
            if (isSpecifiedEnvironment(id)) {
                TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
                DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
                DataSource dataSource = dsFactory.getDataSource();
                Environment.Builder envBuilder = new Environment.Builder(id)
                    .transactionFactory(txFactory)
                    .dataSource(dataSource);
                configuration.setEnvironment(envBuilder.build());
            }
        }
    }
}

databaseIdProviderElement() Method

Parses the databaseIdProvider tag, producing a databaseIdProvider object (enabling multi-database support).

typeHandlerElement works similarly to TypeAlias. TypeHandler supports two configuration methods: defining individual classes or specifying packages. The result comprises JavaType and JdbcType mappings with their corresponding TypeHandlers, stored in the typeHandlerRegistry object.

typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);

Q&A: How does the relationship between Java type, JDBC type, and Handler get mapped?

private void register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler) {
    if (javaType != null) {
        Map<JdbcType, TypeHandler<?>> innerMap = typeHandlerMap.get(javaType);
        if (innerMap == null || innerMap == NULL_TYPE_HANDLER_MAP) {
            innerMap = new HashMap<>();
        }
        innerMap.put(jdbcType, handler);
        typeHandlerMap.put(javaType, innerMap);
    }
    allTypeHandlersMap.put(handler.getClass(), handler);
}

mapperElement() Tag

Parses the <mappers> tag.

Different registration methods defined in the global configuration file use different scanning approaches, but both perform two operations: statement registration and interface registration.

Scan Type Description
resource Relative path
url Absolute path
package Package
class Single interface
private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
        for (XNode child : parent.getChildren()) {
            if ("package".equals(child.getName())) {
                String mapperPackage = child.getStringAttribute("name");
                configuration.addMappers(mapperPackage);
            } else {
                String resource = child.getStringAttribute("resource");
                String url = child.getStringAttribute("url");
                String mapperClass = child.getStringAttribute("class");
                if (resource != null && url == null && mapperClass == null) {
                    ErrorContext.instance().resource(resource);
                    InputStream inputStream = Resources.getResourceAsStream(resource);
                    XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
                    mapperParser.parse();
                } 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();
                } else if (resource == null && url == null && mapperClass != null) {
                    Class<?> mapperInterface = Resources.classForName(mapperClass);
                    configuration.addMapper(mapperInterface);
                } else {
                    throw new BuilderException("Mapper element may only specify url, resource, or class, not multiple.");
                }
            }
        }
    }
}

Starting with the mapperParser.parse() method from Mapper.xml.

public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
        configurationElement(parser.evalNode("/mapper"));
        configuration.addLoadedResource(resource);
        bindMapperForNamespace();
    }
    parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();
}

configurationElement() parses all child tags, ultimately producing a MapperStatement object.

bindMapperForNamespace() associates the namespace (interface type) with the factory class MapperProxyFactory.

  1. configurationElement

configurationElement handles all specific tags in Mapper.xml, including namespace, cache, parameterMap, resultMap, sql, and select|insert|update|delete.

private void configurationElement(XNode context) {
    try {
        String namespace = context.getStringAttribute("namespace");
        if (namespace == null || namespace.equals("")) {
            throw new BuilderException("Mapper namespace cannot be empty");
        }
        builderAssistant.setCurrentNamespace(namespace);
        cacheRefElement(context.evalNode("cache-ref"));
        cacheElement(context.evalNode("cache"));
        parameterMapElement(context.evalNodes("/mapper/parameterMap"));
        resultMapElements(context.evalNodes("/mapper/resultMap"));
        sqlElement(context.evalNodes("/mapper/sql"));
        buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
        throw new BuilderException("Error parsing Mapper XML. Location: '" + resource + "'. Cause: " + e, e);
    }
}

The buildStatementFromContext() method creates an XMLStatementBuilder for parsing CRUD operation tags, then adds the created MappedStatement to mappedStatements.

MapperBuilderAssistant addMappedStatement() method:

MappedStatement statement = statementBuilder.build();
configuration.addMappedStatement(statement);

  1. bindMapperForNamespace

Primarily invokes addMapper()

configuration.addMapper(boundType);

The addMapper() method registers the interface type with MapperRegistry: actually creating a corresponding MapperProxyFactory for that type (used to create MapperProxy through a factory).

knownMappers.put(type, new MapperProxyFactory<>(type));

After interface registration, annotations on interface classes and methods get parsed, such as @CacheNamespace and @Select.

A MapperAnnotationBuilder gets instantiated specifically for annotation parsing.

MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();

The parse() method's parseCache() and parseCacheRef() methods handle @CacheNamespace and @CacheNamespaceRef annotations respectively.

public void parse() {
    String resource = type.toString();
    if (!configuration.isResourceLoaded(resource)) {
        loadXmlResource();
        configuration.addLoadedResource(resource);
        assistant.setCurrentNamespace(type.getName());
        parseCache();
        parseCacheRef();
        Method[] methods = type.getMethods();
        for (Method method : methods) {
            try {
                if (!method.isBridge()) {
                    parseStatement(method);
                }
            } catch (IncompleteElementException e) {
                configuration.addIncompleteMethod(new MethodResolver(this, method));
            }
        }
    }
    parsePendingMethods();
}

The parseStatement() method's various getAnnotation() calls handle corresponding annotation parsing, such as @Options, @SelectKey, and @ResultMap.

Finally, MappedStatement objects get created and added to MapperRegistry, meaning XML configuration and annotation configuration produce equivalent results.

assistant.addMappedStatement(
    mappedStatementId,
    sqlSource,
    statementType,
    sqlCommandType,
    fetchSize,
    timeout,
    null,
    parameterTypeClass,
    resultMapId,
    getReturnType(method),
    resultSetType,
    flushCache,
    useCache,
    false,
    keyGenerator,
    keyProperty,
    keyColumn,
    null,
    languageDriver,
    options != null ? nullOrEmpty(options.resultSets()) : null);

  1. build

After Mapper.xml parsing completes, another build() method gets invoked, returning DefaultSqlSessionFactory, the default SqlSessionFactory implementation.

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

Summary

This section primarily completes parsing of the configuration file, Mapper files, and Mapper interface annotations.

The most significant object obtained is Configuration, which contains all configuration information along with various containers for different data types.

The final result is a DefaultSqlSessionFactory instance that holds a Configuration reference.

Flow Diagram

image

Tags: MyBatis java ORM configuration-parsing SqlSessionFactory

Posted on Fri, 25 Sep 2026 16:09:47 +0000 by ntsf