天天看點

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

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

前面已經說完 mybatis 的使用,現在開始分析源碼,和使用例子一樣,我用的 mybatis 是 3.5.4 版本的。考慮連貫性,我會按下面的順序來展開分析,計劃兩篇部落格寫完,本文隻涉及第一點内容:

1. 加載配置、初始化SqlSessionFactory;

2. 擷取SqlSession和Mapper;

3. 執行Mapper方法。

簡介

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

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

  1. 加載配置、初始化

    SqlSessionFactory

  2. 擷取

    SqlSession

    Mapper

  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
           

注意,考慮可讀性,文中部分源碼經過删減。

初始化的過程

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

Mybatis源碼詳解系列(二)--Mybatis如何加載配置及初始化
  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 等外部資源時會觸發

EntityResolver

resolveEntity

方法。在

XMLMapperEntityResolver.resolveEntity

中,當解析到 mybatis-3-config.dtd、mybatis-3-mapper.dtd 等資源時,會直接從 classpath 下的 org/apache/ibatis/builder/xml/ 路徑擷取資源,而不需要通過 url 擷取。

注意,上面對建構的

Document

對象,隻是 configuration 檔案的,并不包含 mapper 檔案。

先認識下Configuration這個類

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

Configuration

對象了。

在此之前,我們先認識下

Configurantion

這個類,如下圖。可以看到,這些成員屬性對應了 xml 檔案中各個配置項,接下來講的就是如何初始化這些屬性。

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

進入到

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 沒有這麼做,它提供了一個非常好用的工具類--

MetaClass

MetaClass

可以用來初始化某個類的參數集,例如

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。例如

BooleanTypeHandler

ByteTypeHandler

在分析源碼前,我們先來看看聲明 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,而且,必須繼承

    BaseTypeHandler

    TypeReference

    才行。
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 中的配置項,如下。

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

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

    對象持有。
Mybatis源碼詳解系列(二)--Mybatis如何加載配置及初始化

那麼,開始看源碼吧。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

分層,抽象,高内聚,低耦合