Mybatis source code - load mapping file and dynamic agent

preface

This article will analyze how Mybatis parses the SQL statements in the mapping file and how each SQL statement is associated with the methods of the mapping interface during the loading of the configuration file. Before looking at the source code of this part, you need to have relevant knowledge of JDK dynamic agent. If you don't know much about this part, you can look at it first Java foundation - Dynamic Proxy Learn the principle of JDk dynamic agent.

text

1, Configuration of mapping file / mapping interface

The Mybatis configuration file mybatis-config.xml is given as follows.

<?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>
    <settings>
        <setting name="useGeneratedKeys" value="true"/>
    </settings>

    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/test?characterEncoding=utf-8&amp;serverTimezone=UTC&amp;useSSL=false"/>
                <property name="username" value="root"/>
                <property name="password" value="root"/>
            </dataSource>
        </environment>
    </environments>

    <mappers>
        <package name="com.mybatis.learn.dao"/>
    </mappers>
</configuration>

The mappers node of the above configuration file is used to configure the mapping file / mapping interface. There are two sub nodes under the mappers node, with labels < mapper > and < package >, respectively. The descriptions of these two labels are as follows.

labelexplain
<mapper>The tag has three attributes, namely resource, url and class. In the same < mapper > tag, only one of the three attributes can be set, otherwise an error will be reported. Both the resource and url attributes register the mapping file by telling Mybatis where the mapping file is located. The former uses a relative path (relative to the classpath, such as "mapper/BookMapper.xml") and the latter uses an absolute path. The class attribute registers the mapping interface by telling Mybatis the fully qualified name of the mapping interface corresponding to the mapping file. At this time, the mapping file and the mapping interface are required to have the same name and directory.
<package>The mapping interface is registered by setting the package name of the mapping interface. At this time, the mapping file must have the same name and directory as the mapping interface.

As shown in the above table, the configuration file mybatis-config.xml in the example registers the mapping interface by setting the package name of the mapping interface, so the mapping file and the mapping interface need to have the same name and directory, as shown in the following figure.

The specific reasons will be given in the source code analysis below.

2, Source code analysis of loading mapping file

stay Mybatis source code - configuration loading It has been known in that when using Mybatis, the configuration file mybatis-config.xml will be read as a character stream or byte stream, and then SqlSessionFactory will be built based on the character stream or byte stream of the configuration file through SqlSessionFactoryBuilder. In the whole process, mybatis-config.xml will be parsed and the parsing results will be enriched into configuration. Configuration is a single example in Mybatis. Whether the parsing results of configuration files, mapping files or mapping interfaces will eventually exist in configuration. Next, continue at the end of the Mybatis source code - configuration loading article. The parsing of the configuration file occurs in the parseConfiguration() method of XMLConfigBuilder, as shown below.

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"));
        //According to the attributes of the mappers tag, find the mapping file / mapping interface and parse it
        mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
        throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
}

As shown above, when parsing the configuration file of Mybatis, the mapping file / mapping interface will be found and parsed according to the properties of the < mappers > tag in the configuration file. The following is the implementation of the mapperElement() method.

private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
        for (XNode child : parent.getChildren()) {
            if ("package".equals(child.getName())) {
                //Processing package child nodes
                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) {
                    //Process the mapper child node with the resource attribute set
                    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) {
                    //Process the mapper child node with the url attribute set
                    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) {
                    //Process the mapper child node with the class attribute set
                    Class<?> mapperInterface = Resources.classForName(mapperClass);
                    configuration.addMapper(mapperInterface);
                } else {
                    //When two or more attributes of the mapper child node are set at the same time, an error is reported
                    throw new BuilderException(
                            "A mapper element may only specify a url, resource or class, but not more than one.");
                }
            }
        }
    }
}

Combined with the Configuration file in the example, the mapperElement() method should enter the branch for processing package child nodes, so continue to look down. The addMappers(String packageName) method of Configuration is as follows.

public void addMappers(String packageName) {
    mapperRegistry.addMappers(packageName);
}

mapperRegistry is a member variable inside Configuration. There are three overloaded addMappers() methods inside. First, look at the addMappers(String packageName) method, as shown below.

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

Moving on, the implementation of addmappers (string packagename, class <? > supertype) is as follows.

public void addMappers(String packageName, Class<?> superType) {
    ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
    resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
    //Gets the Class object of the mapping interface under the package path
    Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
    for (Class<?> mapperClass : mapperSet) {
        addMapper(mapperClass);
    }
}

