載入Mapper對映檔案

LZC發表於2020-07-24

XMLMapperBuilder用來解析XML中的SQL,MapperAnnotationBuilder用來解析Mapper介面中的註解SQL。這裡只分析一下XMLMapperBuilder

以解析 resource 為例,即解析XML檔案。解析完XML檔案中的SQL,還會去解析該XML檔案對應的Mapper介面中的註解SQL。

// XMLConfigBuilder.java
private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
        for (XNode child : parent.getChildren()) {
            if ("package".equals(child.getName())) {
                String mapperPackage = child.getStringAttribute("name");
                configuration.addMappers(mapperPackage);
            } else {
                String resource = child.getStringAttribute("resource");
                String url = child.getStringAttribute("url");
                String mapperClass = child.getStringAttribute("class");
                if (resource != null && url == null && mapperClass == null) {
                    ErrorContext.instance().resource(resource);
                    // 獲取Mapper.xml的檔案流
                    InputStream inputStream = Resources.getResourceAsStream(resource);
                    // 構造 XMLMapperBuilder 物件
                    XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
                    // 解析 Mapper.xml 檔案
                    mapperParser.parse();
                } 
                // 省略......
            }
        }
    }
}

parse

解析 Mapper XML 配置檔案

// XMLMapperBuilder.java
public void parse() {
    // 判斷當前 Mapper 是否已經載入過
    if (!configuration.isResourceLoaded(resource)) {
        // 解析 <mapper /> 節點
        configurationElement(parser.evalNode("/mapper"));
        // 標記該 Mapper 已經載入過
        configuration.addLoadedResource(resource);

        // 繫結 Mapper.xml 對應的 Mapper 介面,並解析 Mapper 介面中的SQL註解
        // 第一步:
        // 該方法會將Mapper介面儲存到 Configuration 的 MapperRegistry 屬性裡面
        // MapperRegistry 裡面有一個 Map<Class<?>, MapperProxyFactory<?>> knownMappers
        // 最終透過 knownMappers.put(type, new MapperProxyFactory<T>(type)); 註冊Mapper代理
        // 這裡為每一個Mapper介面生成了一個 MapperProxyFactory 
        // 第二步:解析 Mapper 介面中SQL語句並封裝成 MappedStatement 物件
        bindMapperForNamespace();
    }
    parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();
}

configurationElement

// XMLMapperBuilder.java
private void configurationElement(XNode context) {
    try {
        // 獲得 namespace 屬性
        String namespace = context.getStringAttribute("namespace");
        if (namespace == null || namespace.equals("")) {
            throw new BuilderException("Mapper's namespace cannot be empty");
        }
        // 設定 namespace 屬性
        builderAssistant.setCurrentNamespace(namespace);
        // 解析 <cache-ref /> 節點
        cacheRefElement(context.evalNode("cache-ref"));
        // 解析 <cache /> 節點,快取相關的一些屬性設定
        cacheElement(context.evalNode("cache"));
        // 已廢棄!老式風格的引數對映。
        parameterMapElement(context.evalNodes("/mapper/parameterMap"));
        //  解析 <resultMap /> 節點
        resultMapElements(context.evalNodes("/mapper/resultMap"));
        // 解析 <sql /> 節點
        sqlElement(context.evalNodes("/mapper/sql"));
        // 解析 <select /> <insert /> <update /> <delete /> 
        buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
        throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
    }
}

主要分析buildStatementFromContext解析流程

buildStatementFromContext

解析

// XMLMapperBuilder.java
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 /> 節點們,
    // 依次建立 XMLStatementBuilder 物件,執行解析
    for (XNode context : list) {
        // 建立 XMLStatementBuilder 物件
        final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
        try {
            // 執行解析
            statementParser.parseStatementNode();
        } catch (IncompleteElementException e) {
            configuration.addIncompleteStatement(statementParser);
        }
    }
}

org.apache.ibatis.builder.xml.XMLStatementBuilder ,繼承 BaseBuilder 抽象類,Statement XML 配置構建器,主要負責解析 Statement 配置,即 <select /><insert /><update /><delete /> 標籤。

建構函式

public class XMLStatementBuilder extends BaseBuilder {
    // 儲存當前 Mapper.xml 的一些屬性資訊,如namespace
    private final MapperBuilderAssistant builderAssistant;
    // 當前 XML 節點,例如:<select />、<insert />、<update />、<delete /> 標籤
    private final XNode context;
    // 當前節點的 databaseId
    private final String requiredDatabaseId;

    public XMLStatementBuilder(Configuration configuration, MapperBuilderAssistant builderAssistant, XNode context, String databaseId) {
        super(configuration);
        this.builderAssistant = builderAssistant;
        this.context = context;
        this.requiredDatabaseId = databaseId;
    }
}

parseStatementNode

執行 Statement 解析

// XMLStatementBuilder
public void parseStatementNode() {
    // 獲取當前節點的 id 屬性
    String id = context.getStringAttribute("id");
    // 獲取當前節點的 databaseId 屬性
    String databaseId = context.getStringAttribute("databaseId");
    // 判斷 databaseId 是否匹配
    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
        return;
    }
    // 獲取當前節點的各種屬性
    Integer fetchSize = context.getIntAttribute("fetchSize");
    Integer timeout = context.getIntAttribute("timeout");
    String parameterMap = context.getStringAttribute("parameterMap");
    String parameterType = context.getStringAttribute("parameterType");
    Class<?> parameterTypeClass = resolveClass(parameterType);
    String resultMap = context.getStringAttribute("resultMap");
    String resultType = context.getStringAttribute("resultType");
    String lang = context.getStringAttribute("lang");
    LanguageDriver langDriver = getLanguageDriver(lang);

    Class<?> resultTypeClass = resolveClass(resultType);
    String resultSetType = context.getStringAttribute("resultSetType");
    StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);

    // 獲得 SQL 對應的 SqlCommandType 列舉值
    // select、insert、update、delete
    String nodeName = context.getNode().getNodeName();
    SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
    // 判斷是否為 select 操作
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;

    // 獲取一些屬性
    boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
    boolean useCache = context.getBooleanAttribute("useCache", isSelect);
    boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

    // 建立 XMLIncludeTransformer 物件,並替換 <include /> 標籤相關的內容
    // Include Fragments before parsing
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    includeParser.applyIncludes(context.getNode());

    // 解析 <selectKey /> 標籤
    // Parse selectKey after includes and remove them.
    processSelectKeyNodes(id, parameterTypeClass, langDriver);

    // 建立 SqlSource,儲存了完整的SQL資訊
    // 此時 <selectKey> 和 <include>標籤已經被解析過了並且被刪除了
    // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);

    // 獲得 KeyGenerator 物件
    // (僅適用於 insert 和 update)這會令 MyBatis 使用 JDBC 的 getGeneratedKeys 
    // 方法來取出由資料庫內部生成的主鍵
    // (比如:像 MySQL 和 SQL Server 這樣的關係型資料庫管理系統的自動遞增欄位),預設值:false。
    String resultSets = context.getStringAttribute("resultSets");
    String keyProperty = context.getStringAttribute("keyProperty");
    String keyColumn = context.getStringAttribute("keyColumn");
    KeyGenerator keyGenerator;
    String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
    keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);

    // 先從 configuration 中獲得 KeyGenerator 物件。如果存在,說明已經配置過了
    if (configuration.hasKeyGenerator(keyStatementId)) {
        keyGenerator = configuration.getKeyGenerator(keyStatementId);
    } else {
        // 根據標籤屬性,判斷是使用Jdbc3KeyGenerator 還是 NoKeyGenerator 物件
        keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
                                                   configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
            ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
    }

    // 建立 MappedStatement 物件
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
                                        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
                                        resultSetTypeEnum, flushCache, useCache, resultOrdered, 
                                        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}

解析完節點資訊後,最後會生成 MappedStatement 物件儲存到Configuration中。構建MappedStatement 物件的方法在MapperBuilderAssistant類裡面

構建 MappedStatement 物件

// MapperBuilderAssistant.java
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");
    }
    // 獲得 id 編號,格式為 ${namespace}.${id}
    id = applyCurrentNamespace(id, false);
    // 判斷是否為 select 操作
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    // 建立 MappedStatement.Builder 物件
    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 ,並設定到 MappedStatement.Builder 中
    ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
    if (statementParameterMap != null) {
        statementBuilder.parameterMap(statementParameterMap);
    }
    // 建立 MappedStatement 物件
    MappedStatement statement = statementBuilder.build();
    // 新增到 configuration 中的 Map<String, MappedStatement> mappedStatements
    // key 為 id
    // value 為 MappedStatement
    configuration.addMappedStatement(statement);
    return statement;
}

org.apache.ibatis.mapping.MappedStatement 儲存了每個 <select /><insert /><update /><delete /> 節點的詳細資訊。

public final class MappedStatement {
    // Mapper.xml配置檔名,如:UserMapper.xml
    // Mapper介面檔名,如com/example/demo/UserMapper.java
    private String resource;
    // 全域性配置
    private Configuration configuration;
    // 節點的id屬性加名稱空間,如:com.example.demo.UserMapper.getUserByUserName
    private String id;
    private Integer fetchSize;
    private Integer timeout;
    // 操作SQL的物件的型別
    // STATEMENT: 直接操作SQL,不進行預編譯
    // PREPARED: 預處理引數,進行預編譯,獲取資料
    // CALLABLE: 執行儲存過程
    private StatementType statementType;
    // 返回結果型別
    // FORWARD_ONLY:結果集的遊標只能向下滾動
    // SCROLL_INSENSITIVE:結果集的遊標可以上下移動,當資料庫變化時當前結果集不變
    // SCROLL_SENSITIVE:返回可滾動的結果集,當資料庫變化時,當前結果集同步改變
    private ResultSetType resultSetType;
    // sql語句
    private SqlSource sqlSource;
    private Cache cache;
    private ParameterMap parameterMap;
    // 返回結果型別
    private List<ResultMap> resultMaps;
    private boolean flushCacheRequired;
    private boolean useCache;
    private boolean resultOrdered;
    // sql語句的型別,如select、update、delete、insert
    private SqlCommandType sqlCommandType;
    private KeyGenerator keyGenerator;
    private String[] keyProperties;
    private String[] keyColumns;
    private boolean hasNestedResultMaps;
    private String databaseId;
    private Log statementLog;
    private LanguageDriver lang;
    private String[] resultSets;

    public BoundSql getBoundSql(Object parameterObject) {
        // 獲得 BoundSql 物件
        BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
        // 忽略這一步,因為 <parameterMap /> 已經廢棄
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        if (parameterMappings == null || parameterMappings.isEmpty()) {
            boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject);
        }

        // check for nested result maps in parameter mappings (issue #30)
        for (ParameterMapping pm : boundSql.getParameterMappings()) {
            String rmId = pm.getResultMapId();
            if (rmId != null) {
                ResultMap rm = configuration.getResultMap(rmId);
                if (rm != null) {
                    hasNestedResultMaps |= rm.hasNestedResultMaps();
                }
            }
        }

        return boundSql;
    }
}

org.apache.ibatis.mapping.SqlSource,代表從 Mapper XML 或介面方法註解上讀取的一條 SQL 內容。程式碼如下:

public interface SqlSource {
    // 根據使用者傳入的入參,返回 BoundSql 物件
    BoundSql getBoundSql(Object parameterObject);
}
public class BoundSql {
    // 儲存sql語句,例如:select * from user where username = ?
    private final String sql;
    // 儲存對#{}字串的解析結果
    private final List<ParameterMapping> parameterMappings;
    // 使用者傳遞進來的引數
    private final Object parameterObject;
    private final Map<String, Object> additionalParameters;
    private final MetaObject metaParameters;
}

BoundSql語句的解析主要是透過對#{}字元的解析,將其替換成?。最後均包裝成預表示式供PrepareStatement呼叫執行

#{}中的key屬性以及相應的引數對映,比如javaType、jdbcType等資訊均儲存至BoundSql的parameterMappings屬性中供最後的預表示式物件PrepareStatement賦值使用

MyBatis初始化完成後,每一個介面方法都會被封裝成了一個MappedStatement物件,這個物件裡面包含了執行SQL語句的詳細資訊。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章