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. 創建SqlSessionFactorySqlSessionFactoryBuilder利用構建好的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 文件中各個配置項,接下來講的就是如何初始化這些屬性。

mybatis_source_init02

進入到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對象,通過它可以進行值的設置和獲取。這個類將在后續源碼分析中多次出現。

mybatis_source_init03

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

mybatis_source_init04

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_source_init05

源碼分析

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM