Mybatis source code – load mapping file and dynamic agent

Time:2022-5-16

preface

This article will analyzeMybatisHow to resolve the in the mapping file during the loading of the configuration fileSQLStatement and eachSQLStatement is associated with the method of the mapping interface. Before looking at this part of the source code, you need to haveJDKIf you don’t know much about dynamic agents, you can first look at the basics of Java – dynamic agentsJDkThe principle of dynamic agent.

text

I Configuration of mapping file / mapping interface

giveMybatisConfiguration file formybatis-config.xmlAs shown below.

<?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>

Of the above configuration filesmappersNode for configurationMapping file / mapping interfacemappersThere are two sub nodes under the node, with labels respectively<mapper>and<package>, these two labels are described below.

label explain
<mapper> The tag has three attributes, namelyresourceurlandclass, and in the same<mapper>Only one of these three attributes can be set in the tag, otherwise an error will be reported.resourceandurlProperties are through tellingMybatisThe location path of the mapping file is used to register the mapping file. The former uses the relative path (relative to theclasspathFor example“mapper/BookMapper.xml”), which uses absolute paths.classProperty is by tellingMybatisThe mapping interface is registered with 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 where the mapping interface is located. At this time, the mapping file must have the same name and directory as the mapping interface.

According to the table above, the configuration file in the examplemybatis-config.xmlThe mapping interface is registered 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.

II Source code analysis of loading mapping file

In mybatis source code – configuration loading, you already know that you can useMybatisThe configuration file will be read firstmybatis-config.xmlFor character stream or byte stream, and then throughSqlSessionFactoryBuilderBuild based on the character stream or byte stream of the configuration fileSqlSessionFactory。 In the whole process, it will be parsedmybatis-config.xmlAnd enrich the analysis resultsConfiguration, andConfigurationstayMybatisIs a single example. No matter the parsing result of the configuration file, the parsing result of the mapping file, or the parsing result of the mapping interface, it will eventually existConfigurationYes. nextMybatis source code – configuration loadingAt the end of this article, we continue to say that the parsing of configuration files takes place inXMLConfigBuilderofparseConfiguration()Method, 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, in parsingMybatisWhen the configuration file is selected, it will be based on the information in the configuration file<mappers>Tag to find the mapping file / mapping interface and parse it. Here ismapperElement()Implementation of 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, themapperElement()Method should enter processingpackageBranches of child nodes, so continue to look down,ConfigurationofaddMappers(String packageName)The method is as follows.

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

mapperRegistryyesConfigurationInternal member variable, which has three overloadedaddMappers()Method, first lookaddMappers(String packageName)Method, as shown below.

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

Go on,addMappers(String packageName, Class<?> superType)The implementation of 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, look againaddMapper(Class<T> type)The implementation of is as follows.

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. Key is the mapping interface class object and value is 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);
            }
        }
    }
}

Top threeaddMapper()Methods are called layer by layer, which is actually based on the configuration file<mappers>Tagged<package>The fully qualified name of the mapping file / package where the mapping interface is located set by the sub tag to obtain the name of the mapping interfaceClassObject, and then based on theClassObject to create aMapperProxyFactory, as the name suggests,MapperProxyFactoryIt is the dynamic proxy factory of mapping interface, which is responsible for generating dynamic proxy classes for the corresponding mapping interface. Let’s take a brief look hereMapperProxyFactoryImplementation of.

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

}

Very standard basedJDKImplementation of dynamic agent, so you can know,MybatisOne is created for each mapping interfaceMapperProxyFactoryAnd then connect the mapping interface withMapperProxyFactoryStored in the form of key value pairsMapperRegistryofknownMappersCache, and thenMapperProxyFactoryWill map interfaces based onJDKThe proxy class is generated by dynamic proxy. As for how to generate it, we will discuss it in the third sectionMapperProxyFactoryFurther analysis.

Continue the previous process and finish creating the mapping interfaceMapperProxyFactoryAfter that, the mapping file and theSQLFor parsing, the classes relied on for parsing areMapperAnnotationBuilder, its class diagram is shown below.

So one mapping interface corresponds to one mapping interfaceMapperAnnotationBuilderAnd eachMapperAnnotationBuilderGlobally unique inConfigurationClass, the parsing results will be enrichedConfigurationYes.MapperAnnotationBuilderAnalytical method ofparse()As shown below.

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)) {
        //Parsing SQL in SQL file first
        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 toparse()Method, which will first parse the data in the mapping fileSQLStatement, and then parse the in the mapping interfaceSQLStatement, taking parsing mapping file as an example.loadXmlResource()The 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();
        }
    }
}

