MyBatis給我們提供豐富的配置來滿足我們的需求,本文會對MyBatis的配置檔案解析過程進行分析, 其中包含但不限于 properties、 settings、typeAliase、typeHandlers 等。
1、配置檔案解析入口
在單獨使用 MyBatis 時,第一步要做的事情就是根據配置檔案建構SqlSessionFactory對象。相關代碼如下:
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
首先,我們使用 MyBatis 提供的工具類 Resources 加載配置檔案,得到一個輸入流。然後再通過 SqlSessionFactoryBuilder 對象的build方法建構 SqlSessionFactory 對象。是以這裡的 build 方法是我們分析配置檔案解析過程的入口方法。那下面我們來看一下這個方法的代碼:
// -☆- SqlSessionFactoryBuilder
public SqlSessionFactory build(InputStream inputStream) {
// 調用重載方法
return build(inputStream, null, null);
}
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
// 建立配置檔案解析器
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
// 調用 parse 方法解析配置檔案,生成 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) {
// 建立 DefaultSqlSessionFactory
return new DefaultSqlSessionFactory(config);
}
從上面的代碼中,我們大緻可以猜出 MyBatis 配置檔案是通過XMLConfigBuilder進行解析的。不過目前這裡還沒有非常明确的解析邏輯,是以我們繼續往下看。這次來看一下 XMLConfigBuilder 的parse方法,如下:
// -☆- XMLConfigBuilder
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
// 解析配置
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
到這裡大家可以看到一些端倪了,注意一個 xpath 表達式 - /configuration。這個表達式代表的是 MyBatis 的标簽,這裡選中這個标簽,并傳遞給parseConfiguration方法。我們繼續跟下去。
private void parseConfiguration(XNode root) {
try {
// 解析 properties 配置
propertiesElement(root.evalNode("properties"));
// 解析 settings 配置,并将其轉換為 Properties 對象
Properties settings = settingsAsProperties(root.evalNode("settings"));
// 加載 vfs
loadCustomVfs(settings);
// 解析 typeAliases 配置
typeAliasesElement(root.evalNode("typeAliases"));
// 解析 plugins 配置
pluginElement(root.evalNode("plugins"));
// 解析 objectFactory 配置
objectFactoryElement(root.evalNode("objectFactory"));
// 解析 objectWrapperFactory 配置
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
// 解析 reflectorFactory 配置
reflectorFactoryElement(root.evalNode("reflectorFactory"));
// settings 中的資訊設定到 Configuration 對象中
settingsElement(settings);
// 解析 environments 配置
environmentsElement(root.evalNode("environments"));
// 解析 databaseIdProvider,擷取并設定 databaseId 到 Configuration 對象
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
// 解析 typeHandlers 配置
typeHandlerElement(root.evalNode("typeHandlers"));
// 解析 mappers 配置
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
到此,一個 MyBatis 的解析過程就出來了,每個配置的解析邏輯都封裝在了相應的方法中。
2、解析 properties 配置
解析properties節點是由propertiesElement這個方法完成的,該方法的邏輯比較簡單。在分析方法源碼前,先來看一下 properties 節點的配置内容。如下:
<properties resource="jdbc.properties">
<property name="jdbc.username" value="coolblog"/>
<property name="hello" value="world"/>
</properties>
在上面的配置中,我為 properties 節點配置了一個 resource 屬性,以及兩個子節點。下面我們參照上面的配置,來分析一下 propertiesElement 的邏輯。相關分析如下。
// -☆- XMLConfigBuilder
private void propertiesElement(XNode context) throws Exception {
if (context != null) {
// 解析 propertis 的子節點,并将這些節點内容轉換為屬性對象 Properties
Properties defaults = context.getChildrenAsProperties();
// 擷取 propertis 節點中的 resource 和 url 屬性值
String resource = context.getStringAttribute("resource");
String url = context.getStringAttribute("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.");
}
if (resource != null) {
// 從檔案系統中加載并解析屬性檔案
defaults.putAll(Resources.getResourceAsProperties(resource));
} else if (url != null) {
// 通過 url 加載并解析屬性檔案
defaults.putAll(Resources.getUrlAsProperties(url));
}
Properties vars = configuration.getVariables();
if (vars != null) {
defaults.putAll(vars);
}
parser.setVariables(defaults);
// 将屬性值設定到 configuration 中
configuration.setVariables(defaults);
}
}
public Properties getChildrenAsProperties() {
Properties properties = new Properties();
// 擷取并周遊子節點
for (XNode child : getChildren()) {
// 擷取 property 節點的 name 和 value 屬性
String name = child.getStringAttribute("name");
String value = child.getStringAttribute("value");
if (name != null && value != null) {
// 設定屬性到屬性對象中
properties.setProperty(name, value);
}
}
return properties;
}
// -☆- XNode
public List<XNode> getChildren() {
List<XNode> children = new ArrayList<XNode>();
// 擷取子節點清單
NodeList nodeList = node.getChildNodes();
if (nodeList != null) {
for (int i = 0, n = nodeList.getLength(); i < n; i++) {
Node node = nodeList.item(i);
if (node.getNodeType() == Node.ELEMENT_NODE) {
// 将節點對象封裝到 XNode 中,并将 XNode 對象放入 children 清單中
children.add(new XNode(xpathParser, node, variables));
}
}
}
return children;
}
需要注意的是,propertiesElement 方法是先解析 properties 節點的子節點内容,後再從檔案系統或者網絡讀取屬性配置,并将所有的屬性及屬性值都放入到 defaults 屬性對象中。這就會存在同名屬性覆寫的問題,也就是從檔案系統,或者網絡上讀取到的屬性及屬性值會覆寫掉 properties 子節點中同名的屬性和及值。比如上面配置中的jdbc.properties内容如下:
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/myblog?...
jdbc.username=root
jdbc.password=1234
3、解析 settings 配置
settings 節點的解析過程
settings 相關配置是 MyBatis 中非常重要的配置,這些配置用于調整 MyBatis 運作時的行為。比如:
<settings>
<setting name="cacheEnabled" value="true"/>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="autoMappingBehavior" value="PARTIAL"/>
</settings>
接下來,對照上面的配置,來分析源碼。如下:
// -☆- XMLConfigBuilder
private Properties settingsAsProperties(XNode context) {
if (context == null) {
return new Properties();
}
// 擷取 settings 子節點中的内容,getChildrenAsProperties 方法前面已分析過,這裡不再贅述
Properties props = context.getChildrenAsProperties();
// 建立 Configuration 類的“元資訊”對象
MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory);
for (Object key : props.keySet()) {
// 檢測 Configuration 中是否存在相關屬性,不存在則抛出異常
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;
}
在上面的代碼中出現了一個陌生的類MetaClass,他是用來解析目标類的一些元資訊,比如類的成員變量,getter/setter 方法等。關于這個類的邏輯,待會我會詳細解析。接下來,簡單總結一下上面代碼的邏輯。如下:
- 解析 settings 子節點的内容,并将解析結果轉成 Properties 對象
- 為 Configuration 建立元資訊對象
- 通過 MetaClass 檢測 Configuration 中是否存在某個屬性的 setter 方法,不存在則抛異常
-
若通過 MetaClass 的檢測,則傳回 Properties 對象,方法邏輯結束
下面,我們來重點關注一下第2步和第3步的流程。這兩步流程對應的代碼較為複雜,需要一點耐心閱讀。好了,下面開始分析。
元資訊對象建立過程
元資訊類MetaClass的構造方法為私有類型,是以不能直接建立,必須使用其提供的forClass方法進行建立。它的建立邏輯如下:
public class MetaClass {
private final ReflectorFactory reflectorFactory;
private final Reflector reflector;
private MetaClass(Class<?> type, ReflectorFactory reflectorFactory) {
this.reflectorFactory = reflectorFactory;
// 根據類型建立 Reflector
this.reflector = reflectorFactory.findForClass(type);
}
public static MetaClass forClass(Class<?> type, ReflectorFactory reflectorFactory) {
// 調用構造方法
return new MetaClass(type, reflectorFactory);
}
// 省略其他方法
}
上面代碼出現了兩個新的類ReflectorFactory和Reflector,MetaClass 通過引入這些新類幫助它完成功能。下面我們看一下hasSetter方法的源碼就知道是怎麼回事了。
// -☆- MetaClass
public boolean hasSetter(String name) {
// 屬性分詞器,用于解析屬性名
PropertyTokenizer prop = new PropertyTokenizer(name);
// hasNext 傳回 true,則表明 name 是一個複合屬性,後面會進行分析
if (prop.hasNext()) {
// 調用 reflector 的 hasSetter 方法
if (reflector.hasSetter(prop.getName())) {
// 為屬性建立建立 MetaClass
MetaClass metaProp = metaClassForProperty(prop.getName());
// 再次調用 hasSetter
return metaProp.hasSetter(prop.getChildren());
} else {
return false;
}
} else {
// 調用 reflector 的 hasSetter 方法
return reflector.hasSetter(prop.getName());
}
}
從上面的代碼中,我們可以看出 MetaClass 中的 hasSetter 方法最終調用了 Reflector 的 hasSetter 方法。下面來簡單介紹一下上面代碼中出現的幾個類:
- ReflectorFactory -> 顧名思義,Reflector 的工廠類,兼有緩存 Reflector 對象的功能
- Reflector -> 反射器,用于解析和存儲目标類中的元資訊
- PropertyTokenizer -> 屬性名分詞器,用于處理較為複雜的屬性名
4、設定 settings 配置到 Configuration 中
上一節講了 settings 配置的解析過程,這些配置解析出來要有一個存放的地方,以使其他代碼可以找到這些配置。這個存放地方就是 Configuration 對象,本節就來看一下這将 settings 配置設定到 Configuration 對象中的過程。如下:
private void settingsElement(Properties props) throws Exception {
// 設定 autoMappingBehavior 屬性,預設值為 PARTIAL
configuration.setAutoMappingBehavior(AutoMappingBehavior.valueOf(props.getProperty("autoMappingBehavior", "PARTIAL")));
configuration.setAutoMappingUnknownColumnBehavior(AutoMappingUnknownColumnBehavior.valueOf(props.getProperty("autoMappingUnknownColumnBehavior", "NONE")));
// 設定 cacheEnabled 屬性,預設值為 true
configuration.setCacheEnabled(booleanValueOf(props.getProperty("cacheEnabled"), true));
// 省略部分代碼
// 解析預設的枚舉處理器
Class<? extends TypeHandler> typeHandler = (Class<? extends TypeHandler>)resolveClass(props.getProperty("defaultEnumTypeHandler"));
// 設定預設枚舉處理器
configuration.setDefaultEnumTypeHandler(typeHandler);
configuration.setCallSettersOnNulls(booleanValueOf(props.getProperty("callSettersOnNulls"), false));
configuration.setUseActualParamName(booleanValueOf(props.getProperty("useActualParamName"), true));
// 省略部分代碼
}
上面代碼處理調用 Configuration 的 setter 方法,就沒太多邏輯了。這裡來看一下上面出現的一個調用resolveClass,它的源碼如下:
// -☆- BaseBuilder
protected Class<?> resolveClass(String alias) {
if (alias == null) {
return null;
}
try {
// 通過别名解析
return resolveAlias(alias);
} catch (Exception e) {
throw new BuilderException("Error resolving class. Cause: " + e, e);
}
}
protected final TypeAliasRegistry typeAliasRegistry;
protected Class<?> resolveAlias(String alias) {
// 通過别名注冊器解析别名對于的類型 Class
return typeAliasRegistry.resolveAlias(alias);
}
這裡出現了一個新的類TypeAliasRegistry,TypeAliasRegistry 的用途就是将别名和類型進行映射,這樣就可以用别名表示某個類了,友善使用。
5、解析 environments 配置
在 MyBatis 中,事務管理器和資料源是配置在 environments 中的。它們的配置大緻如下:
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>
</environment>
</environments>
接下來我們對照上面的配置進行分析,如下:
private void environmentsElement(XNode context) throws Exception {
if (context != null) {
if (environment == null) {
// 擷取 default 屬性
environment = context.getStringAttribute("default");
}
for (XNode child : context.getChildren()) {
// 擷取 id 屬性
String id = child.getStringAttribute("id");
/*
* 檢測目前 environment 節點的 id 與其父節點 environments 的屬性 default
* 内容是否一緻,一緻則傳回 true,否則傳回 false
*/
if (isSpecifiedEnvironment(id)) {
TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
DataSource dataSource = dsFactory.getDataSource();
Environment.Builder environmentBuilder = new Environment.Builder(id)
.transactionFactory(txFactory)
.dataSource(dataSource);
// 建構 Environment 對象,并設定到 configuration 中
configuration.setEnvironment(environmentBuilder.build());
}
}
}
}
6、解析 typeHandlers 配置
在向資料庫存儲或讀取資料時,我們需要将資料庫字段類型和 Java 類型進行一個轉換。比如資料庫中有CHAR和VARCHAR等類型,但 Java 中沒有這些類型,不過 Java 有String類型。是以我們在從資料庫中讀取 CHAR 和 VARCHAR 類型的資料時,就可以把它們轉成 String 。
在 MyBatis 中,資料庫類型和 Java 類型之間的轉換任務是委托給類型處理器TypeHandler去處理的。MyBatis 提供了一些常見類型的類型處理器,除此之外,我們還可以自定義類型處理器以非常見類型轉換的需求。這裡我就不示範自定義類型處理器的編寫方法了,沒用過或者不熟悉的同學可以 MyBatis 官方文檔,或者我在上一篇文章中寫的示例。
private void typeHandlerElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
// 從指定的包中注冊 TypeHandler
if ("package".equals(child.getName())) {
String typeHandlerPackage = child.getStringAttribute("name");
// 注冊方法 ①
typeHandlerRegistry.register(typeHandlerPackage);
// 從 typeHandler 節點中解析别名到類型的映射
} else {
// 擷取 javaType,jdbcType 和 handler 等屬性值
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);
// 根據 javaTypeClass 和 jdbcType 值的情況進行不同的注冊政策
if (javaTypeClass != null) {
if (jdbcType == null) {
// 注冊方法 ②
typeHandlerRegistry.register(javaTypeClass, typeHandlerClass);
} else {
// 注冊方法 ③
typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);
}
} else {
// 注冊方法 ④
typeHandlerRegistry.register(typeHandlerClass);
}
}
}
}
}
上面調用的注冊方法隻是重載方法的一部分。由于重載太多且重載方法之間互相調用,導緻這一塊的代碼有點淩亂。我一開始在整理這部分代碼時,也很抓狂。後來沒轍了,把重載方法的調用圖畫了出來,才理清了代碼。一圖勝千言,看圖吧。
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsICM38FdsYkRGZkRG9lcvx2bjxiNx8VZ6l2cs0TUzMWMxcUZ1AnMMBjVtJWd0ckW65UbM5WOHJWa5kHT20ESjBjUIF2X0hXZ0xCMx81dvRWYoNHLrdEZwZ1Rh5WNXp1bwNjW1ZUba9VZwlHdssmch1mclRXY39CXldWYtlWPzNXZj9mcw1ycz9WL49zZuBnLwcjN3MTMxITMxEDOwkTMwIzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
在上面的調用圖中,每個藍色背景框下都有一個标簽。每個标簽上面都已一個編号,這些編号與上面代碼中的标簽是一緻的。這裡我把藍色背景框内的方法稱為開始方法,紅色背景框内的方法稱為終點方法,白色背景框内的方法稱為中間方法。下面我會分析從每個開始方法向下分析,為了避免備援分析,我會按照③ → ② → ④ → ①的順序進行分析。大家在閱讀代碼分析時,可以參照上面的圖檔,輔助了解。好了,下面開始進行分析。
1、register(Class, JdbcType, Class) 方法分析
當代碼執行到此方法時,表示javaTypeClass != null && jdbcType != null條件成立,即使用者明确配置了javaType和jdbcType屬性的值。那下面我們來看一下該方法的分析。
public void register(Class<?> javaTypeClass, JdbcType jdbcType, Class<?> typeHandlerClass) {
// 調用終點方法
register(javaTypeClass, jdbcType, getInstance(javaTypeClass, typeHandlerClass));
}
/** 類型處理器注冊過程的終點 */
private void register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler) {
if (javaType != null) {
// JdbcType 到 TypeHandler 的映射
Map<JdbcType, TypeHandler<?>> map = TYPE_HANDLER_MAP.get(javaType);
if (map == null || map == NULL_TYPE_HANDLER_MAP) {
map = new HashMap<JdbcType, TypeHandler<?>>();
// 存儲 javaType 到 Map<JdbcType, TypeHandler> 的映射
TYPE_HANDLER_MAP.put(javaType, map);
}
map.put(jdbcType, handler);
}
// 存儲所有的 TypeHandler
ALL_TYPE_HANDLERS_MAP.put(handler.getClass(), handler);
}
上面的代碼隻有兩層調用,比較簡單,所謂的注冊過程也就是把類型和處理器進行映射而已
2、register(Class, Class) 方法分析
當代碼執行到此方法時,表示javaTypeClass != null && jdbcType == null條件成立,即使用者僅設定了javaType屬性的值。下面我們來看一下該方法的分析。
public void register(Class<?> javaTypeClass, Class<?> typeHandlerClass) {
// 調用中間方法 register(Type, TypeHandler)
register(javaTypeClass, getInstance(javaTypeClass, typeHandlerClass));
}
private <T> void register(Type javaType, TypeHandler<? extends T> typeHandler) {
// 擷取 @MappedJdbcTypes 注解
MappedJdbcTypes mappedJdbcTypes = typeHandler.getClass().getAnnotation(MappedJdbcTypes.class);
if (mappedJdbcTypes != null) {
// 周遊 @MappedJdbcTypes 注解中配置的值
for (JdbcType handledJdbcType : mappedJdbcTypes.value()) {
// 調用終點方法,參考上一小節的分析
register(javaType, handledJdbcType, typeHandler);
}
if (mappedJdbcTypes.includeNullJdbcType()) {
// 調用終點方法,jdbcType = null
register(javaType, null, typeHandler);
}
} else {
// 調用終點方法,jdbcType = null
register(javaType, null, typeHandler);
}
}
上面的代碼包含三層調用,其中終點方法的邏輯上一節已經分析過,主要做的事情是嘗試從注解中擷取JdbcType的值
3、register(Class) 方法分析
當代碼執行到此方法時,表示javaTypeClass == null && jdbcType != null條件成立,即使用者未配置javaType和jdbcType屬性的值。該方法的分析如下。
public void register(Class<?> typeHandlerClass) {
boolean mappedTypeFound = false;
// 擷取 @MappedTypes 注解
MappedTypes mappedTypes = typeHandlerClass.getAnnotation(MappedTypes.class);
if (mappedTypes != null) {
// 周遊 @MappedTypes 注解中配置的值
for (Class<?> javaTypeClass : mappedTypes.value()) {
// 調用注冊方法 ②
register(javaTypeClass, typeHandlerClass);
mappedTypeFound = true;
}
}
if (!mappedTypeFound) {
// 調用中間方法 register(TypeHandler)
register(getInstance(null, typeHandlerClass));
}
}
public <T> void register(TypeHandler<T> typeHandler) {
boolean mappedTypeFound = false;
// 擷取 @MappedTypes 注解
MappedTypes mappedTypes = typeHandler.getClass().getAnnotation(MappedTypes.class);
if (mappedTypes != null) {
for (Class<?> handledType : mappedTypes.value()) {
// 調用中間方法 register(Type, TypeHandler)
register(handledType, typeHandler);
mappedTypeFound = true;
}
}
// 自動發現映射類型
if (!mappedTypeFound && typeHandler instanceof TypeReference) {
try {
TypeReference<T> typeReference = (TypeReference<T>) typeHandler;
// 擷取參數模闆中的參數類型,并調用中間方法 register(Type, TypeHandler)
register(typeReference.getRawType(), typeHandler);
mappedTypeFound = true;
} catch (Throwable t) {
}
}
if (!mappedTypeFound) {
// 調用中間方法 register(Class, TypeHandler)
register((Class<T>) null, typeHandler);
}
}
public <T> void register(Class<T> javaType, TypeHandler<? extends T> typeHandler) {
// 調用中間方法 register(Type, TypeHandler)
register((Type) javaType, typeHandler);
}
不管是通過注解的方式,還是通過反射的方式,它們最終目的是為了解析出javaType的值。解析完成後,這些方法會調用中間方法register(Type, TypeHandler),這個方法負責解析jdbcType,該方法上一節已經分析過。一個複雜解析 javaType,另一個負責解析 jdbcType,邏輯比較清晰了。那我們趁熱打鐵,繼續分析下一個注冊方法,編号為①。
4、register(String) 方法分析
本節代碼的主要是用于自動掃描類型處理器,并調用其他方法注冊掃描結果。該方法的分析如下:
public void register(String packageName) {
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
// 從指定包中查找 TypeHandler
resolverUtil.find(new ResolverUtil.IsA(TypeHandler.class), packageName);
Set<Class<? extends Class<?>>> handlerSet = resolverUtil.getClasses();
for (Class<?> type : handlerSet) {
// 忽略内部類,接口,抽象類等
if (!type.isAnonymousClass() && !type.isInterface() && !Modifier.isAbstract(type.getModifiers())) {
// 調用注冊方法 ④
register(type);
}
}
}
小結
類型處理器的解析過程不複雜,但是注冊過程由于重載方法間互相調用,導緻調用路線比較複雜。這個時候需要想辦法理清方法的調用路線,理清後,整個邏輯就清晰明了了。