接上一節 上一節:解析properties和settings
解析typeAliases
typeAliases節點用於配置別名。別名在mapper中使用resultType時會使用到,是對實體類的簡寫。
別名有兩種配置方式
- 通過package,直接掃描指定包下所有的類,註冊別名
- 通過typeAliase,指定某個類爲其註冊別名
別名註冊代碼如下
/**
* 解析typeAliases節點
*
* @param parent
*/
private void typeAliasesElement(XNode parent) {
if (parent != null) {
// 遍歷所有子節點
// typeAliases節點有兩個子節點,分別是package和typeAlias
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
// 獲取name屬性,package的name屬性指定的是包名
String typeAliasPackage = child.getStringAttribute("name");
// 將這個包下的所有類註冊別名
configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
} else {
// 如果配置的是typeAlias節點,就將該節點的類單獨註冊
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);
}
}
}
}
}
註冊別名
掃包後獲取到包下所有的類之後,會爲這些類生成別名,並將其註冊到Configuration中
/**
* 指定包名,將這個包下所有的類都註冊別名
*
* @param packageName
*/
public void registerAliases(String packageName) {
registerAliases(packageName, Object.class);
}
/**
* 爲指定包下所有的類註冊別名
*
* @param packageName
* @param superType
*/
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);
}
}
}
/**
* 註冊指定類的別名
* @param type
*/
public void registerAlias(Class<?> type) {
// 得到類的簡寫名稱,即不帶包名的名稱
// 因此在mybatis掃描包下,不允許有同樣類名的類存在
// 否則在啓動時就會報錯
String alias = type.getSimpleName();
Alias aliasAnnotation = type.getAnnotation(Alias.class);
if (aliasAnnotation != null) {
// 如果有Alias註解,就以Alias註解指定的別名爲準
// 該註解可以用於解決被掃描包下含有相同名稱類的問題
alias = aliasAnnotation.value();
}
registerAlias(alias, type);
}
/**
* 註冊別名
* @param alias 別名
* @param value 指定的類
*/
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);
// 如果已經有了這個別名,並且這個別名中取到的值不爲null,並且取到的值和傳進來的類不相同就報錯
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中。key是別名,因此別名不可以重複
typeAliases.put(key, value);
}
在註冊別名時,會使用到ResolverUtil工具類。該工具類可以根據指定的條件去查找指定包下的類。該類有個內部接口Test,接口中只有一個matches方法,用於根據指定的規則去匹配。Test接口有兩個實現。ISA用於檢測該類是否繼承了指定的類或者接口,而AnnotatedWith則用於檢測是否添加了指定的註釋,代碼比較簡單這裏就不貼了。這裏使用到了find方法,用於匹配指定包下所有繼承了superType的類
public ResolverUtil<T> find(Test test, String packageName) {
String path = getPackagePath(packageName);
try {
// 獲取指定包下所有的文件名
List<String> children = VFS.getInstance().list(path);
for (String child : children) {
if (child.endsWith(".class")) {
addIfMatching(test, child);
}
}
} catch (IOException ioe) {
log.error("Could not read package: " + packageName, ioe);
}
return this;
}
/**
* 如果匹配成功,就添加到matches中
*
* @param test the test used to determine if the class matches
* @param fqn the fully qualified name of a class
*/
@SuppressWarnings("unchecked")
protected void addIfMatching(Test test, String fqn) {
try {
String externalName = fqn.substring(0, fqn.indexOf('.')).replace('/', '.');
ClassLoader loader = getClassLoader();
if (log.isDebugEnabled()) {
log.debug("Checking to see if class " + externalName + " matches criteria [" + test + "]");
}
Class<?> type = loader.loadClass(externalName);
if (test.matches(type)) {
matches.add((Class<T>) type);
}
} catch (Throwable t) {
log.warn("Could not examine class '" + fqn + "'" + " due to a " +
t.getClass().getName() + " with message: " + t.getMessage());
}
}
解析plugins
mybatis擁有強大的插件機制,可以通過配置mybatis攔截器來統一對sql、參數、返回集等進行處理,該功能廣泛運用與分頁、創建人等字段賦值、邏輯刪除、樂觀鎖等插件的編寫中。mybatis的攔截器編寫難度比spring mvc高得多,想要熟練地編寫mybatis攔截器,需要對源碼比較熟悉。
解析攔截器的代碼比較簡單,plugin節點需要配置一個interceptor屬性,該屬性是自定義攔截器的全類名。在該方法中會先獲取到該屬性,通過該屬性對應攔截器的默認構造去創建實例,並添加到Configuration中。
/**
* 解析plugins節點
* plugin節點用於配置插件
* 即 mybatis攔截器
*
* @param parent
* @throws Exception
*/
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
// 獲取子節點,子節點就是所配置的攔截器
for (XNode child : parent.getChildren()) {
// 獲得攔截器全類名
String interceptor = child.getStringAttribute("interceptor");
// 將節點下的節點封裝成properties
Properties properties = child.getChildrenAsProperties();
// 根據攔截器的全類名,通過默認構造方法創建一個實例
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
// 將攔截器節點下的properties放到攔截器中
interceptorInstance.setProperties(properties);
// 將攔截器添加到配置中
configuration.addInterceptor(interceptorInstance);
}
}
}
解析ObjectFactory節點
objectFactory用來處理查詢得到的結果集,創建對象去將結果集封裝到對象中。
mybatis默認的對象工廠是用無參構造或者有參構造去創建對象,而如果開發者想在創建對象前對其進行一些初始化操作或者處理一些業務方面的邏輯,就可以自定義對象工廠並進行配置。對象工廠的解析比較簡單,拿到type屬性去創建一個實例並添加到Configuration即可。
/**
* 解析objectFactory節點
* objectFactory用來處理查詢得到的結果集
* 創建對象去將結果集封裝到對象中
* mybatis默認的object工廠是用無參構造或者有參構造去創建對象
* 而如果開發者想在創建對象前對其中的一些屬性做初始化操作
* 或者做一些業務方面的邏輯
* 就可以自己去創建對象工廠並進行配置
*
* @param context
* @throws Exception
*/
private void objectFactoryElement(XNode context) throws Exception {
if (context != null) {
// 拿到objectFactory節點的type屬性,該屬性爲對象工廠的全類名
String type = context.getStringAttribute("type");
// 拿到節點下所有的properties
Properties properties = context.getChildrenAsProperties();
// 根據type對應的類,通過默認構造去創建一個實例
ObjectFactory factory = (ObjectFactory) resolveClass(type).getDeclaredConstructor().newInstance();
// 將properties放入對象工廠
factory.setProperties(properties);
// 將對象工廠添加到配置中去
configuration.setObjectFactory(factory);
}
}
解析objectWrapperFactory和reflectorFactory
這兩個節點的解析很簡單,這裏只貼上代碼給讀者去閱讀,很容易就能理解。
/**
* 解析objectWrapperFactory節點
*
* @param context
* @throws Exception
*/
private void objectWrapperFactoryElement(XNode context) throws Exception {
if (context != null) {
// 獲取到type屬性
String type = context.getStringAttribute("type");
// 根據type屬性對應的類去創建對象
ObjectWrapperFactory factory = (ObjectWrapperFactory) resolveClass(type).getDeclaredConstructor().newInstance();
// 將對象放到配置中
configuration.setObjectWrapperFactory(factory);
}
}
/**
* 解析reflectorFactory節點。代碼比較簡單就不寫了。
* 解析流程和objectWrapperFactory一毛一樣
*
* @param context
* @throws Exception
*/
private void reflectorFactoryElement(XNode context) throws Exception {
if (context != null) {
String type = context.getStringAttribute("type");
ReflectorFactory factory = (ReflectorFactory) resolveClass(type).getDeclaredConstructor().newInstance();
configuration.setReflectorFactory(factory);
}
}
處理settings節點
在前一篇文章,已經將解析settings節點的代碼講解完畢,該方法則是用來將解析後的settings節點中的配置,一一添加到Configuration。代碼簡單粗暴,就是一堆set
/**
* 將settings節點的配置set到Configuration中
*
* @param props
*/
private void settingsElement(Properties props) {
configuration.setAutoMappingBehavior(AutoMappingBehavior.valueOf(props.getProperty("autoMappingBehavior", "PARTIAL")));
configuration.setAutoMappingUnknownColumnBehavior(AutoMappingUnknownColumnBehavior.valueOf(props.getProperty("autoMappingUnknownColumnBehavior", "NONE")));
configuration.setCacheEnabled(booleanValueOf(props.getProperty("cacheEnabled"), true));
configuration.setProxyFactory((ProxyFactory) createInstance(props.getProperty("proxyFactory")));
configuration.setLazyLoadingEnabled(booleanValueOf(props.getProperty("lazyLoadingEnabled"), false));
configuration.setAggressiveLazyLoading(booleanValueOf(props.getProperty("aggressiveLazyLoading"), false));
configuration.setMultipleResultSetsEnabled(booleanValueOf(props.getProperty("multipleResultSetsEnabled"), true));
configuration.setUseColumnLabel(booleanValueOf(props.getProperty("useColumnLabel"), true));
configuration.setUseGeneratedKeys(booleanValueOf(props.getProperty("useGeneratedKeys"), false));
configuration.setDefaultExecutorType(ExecutorType.valueOf(props.getProperty("defaultExecutorType", "SIMPLE")));
configuration.setDefaultStatementTimeout(integerValueOf(props.getProperty("defaultStatementTimeout"), null));
configuration.setDefaultFetchSize(integerValueOf(props.getProperty("defaultFetchSize"), null));
configuration.setDefaultResultSetType(resolveResultSetType(props.getProperty("defaultResultSetType")));
configuration.setMapUnderscoreToCamelCase(booleanValueOf(props.getProperty("mapUnderscoreToCamelCase"), false));
configuration.setSafeRowBoundsEnabled(booleanValueOf(props.getProperty("safeRowBoundsEnabled"), false));
configuration.setLocalCacheScope(LocalCacheScope.valueOf(props.getProperty("localCacheScope", "SESSION")));
configuration.setJdbcTypeForNull(JdbcType.valueOf(props.getProperty("jdbcTypeForNull", "OTHER")));
configuration.setLazyLoadTriggerMethods(stringSetValueOf(props.getProperty("lazyLoadTriggerMethods"), "equals,clone,hashCode,toString"));
configuration.setSafeResultHandlerEnabled(booleanValueOf(props.getProperty("safeResultHandlerEnabled"), true));
configuration.setDefaultScriptingLanguage(resolveClass(props.getProperty("defaultScriptingLanguage")));
configuration.setDefaultEnumTypeHandler(resolveClass(props.getProperty("defaultEnumTypeHandler")));
configuration.setCallSettersOnNulls(booleanValueOf(props.getProperty("callSettersOnNulls"), false));
configuration.setUseActualParamName(booleanValueOf(props.getProperty("useActualParamName"), true));
configuration.setReturnInstanceForEmptyRow(booleanValueOf(props.getProperty("returnInstanceForEmptyRow"), false));
configuration.setLogPrefix(props.getProperty("logPrefix"));
configuration.setConfigurationFactory(resolveClass(props.getProperty("configurationFactory")));
}
解析environments
該節點標識環境配置。所謂環境,就是隻運行時需要的一系列參數,也可以理解成開發中常說的“開發環境”“測試環境”“生產環境”。環境最直觀的就是在不同環境下連接不同的數據庫。
environments節點下提供了數據源和事務配置。
/**
* 解析environments節點
* 該節點表示環境配置
* 所謂環境,就是指運行時環境,即開發環境、測試環境、生產環境
* 環境最直觀的提現就是在不同環境下數據庫不同
* environments節點下就提供了數據源和事務配置
*
* @param context
* @throws Exception
*/
private void environmentsElement(XNode context) throws Exception {
if (context != null) {
if (environment == null) {
// 獲取默認的環境id
environment = context.getStringAttribute("default");
}
for (XNode child : context.getChildren()) {
// 拿到子節點的id。父節點的default屬性對應的就是子節點id
String id = child.getStringAttribute("id");
if (isSpecifiedEnvironment(id)) {
// 解析事務管理器
TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
// 解析數據工廠
DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
// 從工廠中拿到數據庫
DataSource dataSource = dsFactory.getDataSource();
// 創建環境並set到Configuration中去
Environment.Builder environmentBuilder = new Environment.Builder(id)
.transactionFactory(txFactory)
.dataSource(dataSource);
configuration.setEnvironment(environmentBuilder.build());
}
}
}
}
解析的邏輯中,需要一併解析事務工廠和DataSource工廠。代碼比較簡單,和objectWrapperFactory一樣,這裏就不貼了
解析databaseIdProvider
該節點用於提供多數據源支持。這裏的多數據源並非指多個數據庫,而是指多個數據庫產品。這裏的代碼和objectWrapperFactory比較類似,不做過多解釋。
/**
* 解析databaseIdProvider節點
* 該節點用於提供多數據源支持
* 這裏的多數據源並非指多個數據庫
* 而是指多個數據庫產品
* 這裏的代碼和objectFactory類似
*
* @param context
* @throws Exception
*/
private void databaseIdProviderElement(XNode context) throws Exception {
DatabaseIdProvider databaseIdProvider = null;
if (context != null) {
String type = context.getStringAttribute("type");
// awful patch to keep backward compatibility
if ("VENDOR".equals(type)) {
type = "DB_VENDOR";
}
Properties properties = context.getChildrenAsProperties();
databaseIdProvider = (DatabaseIdProvider) resolveClass(type).getDeclaredConstructor().newInstance();
databaseIdProvider.setProperties(properties);
}
// 獲取當前環境
Environment environment = configuration.getEnvironment();
if (environment != null && databaseIdProvider != null) {
String databaseId = databaseIdProvider.getDatabaseId(environment.getDataSource());
configuration.setDatabaseId(databaseId);
}
}
結語
昨天加班填坑加到了12點,就沒有繼續爲代碼加註釋,今天在空閒的時候就繼續填坑了。目前對mybatis-config.xml文件的解析基本接近尾聲,還差typeHandlers和mappers兩個節點沒有進行註釋。相信看了這兩篇文章的讀者對於解析配置文件的邏輯已經有了一定的理解,因此自己閱讀後面兩個節點的代碼解析應該不難。明天或者後天會將最後的兩個節點的解析補上