Finally, let's look at the implementation of addmapper (class < T > type), as shown below.

public <T> void addMapper(Class<T> type) {
    if (type.isInterface()) {
        //Judge whether there is a current mapping interface in knownMappers
        //knownMappers is a map storage structure, with key as the mapping interface Class object and value as MapperProxyFactory
        //MapperProxyFactory is the dynamic proxy factory corresponding to the mapping interface
        if (hasMapper(type)) {
            throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
        }
        boolean loadCompleted = false;
        try {
            knownMappers.put(type, new MapperProxyFactory<>(type));
            //Rely on MapperAnnotationBuilder to complete Sql parsing in mapping file and mapping interface
            //First parse the mapping file, and then the mapping interface
            MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
            parser.parse();
            loadCompleted = true;
        } finally {
            if (!loadCompleted) {
                knownMappers.remove(type);
            }
        }
    }
}

The above three addMapper() methods are called layer by layer. In fact, they obtain the Class object of the mapping interface according to the fully qualified name of the mapping file / package of the mapping interface set by the < package > sub tag of the < mappers > tag in the configuration file, and then create a MapperProxyFactory based on the Class object of each mapping interface, as the name suggests, MapperProxyFactory is a dynamic proxy factory for mapping interfaces, which is responsible for generating dynamic proxy classes for corresponding mapping interfaces. Let's take a brief look at the implementation of MapperProxyFactory.

public class MapperProxyFactory<T> {

    private final Class<T> mapperInterface;
    private final Map<Method, MapperMethodInvoker> methodCache = new ConcurrentHashMap<>();

    public MapperProxyFactory(Class<T> mapperInterface) {
        this.mapperInterface = mapperInterface;
    }

    public Class<T> getMapperInterface() {
        return mapperInterface;
    }

    public Map<Method, MapperMethodInvoker> getMethodCache() {
        return methodCache;
    }

    @SuppressWarnings("unchecked")
    protected T newInstance(MapperProxy<T> mapperProxy) {
        return (T) Proxy.newProxyInstance(
                mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
    }

    public T newInstance(SqlSession sqlSession) {
        final MapperProxy<T> mapperProxy = new MapperProxy<>(
                sqlSession, mapperInterface, methodCache);
        return newInstance(mapperProxy);
    }

}

It is a standard implementation based on JDK dynamic proxy, so you can know that Mybatis will create a MapperProxyFactory for each mapping interface, and then store the mapping interface and MapperProxyFactory in the knownMappers cache of MapperRegistry in the form of key value pairs, and then MapperProxyFactory will generate proxy classes for the mapping interface based on JDK dynamic proxy, As for how to generate, MapperProxyFactory will be further analyzed in Section 3.

Continue the previous process. After creating MapperProxyFactory for the mapping interface, you should parse the mapping file and SQL in the mapping interface. The class relied on for parsing is MapperAnnotationBuilder, and its class diagram is as follows.

Therefore, a mapping interface corresponds to a MapperAnnotationBuilder, and each MapperAnnotationBuilder holds a globally unique Configuration class, and the parsing results will be enriched into the Configuration. The parsing method parse() of MapperAnnotationBuilder is as follows.

public void parse() {
    String resource = type.toString();
    //Judge whether the mapping interface has been resolved, and continue to execute until it has not been resolved
    if (!configuration.isResourceLoaded(resource)) {
        //First parse the Sql statement in the mapping file
        loadXmlResource();
        //Add the current mapping interface to the cache to indicate that the current mapping interface has been resolved
        configuration.addLoadedResource(resource);
        assistant.setCurrentNamespace(type.getName());
        parseCache();
        parseCacheRef();
        //Parsing Sql statements in mapping interfaces
        for (Method method : type.getMethods()) {
            if (!canHaveStatement(method)) {
                continue;
            }
            if (getAnnotationWrapper(method, false, Select.class, SelectProvider.class).isPresent()
                    && method.getAnnotation(ResultMap.class) == null) {
                parseResultMap(method);
            }
            try {
                parseStatement(method);
            } catch (IncompleteElementException e) {
                configuration.addIncompleteMethod(new MethodResolver(this, method));
            }
        }
    }
    parsePendingMethods();
}

According to the execution process of parse() method, the SQL statements in the mapping file will be parsed first, and then the SQL statements in the mapping interface will be parsed. Here, take parsing the mapping file as an example. The loadXmlResource() method is implemented as follows.

private void loadXmlResource() {
    if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
        //The path of the mapping file is spliced according to the fully qualified name of the mapping interface
        //This also explains why mapping files and mapping interfaces are required to be in the same directory
        String xmlResource = type.getName().replace('.', '/') + ".xml";
        InputStream inputStream = type.getResourceAsStream("/" + xmlResource);
        if (inputStream == null) {
            try {
                inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
            } catch (IOException e2) {
            
            }
        }
        if (inputStream != null) {
            XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), 
                    xmlResource, configuration.getSqlFragments(), type.getName());
            //Parse mapping file
            xmlParser.parse();
        }
    }
}