loadXmlResource()In the method, firstly, the path of the mapping file should be spliced according to the fully qualified name of the mapping interface. The splicing rule is to splice the fully qualified name“.”replace with“/”, then add at the end“.xml”, which is why the mapping file and mapping interface need to be in the same directory and have the same name. The parsing of mapping files depends onXMLMapperBuilder, its class diagram is shown below.

As shown in the figure, the parsing classes of parsing configuration file and parsing mapping file inherit fromBaseBuilder, thenBaseBuilderGlobally unique inConfigurationTherefore, the analysis results will be enrichedConfiguration, special attention,XMLMapperBuilderThere’s another one calledsqlFragmentsCache for storage<sql>Label correspondingXNode, thissqlFragmentsandConfigurationMediumsqlFragmentsIt is the same cache. Keep this in mind, which will be analyzed and processed later<include>Used when labeling.XMLMapperBuilderofparse()The method 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();
}

Keep lookingconfigurationElement()The implementation of the method is as follows.

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

configurationElement()Method will map the file<mapper>The sub tags under the are parsed into corresponding classes, and then cached in theConfigurationYes. Typically, in the mapping file<mapper>Under the tag, the commonly used sub tags are<parameterMap><resultMap><select><insert><update>and<delete>, here is a simple table of the generated classes for these tags andConfigurationThe unique identification in the.

label Parsing generated classes stayConfigurationUnique identification in
<parameterMap> ParameterMap namespace + “.” + Tag ID
<resultMap> ResultMap namespace + “.” + Tag ID
<select><insert><update><delete> MappedStatement namespace + “.” + Tag ID

In the table abovenamespaceIs a mapping file<mapper>TaggednamespaceProperty, so for the configuration in the mapping fileparameterMapresultMapperhapsSQLExecute the statement inMybatisThe only identification in isnamespace + “.” + Tag ID。 Here’s how to parse<select><insert><update>and<delete>Take the content of the label as an example to illustrate,buildStatementFromContext()The 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 + "." of the mapping interface+ Tag ID, with the value 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>Labels, one is createdXMLStatementBuilderTo parse and generateMappedStatement, again, take a lookXMLStatementBuilderClass diagram of, as shown below.

XMLStatementBuilderHeld in<select><insert><update>and<delete>Node corresponding to labelXNode, and help createMappedStatementAnd enrichConfigurationofMapperBuilderAssistantClass. Let’s have a lookXMLStatementBuilderofparseStatementNode()method.

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 based on namespace + "." in configuration+ Refid to match < SQL > tags
    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 the xmllanguagedriver
    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 cannot be obtained from 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);
}

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

  • take<include>Replace the label with the one it points toSQLFragment;
  • If dynamic is not usedSQL, createRawSqlSourceTo saveSQLStatement, if dynamicSQL(e.g. using<if><foreach>And so on), then createDynamicSqlSourceTo supportSQLDynamic splicing of sentences;
  • obtain<select><insert><update>and<delete>Attributes on the label;
  • Will getSqlSourceAnd the attributes on the labelMapperBuilderAssistantofaddMappedStatement()Method to createMappedStatementAnd add toConfigurationYes.

MapperBuilderAssistantIs the final creationMappedStatementAnd willMappedStatementAdd toConfigurationThe processing class ofaddMappedStatement()The 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 identifier 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;
}

At this point, it is resolved<select><insert><update>and<delete>The contents of the tag are then generatedMappedStatementAnd add toConfigurationThe process analysis of is completed. In fact, it is analyzed<parameterMap>Tags, parsing<resultMap>The general process of the label is basically the same as that above, and finally it is with the help ofMapperBuilderAssistantGenerate corresponding classes (e.gParameterMapResultMap)Then cache toConfigurationAnd the unique identification of each class generated by parsing in the corresponding cache isnamespace + “.” + Tag ID

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

XMLConfigBuilderMediummapperElement()Different branches of the method just enter the whole loading process from different entrances, andMybatisBefore each operation is executed, it will judge whether the current operation has been done, and 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 it is based onJavaConfigTo configureMybatis, then usually directlyConfigurationSetting parameter values and callingConfigurationofaddMappers(String packageName)To load the mapping file / mapping interface.

III Dynamic agent in mybatis

Known inMapperRegistryOne of them is calledknownMappersofmapCache, whose key is the key of the mapping interfaceClassObject with a value ofMybatisDynamic proxy factory created for mapping interfaceMapperProxyFactory, when calling the method defined by the mapping interface to perform database operation, the actual call request will be executed byMapperProxyFactoryThe proxy object generated for the mapping interface. Given hereMapperProxyFactoryThe implementation of is as follows.

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

}

stayMapperProxyFactoryIn,mapperInterfaceFor mapping interfacesClassObject,methodCacheIt’s amapCache, whose key is the method object of the mapping interface, and its value is the value corresponding to this methodMapperMethodInvoker, actually,SQLThe execution of the will ultimately beMapperMethodInvokerComplete, which will be described in detail later. Now watchMapperProxyFactoryTwo overloaded innewInstance()Method, you can know that this is based onJDKDynamic agent, inpublic T newInstance(SqlSession sqlSession)In this method, aMapperProxyAnd call it as a parameterprotected T newInstance(MapperProxy<T> mapperProxy)Method, in whichProxyofnewProxyInstance()Method to create a dynamic proxy object, so it can be concluded that,MapperProxyIt will come trueInvocationHandlerInterface,MapperProxyThe class diagram of is shown below.

Sure enough,MapperProxyRealizedInvocationHandlerInterface and createMapperProxyTimeMapperProxyFactoryWill hold itmethodCachePass toMapperProxyThereforemethodCacheThe actual reading and writing is byMapperProxyTo finish. Let’s have a lookMapperProxyRealizedinvoke()Method, 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 methodcache according to the method object to execute SQL
            //If it cannot be obtained, create a mappermethodinvoker and add it to the methodcache, and then execute SQL
            return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
        }
    } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
    }
}

be based onJDKThe principle of dynamic proxy can be known when callingJDKWhen the dynamic proxy generates a method that maps the proxy object of the interface, the final call request will be sent toMapperProxyofinvoke()Method, inMapperProxyofinvoke()Method is actually based on the object of the method called by the mapping interfacemethodCacheGet from cacheMapperMethodInvokerTo actually execute the request. If you can’t get it, create one for the current method object firstMapperMethodInvokerAnd joinmethodCacheCache, and then use the createdMapperMethodInvokerTo execute the request.cachedInvoker()The method is implemented as follows.

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

        //The mappermethodinvoker obtained from the methodcache cache is null
        //Then create a mappermethodinvoker, add it to the methodcache cache, and return
        return methodCache.computeIfAbsent(method, m -> {
            //JDK1. 8 processing logic of the default () method in the 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;
    }
}

MapperMethodInvokerIt is an interface, which is usually createdMapperMethodInvokerbyPlainMethodInvoker, take a lookPlainMethodInvokerConstructor for.

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

So createPlainMethodInvokerYou need to create aMapperMethod, andPlainMethodInvokerWhen executing, it also passes the executed request toMapperMethodSo go on,MapperMethodThe constructor for is shown below.

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

establishMapperMethodThe parameters to be passed in areMapping interfaceClassobjectObject that maps the method whose interface is calledandConfiguration classConfiguration, inMapperMethodIn the constructor of, it will be created based on the above three parametersSqlCommandandMethodSignatureSqlCommandIt is mainly used to save and map the data associated with the called method of the interfaceMappedStatementInformation,MethodSignatureIt mainly stores the parameter information and return value information of the called method of the mapping interface. Let’s have a look firstSqlCommandConstructor, 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 get the information associated with the called methodMappedStatementObject, and thenMappedStatementofidField assigned toSqlCommandofnameField, and finallyMappedStatementofsqlCommandTypeField assigned toSqlCommandoftypeField, so,SqlCommandIt has the function associated with the called methodMappedStatementInformation about. So how to get the information associated with the called methodMappedStatementWhat about the object? Keep lookingresolveMappedStatement()The implementation of is as follows.

