詳解MyBatis加載映射文件和動態代理

互聯網高級架構師 發佈 2024-05-04T22:12:35.889714+00:00

給出 MyBatis 的配置文件 mybatis-config.xml 如下所示:上述配置文件的 mappers 節點用於配置映射文件/映射接口,mappers 節點下有兩種子節點,標籤分別為 和 ,這兩種標籤的說明如下所示:標籤。

本篇文章將分析MyBatis在配置文件加載的過程中,如何解析映射文件中的SQL語句以及每條Sql語句如何與映射接口的方法進行關聯。

MyBatis版本:3.5.6

一、映射文件/映射接口的配置

給出 MyBatis 的配置文件 mybatis-config.xml 如下所示:

<?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&serverTimezone=UTC&useSSL=false"/>
                <property name="username" value="root"/>
                <property name="password" value="root"/>
            </dataSource>
        </environment>
    </environments>

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

上述配置文件的 mappers 節點用於配置映射文件/映射接口mappers 節點下有兩種子節點,標籤分別為 <mapper> 和 <package>,這兩種標籤的說明如下所示:

標籤

說明

<mapper>

該標籤有三種屬性,分別為 resourceurlclass,且在同一個 <mapper> 標籤中,只能設置這三種屬性中的一種,否則會報錯。resourceurl 屬性均是通過告訴 MyBatis 映射文件所在的位置路徑來註冊映射文件,前者使用相對路徑(相對於 classpath,例如 "mapper/BookMapper.xml"),後者使用絕對路徑。class 屬性是通過告訴MyBatis 映射文件對應的映射接口的全限定名來註冊映射接口,此時要求映射文件與映射接口同名且同目錄。

<package>

通過設置映射接口所在包名來註冊映射接口,此時要求映射文件與映射接口同名且同目錄。

根據上表所示,示例中的配置文件mybatis-config.xml是通過設置映射接口所在包名來註冊映射接口的,所以映射文件與映射接口需要同名且目錄,如下圖所示:

具體的原因會在下文的源碼分析中給出。

二、加載映射文件的源碼分析

在淺析 MyBatis 的配置加載流程中已經知道,使用 MyBatis 時會先讀取配置文件 mybatis-config.xml 為字符流或者字節流,然後通過 SqlSessionFactoryBuilder 基於配置文件的字符流或字節流來構建 SqlSessionFactory

在這整個過程中,會解析 mybatis-config.xml 並將解析結果豐富進 Configuration,且 ConfigurationMyBatis 中是一個單例,無論是配置文件的解析結果,還是映射文件的解析結果,亦或者是映射接口的解析結果,最終都會緩存在 Configuration 中。

接著淺析MyBatis的配置加載流程這篇文章末尾繼續講,配置文件的解析發生在 XMLConfigBuilderparseConfiguration() 方法中,如下所示:

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"));
        // 根據mappers標籤的屬性,找到映射文件/映射接口並解析
        mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
        throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
}

如上所示,在解析 MyBatis 的配置文件時,會根據配置文件中的 <mappers> 標籤的屬性來找到映射文件/映射接口並進行解析。如下是 mapperElement() 方法的實現:

private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
        for (XNode child : parent.getChildren()) {
            if ("package".equals(child.getName())) {
                // 處理package子節點
                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) {
                    // 處理設置了resource屬性的mapper子節點
                    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) {
                    // 處理設置了url屬性的mapper子節點
                    ErrorContext.instance().resource(url);
                    InputStream inputStream = Resources.getUrlAsStream(url);
                    XMLMapperBuilder mapperParser = new XMLMapperBuilder(
                            inputStream, configuration, url, configuration.getSqlFragments());
                    mapperParser.parse();
                } else if (resource == null && url == null && mapperClass != null) {
                    // 處理設置了class屬性的mapper子節點
                    Class<?> mapperInterface = Resources.classForName(mapperClass);
                    configuration.addMapper(mapperInterface);
                } else {
                    // 同時設置了mapper子節點的兩個及以上的屬性時,報錯
                    throw new BuilderException(
                            "A mapper element may only specify a url, resource or class, but not more than one.");
                }
            }
        }
    }
}

結合示例中的配置文件,那麼在 mapperElement() 方法中應該進入處理package子節點的分支,所以繼續往下看,ConfigurationaddMappers(String packageName) 方法如下所示:

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