In the loadXmlResource() method, the path of the mapping file must be spliced according to the fully qualified name of the mapping interface. The splicing rule is to replace the "." of the fully qualified name with "/", and then add ". xml" at the end. This is why the mapping file and the mapping interface need to be in the same directory and have the same name. The parsing of mapping files depends on XMLMapperBuilder, and its class diagram is shown below.

As shown in the figure, the parsing classes of the parsing Configuration file and the parsing mapping file inherit from the BaseBuilder, and the globally unique Configuration is held in the BaseBuilder, so the parsing results will be enriched into the Configuration. In particular, XMLMapperBuilder also has a cache called sqlFragments to store the XNode corresponding to the < SQL > tag, This sqlFragments is the same cache as the sqlFragments in the Configuration. Remember that it will be used later when analyzing and processing the < include > tag. The parse() method of xmlmaperbuilder is as follows.

public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
        //Start parsing from the < mapper > tag of the mapping file
        //The parsing results will enrich the Configuration
        configurationElement(parser.evalNode("/mapper"));
        configuration.addLoadedResource(resource);
        bindMapperForNamespace();
    }

    parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();
}

Continue to look at the implementation of the configurationElement() method, as shown below.

private void configurationElement(XNode context) {
    try {
        String namespace = context.getStringAttribute("namespace");
        if (namespace == null || namespace.isEmpty()) {
            throw new BuilderException("Mapper's namespace cannot be empty");
        }
        builderAssistant.setCurrentNamespace(namespace);
        cacheRefElement(context.evalNode("cache-ref"));
        cacheElement(context.evalNode("cache"));
        //Parse the < ParameterMap > tag to generate a ParameterMap and cache it to the Configuration
        parameterMapElement(context.evalNodes("/mapper/parameterMap"));
        //Parse the < ResultMap > tag to generate a ResultMap and cache it to the Configuration
        resultMapElements(context.evalNodes("/mapper/resultMap"));
        //Save the node XNode corresponding to the < SQL > tag to sqlFragments
        //In fact, it is also saved to the sqlFragments cache of Configuration
        sqlElement(context.evalNodes("/mapper/sql"));
        //Parse < Select >, < Insert >, < update > and < delete > tags
        //Generate MappedStatement and cache it to Configuration
        buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
        throw new BuilderException("Error parsing Mapper XML. The XML location is '" 
                + resource + "'. Cause: " + e, e);
    }
}

The configurationElement() method will parse the sub tags under the mapping file < mapper > into corresponding classes, and then cache them in the Configuration. Generally, under the < mapper > tag of the mapping file, the commonly used sub tags are < parametermap >, < resultmap >, < Select >, < Insert >, < update > and < delete >. A simple table is given below to summarize the classes generated by these tags and their unique identifiers in Configuration.

labelParsing generated classesUnique identification in Configuration
<parameterMap>ParameterMapnamespace + ".. + tag id
<resultMap>ResultMapnamespace + ".. + tag id
<select>,<insert>,<update>,<delete>MappedStatementnamespace + ".. + tag id

The namespace in the above table is the namespace attribute of the < mapper > tag of the mapping file. Therefore, for the parameterMap, resultMap or SQL execution statements configured in the mapping file, the only identification in Mybatis is the namespace + ".. + tag id. The following describes how to parse the contents of < Select >, < Insert >, < update > and < delete > tags. The buildStatementFromContext() method is as follows.

private void buildStatementFromContext(List<XNode> list) {
    if (configuration.getDatabaseId() != null) {
        buildStatementFromContext(list, configuration.getDatabaseId());
    }
    buildStatementFromContext(list, null);
}

