一、簡介
我們上一個篇文章已經配置好了,mybatis
配置文件和測試類。我們先分析一下mybatis
的是如何加載mybatis-config.xml
文件的。
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
這裏是通過mybatis
工具類Resources
加載配置文件,得到一個InputStream
輸入流。
二、資源文件加載
接着我們先看一下Resources
的資源加載工具類。
public static InputStream getResourceAsStream(ClassLoader loader, String resource) throws IOException {
// 通過classLoaderWrapper包裝類加載
InputStream in = classLoaderWrapper.getResourceAsStream(resource, loader);
if (in == null) {
throw new IOException("Could not find resource " + resource);
}
return in;
}
接着通過類加載器,從資源路徑(classpath
)中加載資源,
public InputStream getResourceAsStream(String resource, ClassLoader classLoader) {
return getResourceAsStream(resource, getClassLoaders(classLoader));
}
這時候初始化類加載有五種:
ClassLoader[] getClassLoaders(ClassLoader classLoader) {
return new ClassLoader[]{
classLoader, // 出入的類加載
defaultClassLoader, // 默認類加載
Thread.currentThread().getContextClassLoader(), // 線程類上下文加載器
getClass().getClassLoader(), // 當前類加載器
systemClassLoader // 系統類加載器
};
}
然後會從這些類加載器中加載我們需要資源:
InputStream getResourceAsStream(String resource, ClassLoader[] classLoader) {
// 循環遍歷獲取資源
for (ClassLoader cl : classLoader) {
if (null != cl) {
// try to find the resource as passed
InputStream returnValue = cl.getResourceAsStream(resource);
// 有些加載器需要添加`/`,所以加上這個`/`前綴,重試
if (null == returnValue) {
returnValue = cl.getResourceAsStream("/" + resource);
}
if (null != returnValue) {
return returnValue;
}
}
}
return null;
}
資源路徑: 是我們編譯之後target/classes/
目錄下的資源。
這個邏輯其實還是很簡單的,重點是我們需要複習一下類加載器相關的技能。下面是我們常見的類加載器基本上是這些:
ClassLoader.getSystemClassLoader()
:系統類加載器xxx.class.getClassLoader()
和this.getClass().getClassLoader()
:當前類加載器Thread.currentThread().getContextClassLoader()
:線程上下文加載器
主要區別:
getClassLoader
:是獲取加載當前類的類加載器。可能是***啓動類加載器***、拓展類加載器、系統類加載器,取決於當前類是有哪個加載器加載的。getClontextCLassLoader
:是獲取當前線程上下文的類加載器,用戶可以自己設置,Java SE
環境下一般是AppClassLoader
,Java EE
環境下是WebappClassLoader
。getSystemClassLoader
:是獲取系統類加載器AppClassLoader
。
三、構建SessionFactory
接着我們看看一下sessionFactory
是如何構建的
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
// 構建sessionFactory
SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
我們看一下SqlSessionFactoryBuilder
這個類,這個就一個核心方法:build
。這個方法大體分成兩類,都是參數重載的方法。
public SqlSessionFactory build(InputStream inputStream) {
return build(inputStream, null, null);
}
這個方法可以將數據源信息和屬性一起構建,我們是從配置文件中構建的,所以這兩個參數時null
。
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
// 構建配置文件解析器
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
// 這裏面調用的是解析器的解析各個節點
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.
}
}
}
這裏面核心方法是parser.parse()
。這裏面用來解析mybatis-config.xml
的各個節點。
public Configuration parse() {
// step 防止別多次讀取
if (parsed) {
throw new BuilderException("每個XMLConfigBuilder只能使用一次.");
}
parsed = true;
// step 解析各個節點
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
/**
* 解析配置文件的各個節點
* @param root 根節點
*/
private void parseConfiguration(XNode root) {
try {
// 解析 properties 節點
propertiesElement(root.evalNode("properties"));
// 解析 解析 settings 配置,並將其轉換爲 Properties 對象
Properties settings = settingsAsProperties(root.evalNode("settings"));
// 加載 vfs VFS主要用來加載容器內的各種資源,比如jar或者class文件
loadCustomVfs(settings);
// 解析日誌配置
loadCustomLogImpl(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("解析SQL Mapper配置時出錯. 原因: " + e, e);
}
}
3.1. properties
節點的解析
我們在mybatis-config.xml
中配置properties
節點, 我們裏面配置的是resource
。
<configuration>
<properties resource="jdbc.properties" />
......
</configuration>
jdbc.properties
裏面配置的是我們的數據庫連接相關信息
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://192.168.252.139:3306/mybatis-test
jdbc.username=root
jdbc.password=123456
接着看着propertiesElement
這個是如何解析properties
節點的。
/**
* 解析properties節點
* @param context properties節點
* @throws Exception
*/
private void propertiesElement(XNode context) throws Exception {
if (context != null) {
// step 讀取子節點數據
Properties defaults = context.getChildrenAsProperties();
// step 獲取節點裏面的resource和url
String resource = context.getStringAttribute("resource");
String url = context.getStringAttribute("url");
// step resource和url只能有一個存在
if (resource != null && url != null) {
throw new BuilderException("properties元素不能同時指定URL和基於資源的屬性文件引用。請指定其中一個。");
}
// step resource存在,使用Resources工具類加載properties文件
if (resource != null) {
defaults.putAll(Resources.getResourceAsProperties(resource));
} else if (url != null) {
defaults.putAll(Resources.getUrlAsProperties(url));
}
// step 如果節點裏面有屬性,在設置到defaults裏面
Properties vars = configuration.getVariables();
if (vars != null) {
defaults.putAll(vars);
}
// step 將屬性設置到XPathParser對象裏面
parser.setVariables(defaults);
// step 將屬性設置到configuration對象裏面
configuration.setVariables(defaults);
}
}
這裏我們可以看到,在加載properties
節點的時候,會先加載子節點的數據,然後纔會加載resource
中的數據,所有就會發生resource
會覆蓋properties
節點中子節點數據。
3.2. setting
節點解析
setting
配置是mybatis
中非常重要的配置,一般我們使用默認配置就可以了。我們先看一下mybatis
有哪些全局運行參數:
cacheEnabled
:默認是true
,改配置影響的所有映射器中配置的緩存的全局開關。lazyLoadingEnabled
:默認是true
, 延遲加載的全局開關,當開啓時,所有關聯對象都會延遲加載。特定關聯關係中可以通過設置fetchType
屬性來覆蓋該項的開關狀態。multipleResultSetsEnabled
:默認值是true
,是否允許單一語句返回多結果集(需要兼容驅動)useColumnLabel
: 默認值是true
,使用列標籤代替列名。不同驅動有不同的表現。useGeneratedKeys
:默認值false
,允許自動生成主鍵,需要驅動兼容。autoMappingBehavior
:默認值PARTIAL
,指定mybatis
應如何自動映射到字段或者屬性。NONE
:表示取消自動映射,PARTIAY
:只會自動映射沒有定義嵌套結果集映射的結果集,FULL
會自動映射任意複雜的結果集autoMappingUnkownColumnBehavior
:默認值WARNING
。defaultExecutorType
:默認值SIMPLE
,配置默認的執行器。SIMPLE
:就是普通執行器,REUSE
:執行器會重用預處理器(prepared statements)
,BATCH
:執行器將重用語句並執行批量更新。defaultStatementTimeout
:默認值25
,設置超時時間,它決定驅動等待數據庫影響的秒數。defaultFetchSize
:默認值100
,爲驅動的結果集獲取數量(fetchSize
)設置一個建議值。此參數只可以在查詢設置中被覆蓋。safeRowBoundsEnabled
:默認值false
,允許在嵌套語句中使用分頁RowBounds
。mapUnderscoreToCamelCase
:默認值false
,是否開啓自動駝峯命名規則映射。localCacheScope
:MyBatis
利用本地緩存機制(Local Cache
)防止循環引用和加速重複的嵌套查詢。 默認值爲SESSION
,會緩存一個會話中執行的所有查詢。 若設置值爲STATEMENT
,本地緩存將僅用於執行語句,對相同SqlSession
的不同查詢將不會進行緩存。jdbcTypeForNull
:當沒有提供特定的JDBC
類型時,爲空值指定JDBC
類型。某些驅動需要指定列的JDBC
類型,多數情況直接用一般類型,比如NULL
、VARCHAR
或者OTHER
。lazyLoadTriggerMethods
:默認值equals
、clone
、hashCode
、toString
,指定哪個對象的方法觸發一次延遲加載。
其他的配置可以參看官網:https://mybatis.org/mybatis-3/zh/configuration.html
。
一般來說這些配置我們保持默認就可以。先添加一些setting
的配置。
<settings>
<setting name="cacheEnabled" value="true"/>
<setting name="lazyLoadingEnabled" value="true"/>
</settings>
接着我們看一下如何解析setting
節點的。
private Properties settingsAsProperties(XNode context) {
if (context == null) {
return new Properties();
}
// 獲取setting屬性內容
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;
}
context.getChildrenAsProperties
可以獲取我們配置的那些配置項。
MetaClass.forClass
將用於獲取所有的全局配置信息。在裏面或包含我們方纔配置的那兩個屬性。這個MetaClass
具體如何運行的,我們後面分析。(這個地方我們可以先去省略MetaClass
中操作,這個裏面方法還是挺多的,我們可以知道其的作用是啥,具體怎麼實現後面再看,當成一個黑盒子,不影響我們整體去看源碼的結構。)
最後需要將setting
解析出來的數據設置到configuration
中:
這裏面我們和3.2
上面的那些全局屬性相互對應的看看就可以了。
3.3. typeAliases
別名節點解析
現在我們先配置一些別名,我們先來測試debug
跟一下代碼我們就知道怎麼解析別名了
在mybatis
中,我們可以一些類定義一個別名,在使用的時候用直接用別名就可以了,不需要出入類所在類路徑。
/**
* 解析typeAliases
* @param parent
*/
private void typeAliasesElement(XNode parent) {
if (parent != null) {
// step 1. 遍歷typeAliases節點
for (XNode child : parent.getChildren()) {
// step 2. 判斷是否節點名字爲 package
if ("package".equals(child.getName())) {
// step 2.1. 獲取節點屬性的name
String typeAliasPackage = child.getStringAttribute("name");
configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
} else { // step 3. 判斷節點名爲typeAlias
// step 3.1. 獲取節點屬性的alias和type
String alias = child.getStringAttribute("alias");
String type = child.getStringAttribute("type");
try {
// step 3.2. 獲取class
Class<?> clazz = Resources.classForName(type);
// step 3.3. 註冊別名到映射
if (alias == null) {
typeAliasRegistry.registerAlias(clazz);
} else {
typeAliasRegistry.registerAlias(alias, clazz);
}
} catch (ClassNotFoundException e) { // step 4. class沒有找到拋出異常
throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e);
}
}
}
}
}
從這個大體的解析過程,我們可以看到mybatis
解析別名的時候是有兩種方法的,第一種是解析<package name="com.mly.learn.entity"/>
這一中通過掃描包的方式將別名註冊到映射,第二種是解析<typeAlias type="com.mly.learn.entity.Student" alias="Student" />
這兩個是不可以並存的,我們只能選擇其中一種。
接着我們看一下這兩種方法是如何註冊到映射中的。
首先mybatis
已經註冊一堆類型映射。
在有的別名也需要註冊到typeAliases
的map
中。我們看一下mybatis
是如何將別名註冊到map
中的呢!
public void registerAliases(String packageName, Class<?> superType) {
// 包掃描或者我們在package下的類
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
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);
}
}
}
接着在registerAlias
判斷類上是否有Alias
註解,有的話將value
作爲key
,或者將類名作爲key
,然後判斷typeAliases
中是否存在或者value
相同,否則拋出異常。
/**
* 註冊別名
* @param type
*/
public void registerAlias(Class<?> type) {
// step 獲取類名
String alias = type.getSimpleName();
// step 2. 判斷是否存在Alias 註解, 存在的話,將註解中value值作爲key, 否則將類名作爲key
Alias aliasAnnotation = type.getAnnotation(Alias.class);
if (aliasAnnotation != null) {
alias = aliasAnnotation.value();
}
registerAlias(alias, type);
}
/**
* 註冊alias:class到map中
* @param alias
* @param value
*/
public void registerAlias(String alias, Class<?> value) {
// step 1. 別名== null, 拋出異常
if (alias == null) {
throw new TypeException("The parameter alias cannot be null");
}
// step 2. 轉成小寫字母
String key = alias.toLowerCase(Locale.ENGLISH);
// step 3. 判斷typeAliases是否包含key或者value相同,拋出異常
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);
}
另一種方法,實際調用的還是registerAlias
這個方法一樣的。就不做累述。
3.4. typeHandlers
類型轉換節點解析
我們數據庫的類型和Java
類型是不一樣的,我們需要進行轉換纔可以,例如我們在使用Java
的String
類型,但是在MySql
數據類型是char
或者varchar
類型。這種數據類型之後的對應就需要typeHanlder
進行準換。
看一下默認註冊的一些類型轉換器:
接着看一下mybatis
的是如何處理typeHandler
的xml
節點的。
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) {
typeHandlerRegistry.register(javaTypeClass, typeHandlerClass);
} else {
typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);
}
} else {
typeHandlerRegistry.register(typeHandlerClass);
}
}
}
}
}
掃描包下面的class
將其註冊到類型轉換映射器中,這裏面的就是各種重載方法之間的調用,有點回看暈,先畫個圖看看啊。
先看一下掃描包這個方法執行的過程, 代碼就不一一展示了。
public void register(String packageName) {
// 掃描包路徑下的class
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
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);
}
}
}
其他的就不一一貼代碼了,可以跟着上面的圖一一看一下就OK,邏輯不復雜。
private void register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler) {
if (javaType != null) {
Map<JdbcType, TypeHandler<?>> map = typeHandlerMap.get(javaType);
if (map == null || map == NULL_TYPE_HANDLER_MAP) {
map = new HashMap<>();
}
map.put(jdbcType, handler);
typeHandlerMap.put(javaType, map);
}
allTypeHandlersMap.put(handler.getClass(), handler);
}
3.5. environments
節點解析
在MyBatis
中,事務管理器和數據源是配置在<environments>
節點中的。接着我們來看是如何解析的:
private void environmentsElement(XNode context) throws Exception {
if (context != null) {
if (environment == null) {
// step 獲取默認節點
environment = context.getStringAttribute("default");
}
// step 遍歷environment節點
for (XNode child : context.getChildren()) {
// step 獲取id的子節點
String id = child.getStringAttribute("id");
// step 判斷是否爲指定的節點
if (isSpecifiedEnvironment(id)) {
// step 配置事務
TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
// step 構建 DataSourceFactory 對象
DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
DataSource dataSource = dsFactory.getDataSource();
// step 將事務處理器、數據源和id組裝到一起,並設置到configuration中
Environment.Builder environmentBuilder = new Environment.Builder(id)
.transactionFactory(txFactory)
.dataSource(dataSource);
configuration.setEnvironment(environmentBuilder.build());
}
}
}
}
當我們配置兩個數據源的時候,mybatis
只會讀取default
的數據源ID
。然後將事務管理器和數據源組裝成一個Environment
對象。
這個是比較簡單。
3.6. plugins
插件節點解析
這個部分在分析分頁插件的時候在去具體分析吧。先挖個坑。