private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName,
                                               Class<?> declaringClass, Configuration configuration) {
    //Fully qualified name + "." according to the interface+ The method name splices the ID of mappedstatement
    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()The method will be based onInterface fully qualified name + “.”+ “Method name”AsstatementIdgoConfigurationGet from cacheMappedStatement, at the same timeresolveMappedStatement()The method will recursively traverse from the mapping interface to the interface that declares the called method. The termination conditions of recursion are as follows.

  • according toInterface fully qualified name + “.”+ “Method name”AsstatementIdgoConfigurationGot from the cache ofMappedStatement
  • Recursively traverses from the mapping interface to the interface declaring the called method, and according toDeclare the fully qualified name of the interface of the called method + “+ “Method name”AsstatementIdgoConfigurationCannot get from cacheMappedStatement

The above statement is rather convoluted. Let’s illustrate it with an exampleresolveMappedStatement()The reason why the method is written like this. The following figure shows the package path where the mapping interface and mapping file are located.

BaseMapperBookBaseMapperandBookMapperThe relationship between is shown in the figure below.

thatMybatisWill beBaseMapperBookBaseMapperandBookMapperAll generate oneMapperProxyFactory, as shown below.

Similarly, inConfigurationParsing is also cached in theBookBaseMapper.xmlGenerated by mapping fileMappedStatement, as shown below.

stayMybatisof3.4.2And previous versions will only be based onFully qualified name of mapping interface + “+ Method nameandDeclare the fully qualified name of the interface of the called method + “+ Method namegoConfigurationofmappedStatementsGet from cacheMappedStatementWell, according to this logic,BookMapperCorrespondingSqlCommandWill only be based oncom.mybatis.learn.dao.BookMapper.selectAllBooksandcom.mybatis.learn.dao.BaseMapper.selectAllBooksgomappedStatementsGet from cacheMappedStatement, then combined with the above diagrammappedStatementsThe cache content cannot be obtainedMappedStatementYes, so inMybatisof3.4.3And later versionsresolveMappedStatement()Method to supportInherits the corresponding interface of the mapped interfaceSqlCommandIt can also correspond to the mapping interfaceMappedStatementAssociated

aboutSqlCommandThis is the end of the analysis, andMapperMethodMediumMethodSignatureIt 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, throughJDKWe can know the principle of dynamic proxy. When calling the method of proxy object, the call request will be sent to the method in proxy objectInvocationHandler, inMybatisThe request to call the method of the proxy object of the mapping interface will be sent toMapperProxyTherefore, when calling the method of the proxy object of the mapping interface,MapperProxyofinvoke()The method executes 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 methodcache according to the method object to execute SQL
            //If it cannot be obtained, create a mappermethodinvoker and 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,MybatisJust like the traditionalJDKDynamic agents make a little difference from traditional agentsJDKDynamic agents are usually in theirInvocationHandlerIn, some decorative logic will be added before and after the execution of the proxy object methodMybatisIn, there is no proxy object, only the proxy interface, so there is no logic to call the method of the proxy object. Instead, it is obtained according to the method object of the called methodMapperMethodInvokerAnd implement itsinvoke()Method, usually getPlainMethodInvoker, so keep lookingPlainMethodInvokerofinvoke()Method, as shown below.

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

PlainMethodInvokerofinvoke()Method has no logic, just continue to call itsMapperMethodofexecute()Method, and through the above analysis,MapperMethodMediumSqlCommandAssociatedMappedStatement, andMappedStatementContains the information associated with the called methodSQLInformation, combinedSqlSession, you can complete the operation on the database. How to operate the database will be introduced in subsequent articlesMybatisThis concludes the analysis of dynamic agents in. Finally, summarize it with a pictureMybatisThe dynamic agent execution process in is shown below.

summary

This article can be summarized as follows.

  • In the mapping file, each<select><insert><update>and<delete>Labels will be createdMappedStatementAnd stored inConfigurationofmappedStatementsIn the cache,MappedStatementIt mainly contains the information under this labelSQLStatement, parameter information and output parameter information of this tag, etc. every lastMappedStatementThe unique identifier of the isnamespace + “.” + Tag IDThe reason for setting the unique ID in this way is to call the method of the mapping interface according to theFully qualified name of mapping interface + “+ “Method name”Gets the associated with the called methodMappedStatementTherefore, the mapping filenamespaceIt needs to be consistent with the fully qualified name of the mapping interface<select><insert><update>and<delete>Each tag corresponds to a method of mapping interface, and each<select><insert><update>and<delete>TaggedidIt needs to be consistent with the method name of the mapping interface;
  • callMybatisWhen mapping the method of the interface, the actual execution of the call request is based onJDKThe dynamic proxy is completed by the proxy object generated by the mapping interface, and the proxy object of the mapping interface is generated byMapperProxyFactoryofnewInstance()Method generation, one for each mapping interfaceMapperProxyFactory
  • stayMybatisofJDKIn dynamic proxy, theMapperProxyRealizedInvocationHandlerInterface, soMapperProxystayMybatisofJDKDynamic agent plays the role of calling processor, that is, when calling the method of mapping interface, it is actually calledMapperProxyRealizedinvoke()method;
  • stayMybatisofJDKIn dynamic proxy, there is no proxy object, which can be understood as a proxy for the interfaceMapperProxyofinvoke()Method, instead of calling the method of the proxy object, it will be generated based on the mapping interface and the method object of the called methodMapperMethodAnd executeMapperMethodofexecute()Method, that is, the request to call the method of the mapping interface will be sent toMapperMethod, it can be understood as the method of mapping interfaceMapperMethodAgent.