mapperRegistryConfiguration 內部的成員變量,其內部有三個重載的 addMappers() 方法,首先看 addMappers(String packageName) 方法,如下所示:

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

繼續往下,addMappers(String packageName, Class<?> superType) 的實現如下所示:

public void addMappers(String packageName, Class<?> superType) {
    ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
    resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
    // 獲取包路徑下的映射接口的Class對象
    Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
    for (Class<?> mapperClass : mapperSet) {
        addMapper(mapperClass);
    }
}

最後,再看下 addMapper(Class<T> type) 的實現,如下所示:

public <T> void addMapper(Class<T> type) {
    if (type.isInterface()) {
        // 判斷knownMappers中是否已經有當前映射接口
        // knownMappers是一個map存儲結構,key為映射接口Class對象,value為MapperProxyFactory
        // MapperProxyFactory為映射接口對應的動態代理工廠
        if (hasMapper(type)) {
            throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
        }
        boolean loadCompleted = false;
        try {
            knownMappers.put(type, new MapperProxyFactory<>(type));
            // 依靠MapperAnnotationBuilder來完成映射文件和映射接口中的Sql解析
            // 先解析映射文件,再解析映射接口
            MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
            parser.parse();
            loadCompleted = true;
        } finally {
            if (!loadCompleted) {
                knownMappers.remove(type);
            }
        }
    }
}

上面三個 addMapper() 方法一層一層的調用下來,實際就是根據配置文件中 <mappers> 標籤的 <package> 子標籤設置的映射文件/映射接口所在包的全限定名來獲取映射接口的 Class 對象,然後基於每個映射接口的 Class 對象來創建一個 MapperProxyFactory

顧名思義,MapperProxyFactory 是映射接口的動態代理工廠,負責為對應的映射接口生成動態代理類,這裡先簡要看一下 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);
    }

}

很標準的基於 JDK 動態代理的實現,所以可以知道,MyBatis 會為每個映射接口創建一個 MapperProxyFactory,然後將映射接口與 MapperProxyFactory 以鍵值對的形式存儲在 MapperRegistryknownMappers 緩存中,然後 MapperProxyFactory 會為映射接口基於 JDK 動態代理的方式生成代理類。

至於如何生成,將在第三小節中對 MapperProxyFactory 進一步分析。

繼續之前的流程,為映射接口創建完 MapperProxyFactory 之後,就應該對映射文件和映射接口中的 SQL 進行解析,解析依靠的類為 MapperAnnotationBuilder,其類圖如下所示:

所以一個映射接口對應一個 MapperAnnotationBuilder,並且每個 MapperAnnotationBuilder 中持有全局唯一的 Configuration 類,解析結果會豐富進 Configuration 中。MapperAnnotationBuilder 的解析方法 parse() 如下所示:

public void parse() {
    String resource = type.toString();
    // 判斷映射接口是否解析過,沒解析過才繼續往下執行
    if (!configuration.isResourceLoaded(resource)) {
        // 先解析映射文件中的Sql語句
        loadXmlResource();
        // 將當前映射接口添加到緩存中,以表示當前映射接口已經被解析過
        configuration.addLoadedResource(resource);
        assistant.setCurrentNamespace(type.getName());
        parseCache();
        parseCacheRef();
        // 解析映射接口中的Sql語句
        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();
}

按照 parse() 方法的執行流程,會先解析映射文件中的 SQL 語句,然後再解析映射接口中的 SQL 語句,這裡以解析映射文件為例,進行說明。loadXmlResource() 方法實現如下:

private void loadXmlResource() {
    if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
        // 根據映射接口的全限定名拼接成映射文件的路徑
        // 這也解釋了為什麼要求映射文件和映射接口在同一目錄
        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());
            // 解析映射文件
            xmlParser.parse();
        }
    }
}

loadXmlResource() 方法中,首先要根據映射接口的全限定名拼接出映射文件的路徑,拼接規則就是將全限定名的"."替換成"/",然後在末尾加上".xml",這也是為什麼要求映射文件和映射接口需要在同一目錄下且同名。

對於映射文件的解析,是依靠 XMLMapperBuilder,其類圖如下所示:

如圖所示,解析配置文件和解析映射文件的解析類均繼承於 BaseBuilder,然後 BaseBuilder 中持有全局唯一的 Configuration,所以解析結果會豐富進 Configuration,特別注意,XMLMapperBuilder 還有一個名為 sqlFragments 的緩存,用於存儲 <sql> 標籤對應的 XNode,這個 sqlFragmentsConfiguration 中的 sqlFragments 是同一份緩存,這一點切記,後面在分析處理 <include> 標籤時會用到。

XMLMapperBuilderparse() 方法如下所示:

public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
        // 從映射文件的<mapper>標籤開始進行解析
        // 解析結果會豐富進Configuration
        configurationElement(parser.evalNode("/mapper"));
        configuration.addLoadedResource(resource);
        bindMapperForNamespace();
    }

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

繼續看 configurationElement() 方法的實現,如下所示:

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"));
        // 解析<parameterMap>標籤生成ParameterMap並緩存到Configuration
        parameterMapElement(context.evalNodes("/mapper/parameterMap"));
        // 解析<resultMap>標籤生成ResultMap並緩存到Configuration
        resultMapElements(context.evalNodes("/mapper/resultMap"));
        // 將<sql>標籤對應的節點XNode保存到sqlFragments中
        // 實際也是保存到Configuration的sqlFragments緩存中
        sqlElement(context.evalNodes("/mapper/sql"));
        // 解析<select>,<insert>,<update>和<delete>標籤
        // 生成MappedStatement並緩存到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() 方法會將映射文件 <mapper> 下的各個子標籤解析成相應的類,然後緩存在 Configuration中。通常,在映射文件的 <mapper> 標籤下,常用的子標籤為 <parameterMap>,<resultMap>,<select>,<insert>,<update>和<delete>。

下面給出一個簡單的表格對這些標籤生成的類以及在 Configuration 中的唯一標識進行歸納。

標籤

解析生成的類

在Configuration中的唯一標識

<parameterMap>

ParameterMap

namespace + "." + 標籤id

<resultMap>

ResultMap

namespace + "." + 標籤id

<select>,<insert>,<update>,<delete>

MappedStatement

namespace + "." + 標籤id

上面表格中的namespace是映射文件<mapper>標籤的namespace屬性,因此對於映射文件里配置的parameterMapresultMap或者SQL執行語句,在MyBatis中的唯一標識就是namespace + "." + 標籤id

下圖可以直觀的展示<select>標籤解析後在Configuration中的形態:

下面以如何解析 <select>,<insert>,<update> 和 <delete> 標籤的內容為例,進行說明,buildStatementFromContext() 方法如下所示:

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) {
    // 每一個<select>,<insert>,<update>和<delete>標籤均會被創建一個MappedStatement
    // 每個MappedStatement會存放在Configuration的mappedStatements緩存中
    // mappedStatements是一個map,鍵為映射接口全限定名+"."+標籤id,值為MappedStatement
    for (XNode context : list) {
        final XMLStatementBuilder statementParser = new XMLStatementBuilder(
                    configuration, builderAssistant, context, requiredDatabaseId);
        try {
            statementParser.parseStatementNode();
        } catch (IncompleteElementException e) {
            configuration.addIncompleteStatement(statementParser);
        }
    }
}

對於每一個 <select>,<insert>,<update> 和 <delete> 標籤,均會創建一個 XMLStatementBuilder 來進行解析並生成 MappedStatement

同樣,看一下 XMLStatementBuilder的類圖,如下所示:

XMLStatementBuilder 中持有 <select>,<insert>,<update> 和 <delete>標籤對應的節點 XNode,以及幫助創建 MappedStatement 並豐富進 ConfigurationMapperBuilderAssistant 類。

下面看一下 XMLStatementBuilderparseStatementNode() 方法:

public void parseStatementNode() {
    // 獲取標籤id
    String id = context.getStringAttribute("id");
    String databaseId = context.getStringAttribute("databaseId");

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

    String nodeName = context.getNode().getNodeName();
    // 獲取標籤的類型,例如SELECT,INSERT等
    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);

    // 如果使用了<include>標籤,則將<include>標籤替換為匹配的<sql>標籤中的Sql片段
    // 匹配規則是在Configuration中根據namespace+"."+refid去匹配<sql>標籤
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    includeParser.applyIncludes(context.getNode());

    // 獲取輸入參數類型
    String parameterType = context.getStringAttribute("parameterType");
    Class<?> parameterTypeClass = resolveClass(parameterType);

    // 獲取LanguageDriver以支持實現動態Sql
    // 這裡獲取到的實際上為XMLLanguageDriver
    String lang = context.getStringAttribute("lang");
    LanguageDriver langDriver = getLanguageDriver(lang);

    processSelectKeyNodes(id, parameterTypeClass, langDriver);

    // 獲取KeyGenerator
    KeyGenerator keyGenerator;
    String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
    keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
    // 先從緩存中獲取KeyGenerator
    if (configuration.hasKeyGenerator(keyStatementId)) {
        keyGenerator = configuration.getKeyGenerator(keyStatementId);
    } else {
        // 緩存中如果獲取不到,則根據useGeneratedKeys的配置決定是否使用KeyGenerator
        // 如果要使用,則MyBatis中使用的KeyGenerator為Jdbc3KeyGenerator
        keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
            configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
            ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
    }

    // 通過XMLLanguageDriver創建SqlSource,可以理解為Sql語句
    // 如果使用到了<if>,<foreach>等標籤進行動態Sql語句的拼接,則創建出來的SqlSource為DynamicSqlSource
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    StatementType statementType = StatementType
            .valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    // 獲取<select>,<insert>,<update>和<delete>標籤上的屬性
    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");

    // 根據上面獲取到的參數,創建MappedStatement並添加到Configuration中
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered,
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}

parseStatementNode() 方法整體流程稍長,總結概括起來該方法做了如下幾件事情:

  • 將 <include> 標籤替換為其指向的 SQL 片段;
  • 如果未使用動態 SQL,則創建 RawSqlSource 以保存 SQL 語句,如果使用了動態 SQL(例如使用了<if>,<foreach>等標籤),則創建DynamicSqlSource以支持SQL語句的動態拼接;
  • 獲取<select>,<insert>,<update>和<delete>標籤上的屬性;
  • 將獲取到的 SqlSource 以及標籤上的屬性傳入 MapperBuilderAssistantaddMappedStatement() 方法,以創建 MappedStatement 並添加到 Configuration 中。

MapperBuilderAssistant 是最終創建 MappedStatement 以及將 MappedStatement 添加到 Configuration 的處理類,其 addMappedStatement() 方法如下所示:

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

    // 拼接出MappedStatement的唯一標識
    // 規則是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);
    }

    // 創建MappedStatement
    MappedStatement statement = statementBuilder.build();
    // 將MappedStatement添加到Configuration中
    configuration.addMappedStatement(statement);
    return statement;
}

至此,解析 <select>,<insert>,<update> 和 <delete> 標籤的內容然後生成 MappedStatement 並添加到 Configuration 的流程分析完畢。

實際上,解析 <parameterMap> 標籤,解析 <resultMap> 標籤的大體流程和上面基本一致,最終都是藉助 MapperBuilderAssistant 生成對應的類(例如ParameterMapResultMap)然後再緩存到 Configuration 中,且每種解析生成的類在對應緩存中的唯一標識為namespace + "." + 標籤id

最後,回到本小節開頭,即 XMLConfigBuilder 中的 mapperElement() 方法,在這個方法中,會根據配置文件中 <mappers> 標籤的子標籤的不同,進入不同的分支執行加載映射文件/映射接口的邏輯,實際上,整個加載映射文件/加載映射接口的流程是一個環形,可以用下圖進行示意:

XMLConfigBuilder 中的 mapperElement() 方法的不同分支只是從不同的入口進入整個加載的流程中,同時 MyBatis 會在每個操作執行前判斷是否已經做過當前操作,做過就不再重複執行,因此保證了整個環形處理流程只會執行一遍,不會死循環。

如果是在項目中基於 JavaConfig 的方式來配置 MyBatis,那麼通常會直接對 Configuration 設置參數值,以及調用 ConfigurationaddMappers(String packageName) 來加載映射文件/映射接口。

三、MyBatis中的動態代理

已知在 MapperRegistry 中有一個叫做 knownMappersmap 緩存,其鍵為映射接口的 Class 對象,值為 MyBatis 為映射接口創建的動態代理工廠 MapperProxyFactory,當調用映射接口定義的方法執行資料庫操作時,實際調用請求會由 MapperProxyFactory 為映射接口生成的代理對象來完成。這裡給出 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);
    }

}

MapperProxyFactory 中,mapperInterface 為映射接口的 Class 對象,methodCache是一個 map 緩存,其鍵為映射接口的方法對象,值為這個方法對應的 MapperMethodInvoker,實際上 SQL 的執行最終會由 MapperMethodInvoker 完成,後面會詳細說明。

現在再觀察 MapperProxyFactory 中兩個重載的 newInstance() 方法,可以知道這是基於 JDK 的動態代理,在 public T newInstance(SqlSession sqlSession) 這個方法中,會創建 MapperProxy,並將其作為參數調用 protected T newInstance(MapperProxy<T> mapperProxy) 方法,在該方法中會使用 ProxynewProxyInstance() 方法創建動態代理對象,所以可以斷定,MapperProxy肯定會實現 InvocationHandler接口。

MapperProxy的類圖如下所示:

果然,MapperProxy 實現了 InvocationHandler 接口,並在創建 MapperProxyMapperProxyFactory 會將其持有的 methodCache 傳遞給 MapperProxy,因此 methodCache的實際的讀寫是由 MapperProxy 來完成。下面看一下 MapperProxy 實現的 invoke() 方法,如下所示:

@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 {
            // 從methodCache中根據方法對象獲取MapperMethodInvoker來執行Sql
            // 如果獲取不到,則創建一個MapperMethodInvoker並添加到methodCache中,再執行Sql
            return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
        }
    } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
    }
}

基於 JDK 動態代理的原理可以知道,當調用 JDK 動態代理生成的映射接口的代理對象的方法時,最終調用請求會發送到 MapperProxyinvoke() 方法,在 MapperProxyinvoke() 方法中實際就是根據映射接口被調用的方法的對象去 methodCache 緩存中獲取 MapperMethodInvoker 來實際執行請求。

如果獲取不到那麼就先為當前的方法對象創建一個 MapperMethodInvoker 並加入 methodCache 緩存,然後再用創建出來的 MapperMethodInvoker 去執行請求。cachedInvoker() 方法實現如下所示:

private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
    try {
        MapperProxy.MapperMethodInvoker invoker = methodCache.get(method);
        // 從methodCache緩存中獲取到MapperMethodInvoker不為空則直接返回
        if (invoker != null) {
            return invoker;
        }

        // 從methodCache緩存中獲取到MapperMethodInvoker為空
        // 則創建一個MapperMethodInvoker然後添加到methodCache緩存,並返回
        return methodCache.computeIfAbsent(method, m -> {
            // JDK1.8接口中的default()方法處理邏輯
            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 {
                // 先創建一個MapperMethod
                // 再將MapperMethod作為參數創建PlainMethodInvoker
                return new MapperProxy.PlainMethodInvoker(
                    new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
            }
        });
    } catch (RuntimeException re) {
        Throwable cause = re.getCause();
        throw cause == null ? re : cause;
    }
}

MapperMethodInvoker 是接口,通常創建出來的 MapperMethodInvokerPlainMethodInvoker,看一下 PlainMethodInvoker 的構造函數。

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

因此創建 PlainMethodInvoker 時,需要先創建 MapperMethod,而 PlainMethodInvoker在執行時也是將執行的請求傳遞給 MapperMethod,所以繼續往下,MapperMethod 的構造函數如下所示:

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

創建 MapperMethod 時需要傳入的參數為映射接口的Class對象映射接口被調用的方法的對象配置類 Configuration,在 MapperMethod 的構造函數中,會基於上述三個參數創建 SqlCommandMethodSignature

  1. SqlCommand 主要是保存和映射接口被調用方法所關聯的 MappedStatement 的信息;
  2. MethodSignature 主要是存儲映射接口被調用方法的參數信息和返回值信息。

先看一下 SqlCommand 的構造函數,如下所示:

public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
    // 獲取映射接口被調用方法的方法名
    final String methodName = method.getName();
    // 獲取聲明被調用方法的接口的Class對象
    final Class<?> declaringClass = method.getDeclaringClass();
    // 獲取和映射接口被調用方法關聯的MappedStatement對象
    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 {
        // 將MappedStatement的id賦值給SqlCommand的name欄位
        name = ms.getId();
        // 將MappedStatement的Sql命令類型賦值給SqlCommand的type欄位
        // 比如SELECT,INSERT等
        type = ms.getSqlCommandType();
        if (type == SqlCommandType.UNKNOWN) {
            throw new BindingException("Unknown execution method for: " + name);
        }
    }
}

構造函數中主要做了這些事情:

  1. 先獲取和被調用方法關聯的 MappedStatement 對象;
  2. 然後將 MappedStatementid 欄位賦值給 SqlCommandname 欄位;
  3. 最後將 MappedStatementsqlCommandType 欄位賦值給 SqlCommandtype 欄位。

這樣一來,SqlCommand 就具備了和被調用方法關聯的 MappedStatement 的信息。那麼如何獲取和被調用方法關聯的 MappedStatement 對象呢,繼續看 resolveMappedStatement() 的實現,如下所示:

private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName,
                                               Class<?> declaringClass, Configuration configuration) {
    // 根據接口全限定名+"."+方法名拼接出MappedStatement的id
    String statementId = mapperInterface.getName() + "." + methodName;
    // 如果Configuration中緩存了statementId對應的MappedStatement,則直接返回這個MappedStatement
    // 這是遞歸的終止條件之一
    if (configuration.hasStatement(statementId)) {
        return configuration.getMappedStatement(statementId);
    } else if (mapperInterface.equals(declaringClass)) {
        // 當前mapperInterface已經是聲明被調用方法的接口的Class對象,且未匹配到緩存的MappedStatement,返回null
        // 這是resolveMappedStatement()遞歸的終止條件之一
        return null;
    }
    // 遞歸調用
    for (Class<?> superInterface : mapperInterface.getInterfaces()) {
        if (declaringClass.isAssignableFrom(superInterface)) {
            MappedStatement ms = resolveMappedStatement(superInterface, methodName,
                    declaringClass, configuration);
            if (ms != null) {
                return ms;
            }
        }
    }
    return null;
}

resolveMappedStatement() 方法會根據接口全限定名 + "." + "方法名" 作為 statementIdConfiguration 的緩存中獲取 MappedStatement,同時 resolveMappedStatement() 方法會從映射接口遞歸的遍歷到聲明被調用方法的接口,遞歸的終止條件如下所示:

  • 根據接口全限定名 + "." + "方法名" 作為 statementIdConfiguration 的緩存中獲取到了 MappedStatement
  • 從映射接口遞歸遍歷到了聲明被調用方法的接口,且根據聲明被調用方法的接口的全限定名 + "." + "方法名" 作為 statementIdConfiguration 的緩存中獲取不到 MappedStatement

上面說得比較繞,下面用一個例子說明一下 resolveMappedStatement() 方法這樣寫的原因。下圖是映射接口和映射文件所在的包路徑:

BaseMapperBookBaseMapperBookMapper 的關係如下圖所示:

那麼 MyBatis 會為 BaseMapperBookBaseMapperBookMapper 都生成一個 MapperProxyFactory,如下所示:

同樣,在 Configuration 中也會緩存著解析 BookBaseMapper.xml 映射文件所生成的 MappedStatement,如下所示:

MyBatis3.4.2及以前的版本,只會根據映射接口的全限定名 + "." + 方法名聲明被調用方法的接口的全限定名 + "." + 方法名ConfigurationmappedStatements 緩存中獲取 MappedStatement

那麼按照這樣的邏輯,BookMapper 對應的 SqlCommand 就只會根據 com.mybatis.learn.dao.BookMapper.selectAllBookscom.mybatis.learn.dao.BaseMapper.selectAllBooksmappedStatements 緩存中獲取 MappedStatement

那麼結合上面圖示給出的 mappedStatements 緩存內容,是無法獲取到 MappedStatement 的,因此在 MyBatis3.4.3及之後的版本中,採用了 resolveMappedStatement() 方法中的邏輯,以支持繼承了映射接口的接口對應的 SqlCommand 也能和映射接口對應的 MappedStatement 相關聯

