解析typeAliases元素,完成類型別名的註冊工作
typeAliases
元素在mybatis中用於完成類型別名映射的配置工作,關於mybatis的類型別名機制,我們在前面已經稍作了解,他的作用就是爲指定的JAVA類型提供一個較短的名字,從而簡化我們使用完全限定名帶來的冗餘,是簡化我們使用Mybatis時的代碼量的一個優化性操作。
在Mybatis中配置自定義別名,需要使用的元素是typeAliases
,typealiases
的DTD定義如下::
<!--ELEMENT typeAliases (typeAlias*,package*)-->
<!--ELEMENT typeAlias EMPTY-->
<!--ATTLIST typeAlias
type CDATA #REQUIRED
alias CDATA #IMPLIED
-->
<!--ELEMENT package EMPTY-->
<!--ATTLIST package
name CDATA #REQUIRED
-->
根據typeAliases
的DTD定義,在typealiases
下允許有零個或多個typeAlias
/package
節點,同時typeAlias
和package
均不允許再包含其他子節點。
其中:
typeAlias
節點用於註冊單個別名映射關係,他有兩個可填參數,type
參數指向一個java類型的全限定名稱,爲必填項,alias
參數表示該java對象的別名,非必填,默認是使用java類的Class#getSimpleName()
方法獲取的.package
通常用於批量註冊別名映射關係,他只有一個必填的參數name
,該參數指向一個java包名,包下的所有符合規則(默認是Object.class的子類)的類均會被註冊。
XmlConfigBuilder
的typeAliasesElement
方法對這兩種節點的解析工作也比較簡單:
/**
* 解析配置typeAliases節點
*
* @param parent typeAliases節點
*/
private void typeAliasesElement(XNode parent) {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
// 根據 package 來批量解析別名,別名默認取值爲實體類的SimpleName
String typeAliasPackage = child.getStringAttribute("name");
// 註冊別名映射關係到別名註冊表
configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
} else {
// 處理typeAlias配置,獲取別名和類型後執行註冊操作
// 別名
String alias = child.getStringAttribute("alias");
// java類型
String type = child.getStringAttribute("type");
try {
// 通過反射獲取java類型
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);
}
}
}
}
}
我們先看typeAlias
節點的解析過程,再看package
節點。
XmlConfigBuilder
會依次獲取typeAlias
節點的alias
和type
參數的值,並通過反射將type
轉換爲實際的java類型,然後將別名註冊的操作轉交給typeAliasRegistry
對象來完成,
如果用戶指定了alias
參數的值,那就調用TypeAliasRegistry
的resolveAlias(String,Class)
方法來完成別名註冊,該方法我們前面已經瞭解過了。
如果用戶沒有指定alias
參數的值,註冊別名的工作就交給TypeAliasRegistry
的resolveAlias(Class)
方法來完成:
/**
* 註冊指定類型的別名到別名註冊表中
* <p>
* 在沒有註解的場景下,會將實例類型的簡短名稱首字母小寫後作爲別名使用
* </p><p>
* 如果指定了{@link Alias}註解,則使用註解指定的名稱作爲別名
*
* @param 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);
}
在resolveAlias(Class)
方法中優先使用類型上標註的Alias
註解指定的值作爲別名,如果沒有標註Alias
註解,那麼就將該類型的簡短名稱作爲別名使用。
獲取到指定類型的別名之後,具體實現也是交給了resolveAlias(String,Class)
方法來完成.
Alias
註解比較簡單,他的作用就是爲指定的類型標註別名。
/**
* 用於爲指定的類提供別名
*
* @author Clinton Begin
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Alias {
/**
* 別名
* @return 別名
*/
String value();
}
看完了typeAlias
節點的解析工作,我們繼續看package
節點是如何解析的。
XmlConfigBuilder
對package
的解析工作,在得到package
的name
參數值之後,就完全交給了TypeAliasRegistry
的registerAliases(String)
方法來完成後續的流程。
// 根據 package 來批量解析別名,別名默認取值爲實體類的SimpleName
String typeAliasPackage = child.getStringAttribute("name");
// 註冊別名映射關係到別名註冊表
configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
在TypeAliasRegistry
的registerAliases(String)
方法中,又直接將工作轉交給了registerAliases(String,Class)
方法來完成:
/**
* 註冊指定包下指定類型及其子實現的別名映射關係
*
* @param packageName 指定包名稱
* @param superType 指定類型
*/
public void registerAliases(String packageName, Class<!--?--> superType) {
// 獲取指定包下所有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);
}
}
}
registerAliases(String,Class)
方法有兩個入參,其中String
類型的參數packageName
表示用於掃描JAVA類的包名稱,Class
類型的參數superType
則用於限制用於註冊別名的類必須是superType
的子類或者實現類。
在registerAliases(String,Class)
方法中藉助於ResolverUtil
來完成掃描和篩選指定包下有效類集合的工作。
在獲取到需要處理的類集合之後,TypeAliasRegistry
會將除接口、匿名類以及成員類之外的所有類通過registerAlias(Class)
方法完成別名註冊工作。
registerAlias(Class)
方法在解析typeAlias
節點時已經有過了解,此處不再贅述。
在前文中提到的用於完成掃描和篩選指定包下有效類集合的ResolverUtil
是mybatis提供的一個工具類。
ResolverUtil
定義了兩個屬性,其中ClassLoader
類型的classloader
屬性用於掃描和加載類的類加載器,默認值是Thread#currentThread().getContextClassLoader()
,同時ResolverUtil
對外暴露了他的getter
/setter
方法,用戶可以通過調用其setter
方法來使用指定的類加載器。
/**
* 用於掃描類的類加載器
*/
private ClassLoader classloader;
/**
* 獲取用於掃描類的類加載器,默認使用{@link Thread#currentThread()#getContextClassLoader()}
*
* @return 用於掃描類的類加載器
*/
public ClassLoader getClassLoader() {
return classloader == null ? Thread.currentThread().getContextClassLoader() : classloader;
}
/**
* 配置用於掃描類的類加載器
*
* @param classloader 用於掃描類的類加載器
*/
public void setClassLoader(ClassLoader classloader) {
this.classloader = classloader;
}
Set<class<? extends t>>
類型的matches
屬性負責存放所有滿足條件的Class
集合,ResolverUtil
對外暴露了他的getter
方法:
/**
* 滿足條件的類型集合
*/
private Set<class<? extends t>> matches = new HashSet<>();
/**
* 獲取所有匹配條件的類型集合
*
* @return 所有匹配條件的類型集合
*/
public Set<class<? extends t>> getClasses() {
return matches;
}
ResolverUtil
中還定義了一個Test
接口,該接口用於完成篩選類的條件測試工作:
/**
* 用於篩選類的條件測試接口定義
*/
public interface Test {
/**
* 判斷傳入的類是否滿足必要的條件
*/
boolean matches(Class<!--?--> type);
}
Test
接口只定義了一個matches
方法用於判斷傳入的類是否滿足必要的條件。
除此之外,ResolverUtil
對外暴露的最主要的方法是find(Test,String)
:
/**
* 遞歸掃描指定的包及其子包中的類,並對所有找到的類執行Test測試,只有滿足測試的類纔會保留。
*
* @param test 用於過濾類的測試對象
* @param packageName 被掃描的基礎包名
*/
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;
}
find
方法的作用是遞歸掃描指定的包及其子包中的類,並對所有找到的類執行Test
測試,只有滿足測試條件的類纔會保留。
在find
方法中,首先將傳入的包名packageName
轉換爲文件路徑,
/**
* 將包名轉換爲文件路徑
*
* @param packageName 包名
*/
protected String getPackagePath(String packageName) {
return packageName == null ? null : packageName.replace('.', '/');
}
之後藉助前文配置的VFS
實例來遞歸獲取該文件路徑下的所有文件,篩選出其中以.class
爲結尾的類編譯文件交給addIfMatching
方法完成後續的判斷處理操作。
/**
* 如果指定的類名對應的類滿足指定的條件,則將其添加到{@link #matches}中。
*
* @param test 用於條件判斷的測試類
* @param fqn 類的全限定名稱
*/
@SuppressWarnings("unchecked")
protected void addIfMatching(Test test, String fqn) {
try {
// 將地址名稱轉換爲類的全限定名稱格式,並去掉後綴(.class)
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());
}
}
在addIfMatching
方法中,首先將文件地址名稱轉換爲類的全限定名稱格式,並移除結尾的.class
後綴,之後利用當前配置的ClassLoader
加載該文件對應的類編譯文件得到具體的JAVA類型定義。
最後調用傳入的Test
實現類的matches
方法,校驗獲取到的類是否有效,進而決定是否保存至matches
集合中。
在ResolverUtil
中還爲Test
接口提供了兩個默認實現:
- 一個用於校驗某個類是否是指定類的子類或者實現類
/**
* 校驗某個類是否是指定類的子類或者實現類
*/
public static class IsA implements Test {
/**
* 父類或者接口
*/
private Class<!--?--> parent;
/**
* 構造
*/
public IsA(Class<!--?--> parentType) {
this.parent = parentType;
}
/**
* 判斷某個類是否指定類的子類或者實現類
*/
@Override
public boolean matches(Class<!--?--> type) {
return type != null && parent.isAssignableFrom(type);
}
@Override
public String toString() {
return "is assignable to " + parent.getSimpleName();
}
}
- 一個用於檢查指定的類上是否標註了指定註解
/**
* 用於檢查指定的類上是否標註了指定註解的測試類
*/
public static class AnnotatedWith implements Test {
/**
* 用於校驗的註解類
*/
private Class<!--? extends Annotation--> annotation;
/**
* 構造
*/
public AnnotatedWith(Class<!--? extends Annotation--> annotation) {
this.annotation = annotation;
}
/**
* 判斷指定類上是否標註了指定的註解
*/
@Override
public boolean matches(Class<!--?--> type) {
return type != null && type.isAnnotationPresent(annotation);
}
@Override
public String toString() {
return "annotated with @" + annotation.getSimpleName();
}
}
而且針對這兩Test
實現類,ResolverUtil
還單獨對外提供了相關的find
方法的包裝實現:
- 獲取指定包集合下,所有指定類/接口的子類/實現類
/**
* 獲取指定包集合下,所有指定類/接口的子類/實現類。
*
* @param parent 用於查找子類或者實現類的類定義/接口定義
* @param packageNames 用於查找類的一個或多個包名
*/
public ResolverUtil<t> findImplementations(Class<!--?--> parent, String... packageNames) {
if (packageNames == null) {
return this;
}
// 判斷是否是指定類型的子類或者實現類
Test test = new IsA(parent);
for (String pkg : packageNames) {
// 挨個處理包
find(test, pkg);
}
return this;
}
- 獲取指定包集合下所有標註了指定註解的類集合
/**
* 獲取指定包集合下所有標註了指定註解的類集合
*
* @param annotation 應被標註的註解
* @param packageNames 一個或多個包名
*/
public ResolverUtil<t> findAnnotated(Class<!--? extends Annotation--> annotation, String... packageNames) {
if (packageNames == null) {
return this;
}
// 判斷是否有指定註解
Test test = new AnnotatedWith(annotation);
for (String pkg : packageNames) {
find(test, pkg);
}
return this;
}
到這,typeAliases
元素的解析工作也已經完成了。