咱們知道在使用 Mybatis
時,咱們須要經過 SqlSessionFactoryBuild
去建立 SqlSessionFactory
實例,譬如:java
// resource 爲 mybatis 的配置文件
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
複製代碼
那麼咱們看下 build
方法的具體實現sql
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
try {
// 建立 XMLConfigBuilder 實例並執行解析
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
reader.close();
} catch (IOException e) {
}
}
}
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
複製代碼
Mybatis
主要經過 XMLConfigBuilder
執行對配置文件的解析,具體實現以下文:數據庫
private void parseConfiguration(XNode root) {
try {
//issue #117 read properties first
// 解析 properties 標籤
propertiesElement(root.evalNode("properties"));
// 解析 settings 標籤
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
// 解析 typeAliases 別名標籤
typeAliasesElement(root.evalNode("typeAliases"));
// 解析 plugins 插件標籤
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
// 解析 environments 標籤
environmentsElement(root.evalNode("environments"));
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);
}
}
複製代碼
從 XMLConfigBuilder
的方法 parseConfiguration
實現咱們知道,MyBatis
會依次解析配置文件中的相應標籤,本文將針對開發中經常使用的配置進行分析;主要包括 properties
, typeAliases
, enviroments
, typeHandlers
, mappers
。apache
<configuration>
<!-- 能夠指定 resource 屬性,也能夠指定 url 屬性 -->
<properties resource="org/mybatis/example/config.properties">
<property name="username" value="dev_user"/>
<property name="password" value="F2Fa3!33TYyg"/>
</properties>
</configuration>
複製代碼
從配置示例能夠看出 properties
屬性變量的來源能夠是外部的配置文件,也能夠是配置文件中自定義的,也能夠是 SqlSessionFactoryBuilder
的 build
方法傳參譬如:session
public SqlSessionFactory build(InputStream inputStream, Properties properties) {
return build(inputStream, null, properties);
}
複製代碼
那麼當存在同名的屬性時,將採用哪一種方式的屬性值呢?數據結構
private void propertiesElement(XNode context) throws Exception {
if (context != null) {
// 獲取 properties 標籤下的全部 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.");
}
if (resource != null) {
// 加載 resource 指定的配置文件
defaults.putAll(Resources.getResourceAsProperties(resource));
} else if (url != null) {
// 加載 url 指定的配置文件
defaults.putAll(Resources.getUrlAsProperties(url));
}
/** * 獲取傳參的 properties * 構建 sqlSessionFactory 時能夠傳參 properties * * @see SqlSessionFactoryBuilder.build(InputStream inputStream, Properties properties) */
Properties vars = configuration.getVariables();
if (vars != null) {
defaults.putAll(vars);
}
parser.setVariables(defaults);
// 將 properties 賦值 configuration 中的 variables 變量
configuration.setVariables(defaults);
}
}
複製代碼
public Properties getChildrenAsProperties() {
Properties properties = new Properties();
// 遍歷 properties 標籤下的 propertry 子標籤
for (XNode child : getChildren()) {
// 獲取 propertry 的 name value 屬性
String name = child.getStringAttribute("name");
String value = child.getStringAttribute("value");
if (name != null && value != null) {
properties.setProperty(name, value);
}
}
return properties;
}
複製代碼
從 properties
標籤解析的實現來看,MyBatis
加載 properties
屬性的過程以下:mybatis
properties
標籤內全部子標籤的 property
properties
標籤屬性 resource
或 url
指定的外部屬性配置SqlSessionFactoryBuilder
的方法 build
傳參的屬性配置所以,經過方法參數傳遞的
properties
具備最高優先級,resource/url 屬性中指定的配置文件次之,最低優先級的是properties
標籤內的子標籤property
指定的屬性。app
類型別名是爲 Java 類型設置一個短的名字。它只和 XML 配置有關,存在的意義僅在於用來減小類徹底限定名的冗餘dom
<typeAliases>
<typeAlias alias="Author" type="domain.blog.Author"/>
<typeAlias alias="Blog" type="domain.blog.Blog"/>
<typeAlias alias="Comment" type="domain.blog.Comment"/>
</typeAliases>
複製代碼
也能夠指定一個包名,MyBatis
會在包名下面搜索須要的 Java Bean,好比:ide
<typeAliases>
<package name="domain.blog"/>
</typeAliases>
複製代碼
private void typeAliasesElement(XNode parent) {
if (parent != null) {
for (XNode child : parent.getChildren()) {
// 若是是 package 標籤,對整個包下的 java bean 進行別名處理
// 若 java bean 沒有配置註解的話,使用 bean 的首字母小寫類名做爲別名
// 若 java bean 配置了註解,使用註解值做爲別名
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) {
// 默認別名爲類名,若配置了別名註解則取註解值映射類
typeAliasRegistry.registerAlias(clazz);
} else {
// 經過指定的別名映射類
typeAliasRegistry.registerAlias(alias, clazz);
}
} catch (ClassNotFoundException e) {
throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e);
}
}
}
}
}
複製代碼
typeAliasesElement
在對 typeAliases
標籤解析時,針對採用 package
和 typeAlias
兩種配置方式進行了不一樣的解析。 下面咱們先看下經過包名的配置方式
public void registerAliases(String packageName) {
registerAliases(packageName, Object.class);
}
public void registerAliases(String packageName, Class<?> superType) {
// 獲取包下全部的類
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
Set<Class<? extends Class<?>>> typeSet = resolverUtil.getClasses();
for (Class<?> type : typeSet) {
// Ignore inner classes and interfaces (including package-info.java)
// Skip also inner classes. See issue #6
// 忽略內部類 接口
if (!type.isAnonymousClass() && !type.isInterface() && !type.isMemberClass()) {
registerAlias(type);
}
}
}
public void registerAlias(Class<?> type) {
// 別名爲類名
String alias = type.getSimpleName();
// 是否配置了別名註解,若配置了則別名取註解值
Alias aliasAnnotation = type.getAnnotation(Alias.class);
if (aliasAnnotation != null) {
alias = aliasAnnotation.value();
}
registerAlias(alias, type);
}
複製代碼
當經過 package
指定包名時,MyBatis
會掃描包下全部的類(忽略內部類,接口),若類沒有采用 @Alias
註解的狀況下,會使用 Bean 的首字母小寫的非限定類名來做爲它的別名, 好比 domain.blog.Author
的別名爲 author;如有註解,則別名爲其註解值。
public void registerAlias(String alias, Class<?> value) {
if (alias == null) {
throw new TypeException("The parameter alias cannot be null");
}
// issue #748
// 別名小寫處理
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);
}
複製代碼
在完成別名的解析以後會將其註冊到 typeAliasRegistry
的變量 typeAliases
Map 集合中。
environments
用於事務管理器及數據源相關配置
<environments default="development">
<environment id="development">
<transactionManager type="JDBC">
<property name="..." value="..."/>
</transactionManager>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
<environment id="test">
<transactionManager type="JDBC">
<property name="..." value="..."/>
</transactionManager>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
複製代碼
從
environments
的配置來看MyBatis
是支持多數據源的,但每一個SqlSessionFactory
實例只能選擇其中一個; 若須要鏈接多個數據庫,就得須要建立多個SqlSessinFactory
實例。
private void environmentsElement(XNode context) throws Exception {
if (context != null) {
if (environment == null) {
/** * @see org.apache.ibatis.session.SqlSessionFactoryBuilder.build 時未指定 enviorment, 則取默認的 */
environment = context.getStringAttribute("default");
}
for (XNode child : context.getChildren()) {
String id = child.getStringAttribute("id");
// 查找與 environment 匹配的配置環境
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);
// 設置配置環境
configuration.setEnvironment(environmentBuilder.build());
}
}
}
}
複製代碼
private boolean isSpecifiedEnvironment(String id) {
if (environment == null) {
// 若 environment 爲空說明未指定當前 SqlSessionFactory 實例所需的配置環境;同時 environments 標籤未配置 default 屬性
throw new BuilderException("No environment specified.");
} else if (id == null) {
// environment 標籤須要配置 id 屬性
throw new BuilderException("Environment requires an id attribute.");
} else if (environment.equals(id)) {
// environment == id 說明當前匹配配置環境
return true;
}
return false;
}
複製代碼
因 environments
支持多數據源的配置,因此在解析時會先查找匹配當前 SqlSessionFactory
的 environment
; 而後在解析當前配置環境所需的事務管理器和數據源。
private TransactionFactory transactionManagerElement(XNode context) throws Exception {
if (context != null) {
// 獲取配置事務管理器的類別,也就是別名
String type = context.getStringAttribute("type");
// 獲取事務屬性配置
Properties props = context.getChildrenAsProperties();
// 經過別名查找對應的事務管理器類並實例化
TransactionFactory factory = (TransactionFactory) resolveClass(type).newInstance();
factory.setProperties(props);
return factory;
}
throw new BuilderException("Environment declaration requires a TransactionFactory.");
}
複製代碼
事務管理器解析時會經過配置中指定的 type
別名去查找對應的 TransactionFactory
並實例化。
那麼
MyBatis
內部內置了哪些事務管理器呢?
public Configuration() {
typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class);
// 省略
}
複製代碼
從 Configuration
的構造能夠看出,其構造時會經過 typeAliasRegistry
註冊了別名爲 JDBC
,MANAGED
的兩種事務管理器。
private DataSourceFactory dataSourceElement(XNode context) throws Exception {
if (context != null) {
// 獲取配置數據源的類別,也就是別名
String type = context.getStringAttribute("type");
// 獲取數據源屬性配置
Properties props = context.getChildrenAsProperties();
// 經過別名查找數據源並實例化
DataSourceFactory factory = (DataSourceFactory) resolveClass(type).newInstance();
factory.setProperties(props);
return factory;
}
throw new BuilderException("Environment declaration requires a DataSourceFactory.");
}
複製代碼
同事務管理器同樣,數據源解析時也會經過指定的別名查找對應的數據源實現類一樣其在 Configuration
構造時向 typeAliasRegistry
註冊了三種數據源
public Configuration() {
// 省略
typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class);
typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class);
// 省略
}
複製代碼
<typeHandlers>
<typeHandler handler="org.mybatis.example.ExampleTypeHandler"/>
</typeHandlers>
複製代碼
<typeHandlers>
<package name="org.mybatis.example"/>
</typeHandlers>
複製代碼
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 {
// 映射 java 對象類型
String javaTypeName = child.getStringAttribute("javaType");
// 映射 jdbc 類型
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) {
// 指定了 java type,未指定 jdbc type
typeHandlerRegistry.register(javaTypeClass, typeHandlerClass);
} else {
// 指定了 java type,指定了 jdbc type
typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);
}
} else {
// 未指定 java type 按 typeHandlerClass 註冊
typeHandlerRegistry.register(typeHandlerClass);
}
}
}
}
}
複製代碼
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) {
// 一個 java type 可能會映射多個 jdbc type
Map<JdbcType, TypeHandler<?>> map = typeHandlerMap.get(javaType);
if (map == null || map == NULL_TYPE_HANDLER_MAP) {
map = new HashMap<>();
typeHandlerMap.put(javaType, map);
}
map.put(jdbcType, handler);
}
// 存儲 typeHandler
allTypeHandlersMap.put(handler.getClass(), handler);
}
複製代碼
當指定了
javaType
和jdbcType
最終會將兩者及typeHandler
映射並註冊到typeHandlerMap
中,從typeHandlerMap
的數據結構來看,javaType
可能會與多個jdbcType
映射。 譬如String
->CHAR
,VARCHAR
。
public void register(Class<?> javaTypeClass, Class<?> typeHandlerClass) {
// 將 type handler 實例化
register(javaTypeClass, getInstance(javaTypeClass, typeHandlerClass));
}
複製代碼
private <T> void register(Type javaType, TypeHandler<? extends T> typeHandler) {
// 獲取 MappedJdbcTypes 註解
// 該註解用於設置類型轉換器匹配的 jdbcType
MappedJdbcTypes mappedJdbcTypes = typeHandler.getClass().getAnnotation(MappedJdbcTypes.class);
if (mappedJdbcTypes != null) {
// 遍歷匹配的 jdbcType 並註冊
for (JdbcType handledJdbcType : mappedJdbcTypes.value()) {
register(javaType, handledJdbcType, typeHandler);
}
if (mappedJdbcTypes.includeNullJdbcType()) {
register(javaType, null, typeHandler);
}
} else {
// 未指定 jdbcType 時按 null 處理
register(javaType, null, typeHandler);
}
}
複製代碼
當類型轉換器配置了
javaType
未配置jdbcType
時,會判斷類型轉換器是否配置了@MappedJdbcTypes
註解; 若配置了則使用註解值做爲jdbcType
並註冊,若未配置則按 null 註冊。
public void register(Class<?> typeHandlerClass) {
boolean mappedTypeFound = false;
// 獲取 MappedTypes 註解
// 該註解用於設置類型轉換器匹配的 javaType
MappedTypes mappedTypes = typeHandlerClass.getAnnotation(MappedTypes.class);
if (mappedTypes != null) {
for (Class<?> javaTypeClass : mappedTypes.value()) {
// 執行註冊
register(javaTypeClass, typeHandlerClass);
mappedTypeFound = true;
}
}
if (!mappedTypeFound) {
register(getInstance(null, typeHandlerClass));
}
}
複製代碼
當
javaType
,jdbcType
均爲指定時,會判斷類型轉換器是否配置了@MappedTypes
註解; 若配置了則使用註解值做爲javaType
並註冊。
public void register(String packageName) {
// 掃描指定包下的全部類
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
resolverUtil.find(new ResolverUtil.IsA(TypeHandler.class), packageName);
Set<Class<? extends Class<?>>> handlerSet = resolverUtil.getClasses();
for (Class<?> type : handlerSet) {
//Ignore inner classes and interfaces (including package-info.java) and abstract classes
// 忽略內部類 接口 抽象類
if (!type.isAnonymousClass() && !type.isInterface() && !Modifier.isAbstract(type.getModifiers())) {
// 執行註冊
register(type);
}
}
}
複製代碼
當按指定包名解析時,會掃描包下的全部類(忽略內部類,接口,抽象類)並執行註冊
本文咱們主要分析了 Mybatis
配置文件中標籤 properties
,typeAliases
,enviroments
,typeHandlers
的解析過程,因爲 mappers
的解析比較複雜後續在進行分析;經過本文的分析咱們瞭解到 Configuration
實例中包括如下內容:
typeAliases
存儲別名與類的映射關係javaType
與 jdbcType
,typeHandler
的映射關係,內置 jdbcType
與 typeHandler
的映射關係