對於 SqlCommand 的分析到此為止,而 MapperMethod 中的 MethodSignature 主要是用於存儲被調用方法的參數信息和返回值信息,這裡也不再贅述。

最後對映射接口的代理對象執行方法時的一個執行鏈進行說明。

首先,通過 JDK 動態代理的原理我們可以知道,調用代理對象的方法時,調用請求會發送到代理對象中的 InvocationHandler,在 MyBatis 中,調用映射接口的代理對象的方法的請求會發送到 MapperProxy,所以調用映射接口的代理對象的方法時,MapperProxyinvoke() 方法會執行,實現如下所示:

@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 {
            // 從methodCache中根據方法對象獲取MapperMethodInvoker來執行Sql
            // 如果獲取不到,則創建一個MapperMethodInvoker並添加到methodCache中,再執行Sql
            return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
        }
    } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
    }
}

所以到這裡,MyBatis 就和傳統的 JDK 動態代理產生了一點差別。傳統 JDK 動態代理通常在其 InvocationHandler 中會在被代理對象方法執行前和執行後增加一些裝飾邏輯,而在 MyBatis 中,是不存在被代理對象的。

只有被代理接口,所以也不存在調用被代理對象的方法這一邏輯,取而代之的是根據被調用方法的方法對象獲取 MapperMethodInvoker 並執行其 invoke() 方法,通常獲取到的是 PlainMethodInvoker,所以繼續看 PlainMethodInvokerinvoke() 方法,如下所示:

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

PlainMethodInvokerinvoke() 方法也沒有什麼邏輯,就是繼續調用其 MapperMethodexecute() 方法,而通過上面的分析已經知道,MapperMethod 中的 SqlCommand 關聯著 MappedStatement

MappedStatement 中包含著和被調用方法所關聯的 SQL 信息,結合著 SqlSession,就可以完成對資料庫的操作。關於如何對資料庫操作,將在後續的文章中介紹,本篇文章對於 MyBatis 中的動態代理的分析就到此為止。

最後以一張圖歸納一下 MyBatis 中的動態代理執行流程,如下所示:

總結

1. 每個 CRUD 標籤唯一對應一個 MappedStatement 對象

具體對應關係可以用下圖進行示意:

映射文件中,每一個 <select>,<insert>,<update> 和 <delete> 標籤均會被創建一個 MappedStatement 並存放在 ConfigurationmappedStatements 緩存中,MappedStatement 中主要包含著這個標籤下的 SQL 語句,這個標籤的參數信息和出參信息等。

每一個 MappedStatement 的唯一標識為 namespace + "." + 標籤 id,這樣設置唯一標識的原因是為了調用映射接口的方法時能夠根據映射接口的權限定名 + "." + "方法名"獲取到和被調用方法關聯的 MappedStatement

因此,映射文件的 namespace 需要和映射接口的全限定名一致,每個 <select>,<insert>,<update> 和 <delete> 標籤均對應一個映射接口的方法,每個 <select>,<insert>,<update> 和 <delete> 標籤的 id 需要和映射接口的方法名一致;

2. 每個映射接口對應一個 JDK 動態代理對象

調用 MyBatis 映射接口的方法時,調用請求的實際執行是由基於 JDK 動態代理為映射接口生成的代理對象來完成,映射接口的代理對象由 MapperProxyFactorynewInstance() 方法生成,每個映射接口對應一個 MapperProxyFactory,對應一個 JDK 動態代理對象;

3. MyBatis 中的動態代理是對接口的代理

MyBatisJDK 動態代理中,是不存在被代理對象的,是對接口的代理。MapperProxy 實現了 InvocationHandler 接口,因此 MapperProxyMyBatisJDK 動態代理中扮演調用處理器的角色,即調用映射接口的方法時。

實際上是調用的 MapperProxy 實現的 invoke() 方法,又因為不存在被代理對象,所以在 MapperProxyinvoke() 方法中,並沒有去調用被代理對象的方法,而是會基於映射接口和被調用方法的方法對象生成 MapperMethod 並執行 MapperMethodexecute() 方法,即調用映射接口的方法的請求會發送到 MapperMethod

可以理解為映射接口的方法由 MapperMethod 代理

作者:半夏之沫
連結:https://juejin.cn/post/7203925850398883896
來源:稀土掘金

關鍵字: