1. 程式人生 > >Mybatis詳解系列(二)--Mybatis如何載入配置及初始化

Mybatis詳解系列(二)--Mybatis如何載入配置及初始化

簡介

Mybatis 是一個持久層框架,它對 JDBC 進行了高階封裝,使我們的程式碼中不會出現任何的 JDBC 程式碼,另外,它還通過 xml 或註解的方式將 sql 從 DAO/Repository 層中解耦出來,除了這些基本功能外,它還提供了動態 sql、延遲載入、快取等功能。 相比 Hibernate,Mybatis 更面向資料庫,可以靈活地對 sql 語句進行優化。

前面已經說完 mybatis 的使用( Mybatis詳解系列(一)--持久層框架解決了什麼及如何使用Mybatis ),現在開始分析原始碼,和使用例子一樣,我用的 mybatis 是 3.5.4 版本的。考慮連貫性,我會按下面的順序來展開分析,計劃兩篇部落格寫完,本文只涉及第一點內容:

  1. 載入配置、初始化SqlSessionFactory
  2. 獲取SqlSessionMapper
  3. 執行Mapper方法。

這個過程基本符合下面的程式碼的工作過程。

// 載入配置,初始化SqlSessionFactory物件
String resource = "Mybatis-config.xml";
InputStream in = Resources.getResourceAsStream(resource));
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(in);
// 獲取 SqlSession 和 Mapper
SqlSession sqlSession = sqlSessionFactory.openSession();
EmployeeMapper baseMapper = sqlSession.getMapper(EmployeeMapper.class);
// 執行Mapper方法
Employee employee = baseMapper.selectByPrimaryKey(id);
// do something

注意,考慮可讀性,文中部分原始碼經過刪減。

初始化的過程

這裡簡單概括下初始化的整個流程,如下圖。

  1. 構建 xml 的“節點樹”。XPathParser使用的是 JDK 自帶的 JAXP API來解析並構建Document物件,並且支援 XPath 功能。
  2. 初始化Configuration物件的成員屬性。XMLConfigBuilder利用“節點樹”來構建Configuration物件(也會去解析註解的配置),Configuration物件包含了 configuration 檔案和 mapper 檔案的所有配置資訊。這部分內容比較難,尤其是初始化 mapper 相關的配置。
  3. 建立SqlSessionFactory
    SqlSessionFactoryBuilder利用構建好的Configuration物件來建立SqlSessionFactory

上面的過程只要進入到SqlSessionFactoryBuilder.build(InputStream)方法就可以直觀的看到。

public SqlSessionFactory build(InputStream inputStream) {
    return build(inputStream, null, null);
}
// 入參裡我們可以指定使用哪個環境,還可以傳入properties來“覆蓋”xml中<properties>變數
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
        // 1. 構建XMLConfigBuilder物件,這個過程會構建Document物件
        XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
        // 2. 構建Configuration物件後,然後呼叫build(Configuration)
        return build(parser.parse());
    } catch(Exception e) {
        throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
        ErrorContext.instance().reset();
        try {
            inputStream.close();
        } catch(IOException e) {
            // Intentionally ignore. Prefer previous error.
        }
    }
}
public SqlSessionFactory build(Configuration config) {
    // 3. 直接使用構造方法構建DefaultSqlSessionFactory物件
    return new DefaultSqlSessionFactory(config);
}

接下來會具體分析第1和2點的程式碼,第3點比較簡單,就不展開了。

構建xml節點樹

XMLConfigBuilder使用XPathParser來解析 xml 獲得“節點樹”,它本身會通過“節點樹”的配置資訊來進行初始化操作。現在我們進入到XMLConfigBuilder的構造方法:

private final XPathParser parser;
private String environment;
public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
    // 構建XPathParser物件,構建時去解析xml
    this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
}
// 這裡只是初始化XMLConfigBuilder的幾個成員屬性
private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
    // ······
}

XPathParser的構造方法裡將對 xml 進行解析,如下。點進 XPathParser.createDocument(InputSource)方法就會發現 mybatis 使用的是 JAXP 的 API,這部分的內容就不在本文的討論範圍,感興趣可參考我的另一篇部落格: 原始碼詳解系列(三) ------ dom4j的使用和分析(重點對比和DOM、SAX的區別) 。

	private final Document document;
    private Properties variables;
	public XPathParser(Reader reader, boolean validation, Properties variables, EntityResolver entityResolver) {
        // 初始化一列成員屬性,沒必要看
		commonConstructor(validation, variables, entityResolver);
        // 構建Document物件,使用的是JAXP的API
        this.document = createDocument(new InputSource(reader));
  }

這裡補充說明下XMLMapperEntityResolver這個類。它是EntityResolver子類,xml 的解析會基於事件觸發對應的 Resolver 或 Handler,當解析到 dtd 等外部資源時會觸發EntityResolverresolveEntity方法。在XMLMapperEntityResolver.resolveEntity中,當解析到 mybatis-3-config.dtd、mybatis-3-mapper.dtd 等資源時,會直接從 classpath 下的 org/apache/ibatis/builder/xml/ 路徑獲取資源,而不需要通過 url 獲取。

注意,上面對構建的Document物件,只是 configuration 檔案的,並不包含 mapper 檔案。

先認識下Configuration這個類

我們已經拿到了配置資訊,接下來就是構建Configuration物件了。

在此之前,我們先認識下Configurantion這個類,如下圖。可以看到,這些成員屬性對應了 xml 檔案中各個配置項,接下來講的就是如何初始化這些屬性。

進入到XMLConfigBuilder.parse()方法,可以看到所有配置項的初始化順序。這裡的XNode類是 mybatis 對org.w3c.dom.Node的包裝,為後續操作 xml 節點提供了更加簡便的介面。

public Configuration parse() {
    if (parsed) {
        throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    // 標記已經解析過
    parsed = true;
    // 通過Document物件構建configuration節點的XNode物件,並構建Configurantion物件
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
}
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);
        // read it after objectFactory and objectWrapperFactory issue #631
        environmentsElement(root.evalNode("environments"));
        databaseIdProviderElement(root.evalNode("databaseIdProvider"));
        typeHandlerElement(root.evalNode("typeHandlers"));
        mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
        throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
}

接下來會挑其中幾個配置項展開分析,而不會每個都講到,重點關注 typeHandlers 和 mapper 節點的配置。

properties

properties 是 xml 中使用的全域性引數,可以在 xml 中顯式配置或引入外部 properties 檔案,也可以在構建SqlSessionFactory物件時通過方法入參傳入(比較少用),通過下面的程式碼可以知道:

  1. properties節點的屬性 resource 和 url 只能配置一個,兩個都配置會報錯;
  2. 不同方式配置會覆蓋,優先順序如下:方法入參方式 > xml 中引入外部 properties 檔案方式 > xml 中顯示配置方式,優先順序低的會被優先順序高的覆蓋。
private void propertiesElement(XNode context) throws Exception {
    if (context != null) {
        // 獲取xml裡顯式配置的所有property
        Properties defaults = context.getChildrenAsProperties();
        // 獲取resource和url屬性值
        String resource = context.getStringAttribute("resource");
        String url = context.getStringAttribute("url");
        // resource和url只能有一個
        if (resource != null && url != null) {
            throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference.  Please specify one or the other.");
        }
        // 新增resource或url指定資源的properties,如果相同,就覆蓋
        if (resource != null) {
            defaults.putAll(Resources.getResourceAsProperties(resource));
        } else if (url != null) {
            defaults.putAll(Resources.getUrlAsProperties(url));
        }
        // 新增方法入參的properties,如果相同,就覆蓋
        Properties vars = configuration.getVariables();
        if (vars != null) {
            defaults.putAll(vars);
        }
        // 重新設定XPathParser物件和Configuration物件裡的成員屬性,以備後面配置項使用
        parser.setVariables(defaults);
        configuration.setVariables(defaults);
    }
}

settings

setting 的初始化過程比較簡單,這裡我們重點關注下MetaClass這個類。

private final ReflectorFactory localReflectorFactory = new DefaultReflectorFactory();
private Properties settingsAsProperties(XNode context) {
    if (context == null) {
        return new Properties();
    }
    // 獲取settings子節點的配置資訊
    Properties props = context.getChildrenAsProperties();
    // 判斷該配置項是否存在,不合法會拋錯
    MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory);
    for (Object key : props.keySet()) {
        if (!metaConfig.hasSetter(String.valueOf(key))) {
            throw new BuilderException("The setting " + key + " is not known.  Make sure you spelled it correctly (case sensitive).");
        }
    }
    return props;
}
// 這裡就是直接初始化屬性了
private void settingsElement(Properties props) {	
    // ······
}

通常情況下,如果要判斷一個配置引數是否存在,可能會在程式碼中將引數集給寫死,但是 mybatis 沒有這麼做,它提供了一個非常好用的工具類--MetaClassMetaClass可以用來初始化某個類的引數集,例如Configuration,並且提供了這些引數的Invoker物件,通過它可以進行值的設定和獲取。這個類將在後續原始碼分析中多次出現。

typeAliases

TypeAliasRegistry,即別名註冊器,存放著 alias = Class 的鍵值對,這些別名僅限於在載入配置的時候使用。

我們可以通過兩種方式配置:package 和 typeAlias 的方式,而且這兩種方式可以共存。

private void typeAliasesElement(XNode parent) {
    if (parent != null) {
        // 遍歷typeAliases下的typeAlias或package節點
        for (XNode child : parent.getChildren()) {
            // 配置包的情況
            if ("package".equals(child.getName())) {
                String typeAliasPackage = child.getStringAttribute("name");
                // 使用包名註冊
                configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
            } else {
                // 配置具體類的情況
                String alias = child.getStringAttribute("alias");
                String type = child.getStringAttribute("type");
                try {
                    // 載入指定類
                    Class<?> clazz = Resources.classForName(type);
                    if (alias == null) {
                        // 如果沒有通過xml顯式設定別名,將讀取該類的Alias註解裡的value值
                        // 如果沒有通過xml或註解顯式設定別名,將使用該Class物件的simpleName小寫作為別名
                        typeAliasRegistry.registerAlias(clazz);
                    } else {
                        typeAliasRegistry.registerAlias(alias, clazz);
                    }
                } catch (ClassNotFoundException e) {
                    throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e);
                }
            }
        }
    }
}

這裡只看使用 package 註冊別名的情況,進入到TypeAliasRegistry.registerAliases(String)方法。通過以下程式碼可知,註冊別名時無法註冊介面或內部類。這裡 mybatis 又提供了一個好用的工具類--ResolverUtil,通過ResolverUtil我們可以獲取到指定包路徑下的介面、註解或指定類的子類。

public void registerAliases(String packageName) {
    // 查詢指定包名下Object的子類,並註冊別名
    registerAliases(packageName, Object.class);
}

public void registerAliases(String packageName, Class<?> superType) {
    ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
    // 查詢指定包名下superType的子類
    resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
    Set<Class<? extends Class<?>>> typeSet = resolverUtil.getClasses();
    
    for (Class<?> type : typeSet) {
        // 跳過內部類和介面
        if (!type.isAnonymousClass() && !type.isInterface() && !type.isMemberClass()) {
            // 註冊指定類的別名
            registerAlias(type);
        }
    }
}

接著進入TypeAliasRegistry.registerAlias(Class<?>)。因為按 package 註冊別名的方式沒有在 xml 中指定別名,所以,這裡會試圖從類的Alias註解裡獲取,如果沒有,預設使用該類的 simpleName。

public void registerAlias(Class<?> type) {
    // 獲取指定類的simpleName
    String alias = type.getSimpleName();
    // 獲取指定類的Alias註解
    Alias aliasAnnotation = type.getAnnotation(Alias.class);
    if (aliasAnnotation != null) {
        // 如果不為空,設定別名為註解裡的value
        alias = aliasAnnotation.value();
    }
    // 註冊指定類的別名
    registerAlias(alias, type);
}

最後進入TypeAliasRegistry.registerAlias(String, Class<?>)方法,通過以下程式碼可知,別名都會被轉化為小寫,而且,如果同一個別名註冊多個不同的類,會報錯。最終會以 alias=Class 的鍵值對存入TypeAliasRegistry維護的 map中,供其他配置項使用。

// 存放著 alias=Class 的鍵值對
private final Map<String, Class<?>> typeAliases = new HashMap<>();
public void registerAlias(String alias, Class<?> value) {
    if (alias == null) {
        throw new TypeException("The parameter alias cannot be null");
    }
    // 取別名的小寫
    String key = alias.toLowerCase(Locale.ENGLISH);
    // 如果相同的別名或類已經註冊過,會拋錯
    if (typeAliases.containsKey(key) && typeAliases.get(key) != null && !typeAliases.get(key).equals(value)) {
        throw new TypeException("The alias '" + alias + "' is already mapped to the value '" + typeAliases.get(key).getName() + "'.");
    }
	// 存入鍵值對
    typeAliases.put(key, value);
}

plugins

外掛/攔截器的初始化比較簡單,就簡單過一下吧。通過程式碼可知,我們可以在 plugin 節點下增加 property節點。

private void pluginElement(XNode parent) throws Exception {
    if (parent != null) {
        for (XNode child : parent.getChildren()) {
            // 獲取interceptor名
            String interceptor = child.getStringAttribute("interceptor");
            // 獲取interceptor的引數
            Properties properties = child.getChildrenAsProperties();
            // 例項化。注意,這裡解析Class時會先從別名註冊器查,沒有才會用Class.forName的方式例項化
            Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
            // 設定引數
            interceptorInstance.setProperties(properties);
            // 新增到configuration的interceptorChain
            configuration.addInterceptor(interceptorInstance);
        }
    }
}

environments

這裡的Environment物件包含了兩個部分:事務工廠和資料來源,並且使用 id 作為唯一標識。在下面的程式碼中,事務工廠和資料來源的例項化過程有點類似於外掛的過程,這裡就不展開了。

private void environmentsElement(XNode context) throws Exception {
    if (context != null) {
        // 如果沒有指定環境,會使用default
        if (environment == null) {
            environment = context.getStringAttribute("default");
        }
        for (XNode child : context.getChildren()) {
            String id = child.getStringAttribute("id");
            // 判斷是否指定環境
            if (isSpecifiedEnvironment(id)) {
                // 根據配置的transactionManager建立TransactionFactory物件
                TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
                // 根據配置的dataSource建立DataSourceFactory物件
                DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
                // 獲取資料來源
                DataSource dataSource = dsFactory.getDataSource();
                // 根據id(環境名)、資料來源和事務工廠構建並設定Environment物件
                Environment.Builder environmentBuilder = new Environment.Builder(id)
                    .transactionFactory(txFactory)
                    .dataSource(dataSource);
                configuration.setEnvironment(environmentBuilder.build());
            }
        }
    }
}

typeHandlers*

配置TypeHandler的規則

TypeHandler用於處理引數對映和結果集對映,一個TypeHandler一般需要包含 javaType 和 jdbcType 兩個屬性來標識,如果某個 javaType 和資料庫的 jdbcType 關係為的 一對一或一對多,則可以不用設定 jdbcType。例如BooleanTypeHandlerByteTypeHandler

在分析原始碼前,我們先來看看宣告 javaType 和 jdbcType 的幾種方式:

  1. xml 中宣告,如下
<typeHandlers>
  <typeHandler handler="org.mybatis.example.ExampleTypeHandler" javaType="String" jdbcType="VARCHAR"/>
</typeHandlers>
  1. 在註解中宣告,如下:
@MappedTypes(value = String.class)
@MappedJdbcTypes(value = JdbcType.VARCHAR)
public class ExampleTypeHandler implements TypeHandler<String> {
}
  1. 在泛型中宣告,如下。這種只能用來配置 javaType,而且,必須繼承BaseTypeHandlerTypeReference才行。
public class BigDecimalTypeHandler extends BaseTypeHandler<BigDecimal> {
}

相容的配置方式越多,程式碼邏輯也會更復雜,如果 xml 中沒有顯式地配置 javaType 或 jdbcType,mybatis 會嘗試去推斷出來,只要明白這個邏輯,接下來的程式碼就簡單很多了。

原始碼分析

現在開始分析原始碼吧。我們可以使用 package 和 typeHandler 的兩種配置方式,且它們可以共存。

private void typeHandlerElement(XNode parent) {
    if (parent != null) {
        for (XNode child : parent.getChildren()) {
            // 使用包名註冊的情況
            if ("package".equals(child.getName())) {
                String typeHandlerPackage = child.getStringAttribute("name");
                typeHandlerRegistry.register(typeHandlerPackage);
            } else {
                //使用具體類名註冊的情況
                String javaTypeName = child.getStringAttribute("javaType");
                String jdbcTypeName = child.getStringAttribute("jdbcType");
                String handlerTypeName = child.getStringAttribute("handler");
                Class<?> javaTypeClass = resolveClass(javaTypeName);
                JdbcType jdbcType = resolveJdbcType(jdbcTypeName);
                Class<?> typeHandlerClass = resolveClass(handlerTypeName);
                if (javaTypeClass != null) {
                    if (jdbcType == null) {
                        // javaType不為空,jdbcType為空的情況
                        typeHandlerRegistry.register(javaTypeClass, typeHandlerClass);
                    } else {
                        // javaType不為空,jdbcType不為空的情況
                        typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);
                    }
                } else {
                    // javaType為空,jdbcType為空的情況
                    typeHandlerRegistry.register(typeHandlerClass);
                }
            }
        }
    }
}

按 package 註冊型別處理器的方式有點像前面提到的按 package 註冊別名,都會先載入指定包裡的類,這裡就不展開了,直接看按類名註冊的情況(不指定 javaType 和 jdbcType),進入到TypeHandlerRegistry.register(Class<?>)方法。這種情況下,mybatis 會先去推斷出該型別處理器對應的 javaType,方法如下:

  1. 通過 MappedTypes 註解的 value 來判斷;
  2. 通過泛型判斷,這種型別處理器需要繼承BaseTypeHandler,而不僅僅只是實現TypeHandler。(3.1.0之後才支援)
public void register(Class<?> typeHandlerClass) {
    boolean mappedTypeFound = false;
    // 獲取指定型別處理器的MappedTypes註解,裡面的value就是該型別處理器處理的javaType
    MappedTypes mappedTypes = typeHandlerClass.getAnnotation(MappedTypes.class);
    if (mappedTypes != null) {
        // 獲取MappedTypes註解的value,並遍歷
        for (Class<?> javaTypeClass : mappedTypes.value()) {
            // 根據javaType註冊型別處理器
            register(javaTypeClass, typeHandlerClass);
            mappedTypeFound = true;
        }
    }
    // 如果沒有MappedTypes註解,mybatis 3.1.0之後會通過泛型推斷出javaType,但這種型別處理器需要繼承BaseTypeHandler,而不僅僅只是實現TypeHandler
    if (!mappedTypeFound) {
        register(getInstance(null, typeHandlerClass));
    }
}

接下來就是推斷 jdbcType 了,這裡會通過 MappedJdbcTypes 註解來確定(可配置多個 jdbcType),如果設定了includeNullJdbcType=true,則會將 jdbcTyp 為 null 情況也註冊上去。如果沒有MappedJdbcTypes 註解,會直接將 jdbcTyp 為 null 情況也註冊上去。

public void register(Class<?> javaTypeClass, Class<?> typeHandlerClass) {
    // 例項化型別處理器,並根據javaType註冊
    register(javaTypeClass, getInstance(javaTypeClass, typeHandlerClass));
}
public <T> void register(Class<T> javaType, TypeHandler<? extends T> typeHandler) {
    // 強轉javaType為Type型別
    register((Type) javaType, typeHandler);
}
private <T> void register(Type javaType, TypeHandler<? extends T> typeHandler) {
    // 獲取型別處理器的MappedJdbcTypes註解,裡面的value就是該型別處理器處理的jdbcType
    MappedJdbcTypes mappedJdbcTypes = typeHandler.getClass().getAnnotation(MappedJdbcTypes.class);
    if (mappedJdbcTypes != null) {
        // 獲取MappedJdbcTypes註解的value,並遍歷
        for (JdbcType handledJdbcType : mappedJdbcTypes.value()) {
            // 根據javaType和jdbcType註冊型別處理器
            register(javaType, handledJdbcType, typeHandler);
        }
        // 讀取MappedJdbcTypes註解的includeNullJdbcType,如果為true,則根據javaType註冊型別處理器
        // 當includeNullJdbcType為true時,即使不指定jdbcType,該型別處理器也能被使用。從 Mybatis 3.4.0 開始,如果某個 Java 型別只有一個註冊的型別處理器,即使沒有設定 includeNullJdbcType=true,那麼這個型別處理器也會是 ResultMap 使用 Java 型別時的預設處理器。
        if (mappedJdbcTypes.includeNullJdbcType()) {
            register(javaType, null, typeHandler);
        }
    } else {
        // 根據javaType註冊型別處理
        register(javaType, null, typeHandler);
    }
}

最後就是具體的註冊過程了。mybatis 進行引數或結果集對映時一般用到的是 typeHandlerMap,其他的成員屬性一般用於判斷是否有某種型別處理器。

// javaType=(jdbcType=typeHandler)
private final Map<Type, Map<JdbcType, TypeHandler<?>>> typeHandlerMap = new ConcurrentHashMap<>();
// class=typeHandler,這個沒什麼用
private final Map<Class<?>, TypeHandler<?>> allTypeHandlersMap = new HashMap<>();

private void register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler) {
    // 只有javaType非空時才會放入typeHandlerMap
    if (javaType != null) {
        // 從typeHandlerMap裡獲取當前javaType的jdbcType=TypeHandler
        Map<JdbcType, TypeHandler<?>> map = typeHandlerMap.get(javaType); 
        // 如果這張表為空,則重置
        if (map == null || map == NULL_TYPE_HANDLER_MAP) {
            map = new HashMap<>();
        }
        // 放入當前需要註冊的jdbcType=TypeHandler,注意,相同的會被覆蓋掉
        map.put(jdbcType, handler);
        // 放入javaType=map
        typeHandlerMap.put(javaType, map);
    }
    // allTypeHandlersMap放入了所有的handler,包括javaType為空的。
    allTypeHandlersMap.put(handler.getClass(), handler);
}

mappers*

mapper 的節點物件

接下來就是初始化中最難的部分了。因為 mybatis 的 mapper 支援了非常多個語法,甚至還允許使用註解配置,所以,在對 mapper 的解析方面需要非常複雜的邏輯。我們先來看看 mapper 中的配置項,如下。

ResultMap的組成

接下來我只會寫 resultMap 節點的 xml 配置,其他的就不寫了。為了更好地理清程式碼邏輯,我們先看看 resultMap 的幾種配置方式。

<resultMap id="detailedBlogResultMap" type="Blog">
    <constructor>
        <idArg column="blog_id" javaType="int" />
    </constructor>
    <result property="title" column="blog_title" />
    <association property="author" javaType="Author">
        <id property="id" column="author_id" />
        <result property="username" column="author_username" />
        <result property="password" column="author_password" />
        <result property="email" column="author_email" />
    </association>
    <collection property="posts" ofType="Post">
        <id property="id" column="post_id" />
        <result property="subject" column="post_subject" />
        <association property="author" javaType="Author" />
        <collection property="comments" ofType="Comment">
            <id property="id" column="comment_id" />
        </collection>
        <collection property="tags" ofType="Tag">
            <id property="id" column="tag_id" />
        </collection>
    </collection>
    <discriminator javaType="int" column="draft">
        <case value="1" resultMap="resultMap01"/>
        <case value="2" resultMap="resultMap02"/>
        <case value="3" resultMap="resultMap03"/>
        <case value="4" resultMap="resultMap04"/>
    </discriminator>
</resultMap>

針對上面的配置,需要重點理解:

  1. 整個 resultMap 將作為ResultMap物件存在,並使用 id 作為唯一標識。除了 id="detailedBlogResultMap" 的 ResultMap物件,association 、collection 和 case 節點也會生成新的ResultMap物件(如果不是配置 resultMap 和 select 屬性的話)。
  2. idArg、result、association 和 collection 節點都會被轉換為ResultMapping物件被ResultMap物件持有,區別在於 association 和 collection 的ResultMapping物件會持有 nestedResultMapId 來指向另外一個ResultMap物件,持有 nestedQueryId 來指向另外一個MappedStatement物件。
  3. discriminator 節點,將轉換為Discriminator物件被ResultMap物件持有。

原始碼分析

那麼,開始看原始碼吧。mapper 的配置支援下面兩種配置,兩者可以共存:

  1. mapper 節點配置。支援 resource、url 和 class 屬性,但這三個屬性只能配置一個,不然會報錯。
  2. package 節點配置。
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 {
                // 使用mapper配置的情況
                String resource = child.getStringAttribute("resource");
                String url = child.getStringAttribute("url");
                String mapperClass = child.getStringAttribute("class");
                if (resource != null && url == null && mapperClass == null) {
                    // resource屬性不為空
                    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屬性不為空
                    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屬性不為空
                    Class<?> mapperInterface = Resources.classForName(mapperClass);
                    configuration.addMapper(mapperInterface);
                // resource、url和class只能存在一個
                } else {
                    throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
                }
            }
        }
    }
}

使用 package 配置 mapper 的情況,會有載入包內類的過程,和前面的 typeAliases 差不多,所以這裡選擇使用 mapper 配置(屬性為class)的情況,進入到Configuration.addMapper(Class<T>)。在註冊 mapper 時,其實有兩個內容:

  1. 註冊 mapper 介面,初始化 mapperRegistry 裡的 type=mapperProxyFactory 的map。MapperProxyFactory用於生成Mapper的代理類,後面會講到。
  2. 解析 mapper 的 xml 檔案和註解,初始化 mappedStatements、caches、resultMaps、parameterMaps 等屬性。
public <T> void addMapper(Class<T> type) {
    mapperRegistry.addMapper(type);
}
public <T> void addMapper(Class<T> type) {
	// 只有是接口才行
    if (type.isInterface()) {
        // 該mapper是不是已經註冊
        if (hasMapper(type)) {
            throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
        }
        boolean loadCompleted = false;
        try {
            // 註冊該mapper介面
            knownMappers.put(type, new MapperProxyFactory<>(type));
            // 接下來解析mapper的xml和註解,不要被MapperAnnotationBuilder這個類名誤導,接下來不止會解析註解,也會解析xml
            MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
            parser.parse();
            loadCompleted = true;
        } finally {
            if (!loadCompleted) {
                knownMappers.remove(type);
            }
        }
    }
}

進入到MapperAnnotationBuilder.parse()方法,這裡先解析 xml 檔案,再解析註解。接下來我們只看 xml 的,註解的就不看了。

// 存放已載入的資源
protected final Set<String> loadedResources = new HashSet<>();
public void parse() {
    String resource = type.toString();
    // 該資源未被載入才進入
    if (!configuration.isResourceLoaded(resource)) {
        // 載入xml
        loadXmlResource();
        // 標記已載入
        configuration.addLoadedResource(resource);
        assistant.setCurrentNamespace(type.getName());
        // 接下來是解析註解
        parseCache();
        parseCacheRef();
        Method[] methods = type.getMethods();
        for (Method method : methods) {
            try {
                // issue #237
                if (!method.isBridge()) {
                    parseStatement(method);
                }
            } catch (IncompleteElementException e) {
                // 未解析完成,會放入對應的集合中,等待最後再解析
                configuration.addIncompleteMethod(new MethodResolver(this, method));
            }
        }
    }
    // 因為存在巢狀引用的問題,有些內容還沒解析完,這裡會做最後的解析
    parsePendingMethods();
}

進入到MapperAnnotationBuilder.loadXmlResource()方法。這裡的XMLMapperBuilder用於解析 mapper 檔案的配置,前面說到的XMLConfigBuilder則是解析 configurantion 檔案的配置,它們都是BaseBuilder的子類。

private void loadXmlResource() {
    // 該名稱空間未被載入,才會進入
    if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
        // 根據mapper獲取xml
        String xmlResource = type.getName().replace('.', '/') + ".xml";
        InputStream inputStream = type.getResourceAsStream("/" + xmlResource);
        if (inputStream == null) {
            // Search XML mapper that is not in the module but in the classpath.
            try {
                inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
            } catch (IOException e2) {
                // ignore, resource is not required
            }
        }
        if (inputStream != null) {
            // 和XMLConfigBuilder一樣,這裡會解析xml並構建document
            XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
            // 進入解析
            xmlParser.parse();
        }
    }
}

進入到XMLMapperBuilder.parse()。我們會發現,如果使用 resource 或 url 的方式來配置 mapper,那麼 Mapper 介面的註冊會在這個方法裡。

public void parse() {
    // 該資源未載入才會進入
    if (!configuration.isResourceLoaded(resource)) {
        // 構建mapper節點的XNode物件,並解析
        configurationElement(parser.evalNode("/mapper"));
        // 標記已解析
        configuration.addLoadedResource(resource);
        // 註冊Mapper介面,其實這個註冊過了的
        bindMapperForNamespace();
    }
	// 因為存在巢狀引用的問題,有的節點還沒初始化完成,這裡繼續初始化
    parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();
}
private void configurationElement(XNode context) {
    try {
        // mapper檔案的namespace不能為空
        String namespace = context.getStringAttribute("namespace");
        if (namespace == null || namespace.equals("")) {
            throw new BuilderException("Mapper's namespace cannot be empty");
        }
        builderAssistant.setCurrentNamespace(namespace);
        // 接下來講初始化各個節點
        cacheRefElement(context.evalNode("cache-ref"));
        cacheElement(context.evalNode("cache"));
        parameterMapElement(context.evalNodes("/mapper/parameterMap"));
        resultMapElements(context.evalNodes("/mapper/resultMap"));
        sqlElement(context.evalNodes("/mapper/sql"));
        buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
        throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
    }
}

前面已經說過,我們只看 resultMap 的構建,進入到XMLMapperBuilder.resultMapElements(List<XNode>)

private void resultMapElements(List<XNode> list) {
    // 我們可以配置多個resultMap,這裡一個個遍歷
    for (XNode resultMapNode : list) {
        try {
            // 解析resultMap節點
            resultMapElement(resultMapNode);
        } catch (IncompleteElementException e) {
            // ignore, it will be retried
        }
    }
}
private ResultMap resultMapElement(XNode resultMapNode) {
    return resultMapElement(resultMapNode, Collections.emptyList(), null);
}
// 注意,這個類傳入的resultMapNode不僅是resultMap節點,也可以是association、collection或case節點
// 如果是association、collection或case節點,enclosingType為當前resultMap節點的type,additionalResultMappings為所屬resultMap的ResultMappings
private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings, Class<?> enclosingType) {
    ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
    // 獲取當前的類名
    String type = resultMapNode.getStringAttribute("type",
                                                   resultMapNode.getStringAttribute("ofType",
                                                                                    resultMapNode.getStringAttribute("resultType",
                                                                                                                     resultMapNode.getStringAttribute("javaType"))));
    // 獲取該類的Class物件。如果為空,針對association和case的情況會通過enclosingType來推斷
    Class<?> typeClass = resolveClass(type);
    if (typeClass == null) {
        typeClass = inheritEnclosingType(resultMapNode, enclosingType);
    }
    Discriminator discriminator = null;
    List<ResultMapping> resultMappings = new ArrayList<>(additionalResultMappings);
    List<XNode> resultChildren = resultMapNode.getChildren();
    for (XNode resultChild : resultChildren) {
        // 如果為constructor節點
        if ("constructor".equals(resultChild.getName())) {
            // 這裡會將每個idArg或arg轉換為ResultMapping物件,並放入resultMappings
            processConstructorElement(resultChild, typeClass, resultMappings);
        // 如果為discriminator節點
        } else if ("discriminator".equals(resultChild.getName())) {
            // discriminator將轉換為Discriminator物件
            discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
        // 這種就是的result、collection或association節點了
        } else {
			List<ResultFlag> flags = new ArrayList<>();
            // 標記id
            if ("id".equals(resultChild.getName())) {
                flags.add(ResultFlag.ID);
            }
            // 將result、collection或association節點轉換為ResultMapping物件,並放入resultMappings,如果是collection或association節點,會指向生成的新的ResultMap物件或已有的ResultMap物件
            resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
        }
    }
    // 獲取resultMap的id、extends和autoMapping屬性
    String id = resultMapNode.getStringAttribute("id",
                                                 resultMapNode.getValueBasedIdentifier());
    String extend = resultMapNode.getStringAttribute("extends");
    Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
    // 建立ResultMapResolver物件
    ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
    try {
        // 解析resultMap,這裡所謂的解析,其實就是將extends的東西放入resultMappings
        return resultMapResolver.resolve();
    } catch (IncompleteElementException  e) {
        // 如果沒有解析完成,放入集合incompleteResultMaps,等待後面再解析
        configuration.addIncompleteResultMap(resultMapResolver);
        throw e;
    }
}

以上,mybatis 初始化的原始碼基本已分析完,不足的地方歡迎指正。

相關原始碼請移步:mybatis-demo

本文為原創文章,轉載請附上原文出處連結:https://www.cnblogs.com/ZhangZiSheng001/p/12704076.html