private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    //A MappedStatement will be created for each < Select >, < Insert >, < update > and < delete > tag
    //Each MappedStatement is stored in the mappedStatements cache of Configuration
    //mappedStatements is a map. The key is the fully qualified name + ".. + tag id of the mapping interface, and the value is MappedStatement
    for (XNode context : list) {
        final XMLStatementBuilder statementParser = new XMLStatementBuilder(
                    configuration, builderAssistant, context, requiredDatabaseId);
        try {
            statementParser.parseStatementNode();
        } catch (IncompleteElementException e) {
            configuration.addIncompleteStatement(statementParser);
        }
    }
}

For each < Select >, < Insert >, < update > and < delete > tag, an XMLStatementBuilder will be created to parse and generate MappedStatement. Similarly, take a look at the class diagram of XMLStatementBuilder, as shown below.

XMLStatementBuilder holds the node XNode corresponding to < Select >, < Insert >, < update > and < delete > tags, and the MapperBuilderAssistant class that helps create MappedStatement and enrich Configuration. Let's take a look at the parseStatementNode() method of XMLStatementBuilder.

public void parseStatementNode() {
    //Get tag id
    String id = context.getStringAttribute("id");
    String databaseId = context.getStringAttribute("databaseId");

    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
        return;
    }

    String nodeName = context.getNode().getNodeName();
    //Get the type of tag, such as SELECT, INSERT, etc
    SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
    boolean useCache = context.getBooleanAttribute("useCache", isSelect);
    boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

    //If the < include > tag is used, replace the < include > tag with the Sql fragment in the matching < Sql > tag
    //The matching rule is to match < SQL > tags according to namespace + ".. + refid in Configuration
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    includeParser.applyIncludes(context.getNode());

    //Get input parameter type
    String parameterType = context.getStringAttribute("parameterType");
    Class<?> parameterTypeClass = resolveClass(parameterType);

    //Get LanguageDriver to support dynamic Sql implementation
    //What you get here is actually an XML languagedriver
    String lang = context.getStringAttribute("lang");
    LanguageDriver langDriver = getLanguageDriver(lang);

    processSelectKeyNodes(id, parameterTypeClass, langDriver);

    //Get KeyGenerator
    KeyGenerator keyGenerator;
    String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
    keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
    //Get the KeyGenerator from the cache first
    if (configuration.hasKeyGenerator(keyStatementId)) {
        keyGenerator = configuration.getKeyGenerator(keyStatementId);
    } else {
        //If it is not available in the cache, it is determined whether to use KeyGenerator according to the configuration of useGeneratedKeys
        //If you want to use, the KeyGenerator used in Mybatis is Jdbc3KeyGenerator
        keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
            configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
            ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
    }

    //Creating SqlSource through XMLLanguageDriver can be understood as a Sql statement
    //If < if >, < foreach > and other tags are used to splice dynamic Sql statements, the created SqlSource is DynamicSqlSource
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    StatementType statementType = StatementType
            .valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    //Get the attributes on the < Select >, < Insert >, < update > and < delete > tags
    Integer fetchSize = context.getIntAttribute("fetchSize");
    Integer timeout = context.getIntAttribute("timeout");
    String parameterMap = context.getStringAttribute("parameterMap");
    String resultType = context.getStringAttribute("resultType");
    Class<?> resultTypeClass = resolveClass(resultType);
    String resultMap = context.getStringAttribute("resultMap");
    String resultSetType = context.getStringAttribute("resultSetType");
    ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
    if (resultSetTypeEnum == null) {
        resultSetTypeEnum = configuration.getDefaultResultSetType();
    }
    String keyProperty = context.getStringAttribute("keyProperty");
    String keyColumn = context.getStringAttribute("keyColumn");
    String resultSets = context.getStringAttribute("resultSets");

    //According to the parameters obtained above, create a MappedStatement and add it to the Configuration
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered,
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}

The overall process of parseStatementNode() method is slightly longer. To sum up, this method does the following things.

  • Replace the < include > tag with the SQL fragment it points to;
  • If dynamic SQL is not used, create RawSqlSource to save SQL statements. If dynamic SQL is used (for example, labels such as < if >, < foreach >), create DynamicSqlSource to support dynamic splicing of SQL statements;
  • Get the attributes on < Select >, < Insert >, < update > and < delete > tags;
  • Pass the obtained SqlSource and the properties on the label into the MapperBuilderAssistant's addMappedStatement() method to create a MappedStatement and add it to the Configuration.

MapperBuilderAssistant is the processing class that finally creates MappedStatement and adds MappedStatement to Configuration. Its addMappedStatement() method is as follows.

public MappedStatement addMappedStatement(
        String id,
        SqlSource sqlSource,
        StatementType statementType,
        SqlCommandType sqlCommandType,
        Integer fetchSize,
        Integer timeout,
        String parameterMap,
        Class<?> parameterType,
        String resultMap,
        Class<?> resultType,
        ResultSetType resultSetType,
        boolean flushCache,
        boolean useCache,
        boolean resultOrdered,
        KeyGenerator keyGenerator,
        String keyProperty,
        String keyColumn,
        String databaseId,
        LanguageDriver lang,
        String resultSets) {

    if (unresolvedCacheRef) {
        throw new IncompleteElementException("Cache-ref not yet resolved");
    }

    //Splice out the unique ID of the MappedStatement
    //The rule is namespace + ".. + ID
    id = applyCurrentNamespace(id, false);
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;

    MappedStatement.Builder statementBuilder = new MappedStatement
        .Builder(configuration, id, sqlSource, sqlCommandType)
            .resource(resource)
            .fetchSize(fetchSize)
            .timeout(timeout)
            .statementType(statementType)
            .keyGenerator(keyGenerator)
            .keyProperty(keyProperty)
            .keyColumn(keyColumn)
            .databaseId(databaseId)
            .lang(lang)
            .resultOrdered(resultOrdered)
            .resultSets(resultSets)
            .resultMaps(getStatementResultMaps(resultMap, resultType, id))
            .resultSetType(resultSetType)
            .flushCacheRequired(valueOrDefault(flushCache, !isSelect))
            .useCache(valueOrDefault(useCache, isSelect))
            .cache(currentCache);

    ParameterMap statementParameterMap = getStatementParameterMap(
            parameterMap, parameterType, id);
    if (statementParameterMap != null) {
        statementBuilder.parameterMap(statementParameterMap);
    }

    //Create MappedStatement
    MappedStatement statement = statementBuilder.build();
    //Add MappedStatement to Configuration
    configuration.addMappedStatement(statement);
    return statement;
}

So far, the process analysis of parsing < Select >, < Insert >, < update > and < delete > tags and then generating MappedStatement and adding it to Configuration has been completed. In fact, the general process of parsing < ParameterMap > tags and < ResultMap > tags is basically the same as above. In the end, the corresponding classes (such as ParameterMap and ResultMap) are generated with the help of MapperBuilderAssistant Then it is cached in the Configuration, and the unique id of each class generated by parsing in the corresponding cache is namespace + ".. + tag id.

Finally, go back to the beginning of this section, that is, the mapperElement() method in XMLConfigBuilder. In this method, you will enter different branches to execute the logic of loading mapping file / mapping interface according to the sub tags of < mappers > tag in the configuration file. In fact, the whole process of loading mapping file / loading mapping interface is a ring, which can be illustrated by the following figure.

Different branches of mapperElement() method in XMLConfigBuilder only enter the whole loading process from different entrances. At the same time, Mybatis will judge whether the current operation has been done before each operation is executed. If it has been done, it will not be repeated. Therefore, it ensures that the whole ring processing process will only be executed once without dead loop. And, if Mybatis is configured based on JavaConfig in the project, the parameter value is usually set directly to Configuration, and addMappers(String packageName) of Configuration is called to load the mapping file / mapping interface.

3, Dynamic proxy in Mybatis

It is known that there is a map cache called knownMappers in MapperRegistry. Its key is the Class object of the mapping interface and its value is the dynamic proxy factory MapperProxyFactory created by Mybatis for the mapping interface. When calling the method defined by the mapping interface to perform database operation, the actual call request will be completed by the proxy object generated by MapperProxyFactory for the mapping interface. Here is the implementation of MapperProxyFactory, as shown below.

public class MapperProxyFactory<T> {

    private final Class<T> mapperInterface;
    private final Map<Method, MapperMethodInvoker> methodCache = new ConcurrentHashMap<>();

    public MapperProxyFactory(Class<T> mapperInterface) {
        this.mapperInterface = mapperInterface;
    }

    public Class<T> getMapperInterface() {
        return mapperInterface;
    }

    public Map<Method, MapperMethodInvoker> getMethodCache() {
        return methodCache;
    }

    @SuppressWarnings("unchecked")
    protected T newInstance(MapperProxy<T> mapperProxy) {
        return (T) Proxy.newProxyInstance(
                mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
    }

    public T newInstance(SqlSession sqlSession) {
        final MapperProxy<T> mapperProxy = new MapperProxy<>(
                sqlSession, mapperInterface, methodCache);
        return newInstance(mapperProxy);
    }

}

In MapperProxyFactory, mapperInterface is the Class object of the mapping interface, methodCache is a map cache, its key is the method object of the mapping interface, and its value is the MapperMethodInvoker corresponding to this method. In fact, the execution of SQL will be completed by MapperMethodInvoker, which will be described in detail later. Now look at the two overloaded newInstance() methods in MapperProxyFactory. You can see that this is a dynamic Proxy based on JDK. In the public T newInstance(SqlSession sqlSession) method, MapperProxy will be created and called as a parameter to the protected t newinstance (MapperProxy < T > MapperProxy) method. In this method, the newProxyInstance() of Proxy will be used Method to create a dynamic Proxy object, so it can be concluded that MapperProxy will certainly implement the InvocationHandler interface. The Class diagram of MapperProxy is shown below.

Sure enough, MapperProxy implements the InvocationHandler interface. When creating MapperProxy, MapperProxyFactory will pass the methodCache it holds to MapperProxy. Therefore, the actual reading and writing of methodCache is completed by MapperProxy. Let's take a look at the invoke() method implemented by MapperProxy, as shown below.

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
        if (Object.class.equals(method.getDeclaringClass())) {
            return method.invoke(this, args);
        } else {
            //Get MapperMethodInvoker from the methodCache according to the method object to execute Sql
            //If not, create a MapperMethodInvoker, add it to the methodCache, and then execute Sql
            return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
        }
    } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
    }
}

Based on the principle of JDK dynamic proxy, when calling the method of the proxy object of the mapping interface generated by JDK dynamic proxy, the final call request will be sent to the invoke() method of MapperProxy. In the invoke() method of MapperProxy, it is actually to obtain MapperMethodInvoker from the methodCache cache cache according to the object of the method called by the mapping interface to actually execute the request, If you can't get it, first create a MapperMethodInvoker for the current method object and add it to the methodCache cache, and then use the created MapperMethodInvoker to execute the request. The cachedInvoker() method is implemented as follows.

private MapperProxy.MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
    try {
        MapperProxy.MapperMethodInvoker invoker = methodCache.get(method);
        //If the MapperMethodInvoker obtained from the methodCache cache is not empty, it will be returned directly
        if (invoker != null) {
            return invoker;
        }

        //The MapperMethodInvoker obtained from the methodCache cache is null
        //Then a MapperMethodInvoker is created, added to the methodCache cache, and returned
        return methodCache.computeIfAbsent(method, m -> {
            //Processing logic of default() method in JDK1.8 interface
            if (m.isDefault()) {
                try {
                    if (privateLookupInMethod == null) {
                        return new MapperProxy.DefaultMethodInvoker(getMethodHandleJava8(method));
                    } else {
                        return new MapperProxy.DefaultMethodInvoker(getMethodHandleJava9(method));
                    }
                } catch (IllegalAccessException | InstantiationException | InvocationTargetException
                        | NoSuchMethodException e) {
                    throw new RuntimeException(e);
                }
            } else {
                //First create a MapperMethod
                //Then use MapperMethod as a parameter to create PlainMethodInvoker
                return new MapperProxy.PlainMethodInvoker(
                    new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
            }
        });
    } catch (RuntimeException re) {
        Throwable cause = re.getCause();
        throw cause == null ? re : cause;
    }
}

MapperMethodInvoker is an interface. Usually, the created MapperMethodInvoker is PlainMethodInvoker. Take a look at the constructor of PlainMethodInvoker.

public PlainMethodInvoker(MapperMethod mapperMethod) {
    super();
    this.mapperMethod = mapperMethod;
}

Therefore, when creating PlainMethodInvoker, you need to create MapperMethod first, and PlainMethodInvoker also passes the executed request to MapperMethod during execution, so continue. The constructor of MapperMethod is as follows.

public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
    this.command = new SqlCommand(config, mapperInterface, method);
    this.method = new MethodSignature(config, mapperInterface, method);
}

When creating MapperMethod, the parameters to be passed in are the Class object of the mapping interface, the object of the method called by the mapping interface and the Configuration Class Configuration. In the MapperMethod constructor, SqlCommand and MethodSignature will be created based on the above three parameters. SqlCommand mainly saves the MappedStatement information associated with the called method of the mapping interface, MethodSignature mainly stores the parameter information and return value information of the called method of the mapping interface. First, take a look at the constructor of SqlCommand, as shown below.

public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
    //Gets the method name of the called method of the mapping interface
    final String methodName = method.getName();
    //Gets the Class object that declares the interface of the called method
    final Class<?> declaringClass = method.getDeclaringClass();
    //Gets MappedStatement object associated with the called method of the mapping interface
    MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass,
            configuration);
    if (ms == null) {
        if (method.getAnnotation(Flush.class) != null) {
            name = null;
            type = SqlCommandType.FLUSH;
        } else {
            throw new BindingException("Invalid bound statement (not found): "
                    + mapperInterface.getName() + "." + methodName);
        }
    } else {
        //Assign the id of MappedStatement to the name field of SqlCommand
        name = ms.getId();
        //Assign the Sql command type of MappedStatement to the type field of SqlCommand
        //For example, SELECT, INSERT, etc
        type = ms.getSqlCommandType();
        if (type == SqlCommandType.UNKNOWN) {
            throw new BindingException("Unknown execution method for: " + name);
        }
    }
}

The constructor mainly does these things: first obtain the MappedStatement object associated with the called method, then assign the id field of MappedStatement to the name field of SqlCommand, and finally assign the sqlCommandType field of MappedStatement to the type field of SqlCommand. In this way, SqlCommand has the information of MappedStatement associated with the called method. So how to get the MappedStatement object associated with the called method? Continue to look at the implementation of resolveMappedStatement(), as shown below.

private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName,
                                               Class<?> declaringClass, Configuration configuration) {
    //Splice the id of MappedStatement according to the fully qualified name of the interface + ". + method name
    String statementId = mapperInterface.getName() + "." + methodName;
    //If the MappedStatement corresponding to statementId is cached in the Configuration, the MappedStatement will be returned directly
    //This is one of the termination conditions of recursion
    if (configuration.hasStatement(statementId)) {
        return configuration.getMappedStatement(statementId);
    } else if (mapperInterface.equals(declaringClass)) {
        //Currently, mapperInterface is already a Class object that declares the interface of the called method, and does not match the cached MappedStatement. null is returned
        //This is one of the termination conditions for resolveMappedStatement() recursion
        return null;
    }
    //Recursive call
    for (Class<?> superInterface : mapperInterface.getInterfaces()) {
        if (declaringClass.isAssignableFrom(superInterface)) {
            MappedStatement ms = resolveMappedStatement(superInterface, methodName,
                    declaringClass, configuration);
            if (ms != null) {
                return ms;
            }
        }
    }
    return null;
}

resolveMappedStatement() method will fetch MappedStatement from the cache of Configuration according to the fully qualified name + ". +" method name "of the interface as the statementId. Meanwhile, resolveMappedStatement() method will recursively traverse from the mapping interface to the interface declaring the called method. The termination conditions of recursion are as follows.

  • According to the fully qualified name of the interface + ". +" method name "as the statementId, go to the cache of Configuration and get the MappedStatement;
  • Recursively traversed from the mapping interface to the interface declaring the called method, and according to the fully qualified name + ". +" method name "of the interface declaring the called method as the statementId, the MappedStatement cannot be obtained in the cache of Configuration.

The above statement is rather windy. Let's use an example to explain why the resolveMappedStatement() method is written like this. The following figure shows the package path where the mapping interface and mapping file are located.

The relationship between BaseMapper, BookBaseMapper and BookMapper is shown in the following figure.

Then Mybatis will generate a MapperProxyFactory for BaseMapper, BookBaseMapper and BookMapper, as shown below.

Similarly, the MappedStatement generated by parsing the BookBaseMapper.xml mapping file will also be cached in Configuration, as shown below.

In Mybatis version 3.4.2 and earlier, mappedStatements are only obtained from the mapedstatements cache of Configuration according to the fully qualified name + ".. + method name of the mapping interface and the fully qualified name +".. + method name of the interface declaring the called method. Then, according to this logic, The SqlCommand corresponding to BookMapper will only obtain mappedStatements from the mappedStatements cache according to com.mybatis.learn.dao.BookMapper.selectAllBooks and com.mybatis.learn.dao.BaseMapper.selectAllBooks. In combination with the mappedStatements cache content shown in the figure above, mappedStatements cannot be obtained. Therefore, in Mybatis 3.4.3 and later versions, The logic in resolveMappedStatement() method is adopted to support that the SqlCommand corresponding to the interface that inherits the mapping interface can also be associated with the MappedStatement corresponding to the mapping interface.

The analysis of SqlCommand ends here, and the MethodSignature in MapperMethod is mainly used to store the parameter information and return value information of the called method, which will not be repeated here.

Finally, an execution chain when the proxy object of the mapping interface executes the method is described. First, we can know that by calling the principle of JDK dynamic proxy, the calling request will be sent to the InvocationHandler in the proxy object when the method of calling the proxy object is invoked. In Mybatis, the request of the method of calling the proxy object of the interface will be sent to MapperProxy, so the invoke() method of MapperProxy will be executed when the method of the proxy object of the mapping interface is invoked. The implementation is as follows.

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
        if (Object.class.equals(method.getDeclaringClass())) {
            return method.invoke(this, args);
        } else {
            //Get MapperMethodInvoker from the methodCache according to the method object to execute Sql
            //If not, create a MapperMethodInvoker, add it to the methodCache, and then execute Sql
            return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
        }
    } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
    }
}

So here, Mybatis is a little different from the traditional JDK dynamic agent. The traditional JDK dynamic agent usually adds some decorative logic in its InvocationHandler before and after the proxy object method is executed. In Mybatis, there is no proxy object, only the proxy interface, so there is no logic to call the proxy object's method, Instead, get MapperMethodInvoker according to the method object of the called method and execute its invoke() method. Usually, you get PlainMethodInvoker, so continue to look at the invoke() method of PlainMethodInvoker, as shown below.

@Override
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
    return mapperMethod.execute(sqlSession, args);
}

The invoke() method of PlainMethodInvoker has no logic, that is, it continues to call the execute() method of its MapperMethod. Through the above analysis, it is known that the SqlCommand in MapperMethod is associated with MappedStatement, which contains the SQL information associated with the called method. Combined with SqlSession, the operation on the database can be completed. How to operate the database will be introduced in subsequent articles. This article will stop the analysis of dynamic agents in Mybatis. Finally, a diagram is used to summarize the dynamic agent execution process in Mybatis, as shown below.

summary

This article can be summarized as follows.

  • In the mapping file, each < Select >, < Insert >, < update > and < delete > tag will be created a MappedStatement and stored in the mappedStatements cache of Configuration. The MappedStatement mainly contains the SQL statement under this tag, parameter information and output parameter information of this tag. The unique id of each MappedStatement is the namespace + ".. + tag id. the reason for setting the unique id is that when calling the method of the mapping interface, the MappedStatement associated with the called method can be obtained according to the fully qualified name +".. + "method name" of the mapping interface. Therefore, the namespace of the mapping file needs to be consistent with the fully qualified name of the mapping interface. Each < Select >, The < Insert >, < update > and < delete > tags correspond to a method of the mapping interface. The id of each < Select >, < Insert >, < update > and < delete > tag should be consistent with the method name of the mapping interface;
  • When calling the method of Mybatis mapping interface, the actual execution of the call request is completed by the proxy object generated for the mapping interface based on JDK dynamic proxy. The proxy object of the mapping interface is generated by the newInstance() method of MapperProxyFactory, and each mapping interface corresponds to a MapperProxyFactory;
  • In the JDK dynamic proxy of Mybatis, the InvocationHandler interface is implemented by MapperProxy. Therefore, MapperProxy plays the role of calling processor in the JDK dynamic proxy of Mybatis, that is, when calling the method of the mapping interface, it is actually the invoke() method implemented by the called MapperProxy;
  • In the JDK dynamic proxy of Mybatis, there is no proxy object, which can be understood as a proxy for the interface. Therefore, in the invoke() method of MapperProxy, the method of the proxy object is not called, but the MapperMethod is generated based on the mapping interface and the method object of the called method, and the execute() method of MapperMethod is executed, That is, the request to call the method of the mapping interface will be sent to MapperMethod. It can be understood that the method of the mapping interface is represented by MapperMethod.

Tags: Database Mybatis SQL Dynamic Proxy mapper

Posted on Sun, 28 Nov 2021 22:44:45 -0500 by rickmans