Mybatis源碼之美:2.6.解析typeAliases元素,完成類型別名的註冊工作

解析typeAliases元素,完成類型別名的註冊工作

> 點擊查看typeAliases元素的用法

typeAliases元素在mybatis中用於完成類型別名映射的配置工作,關於mybatis的類型別名機制,我們在前面已經稍作了解,他的作用就是爲指定的JAVA類型提供一個較短的名字,從而簡化我們使用完全限定名帶來的冗餘,是簡化我們使用Mybatis時的代碼量的一個優化性操作。

在Mybatis中配置自定義別名,需要使用的元素是typeAliasestypealiases的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節點,同時typeAliaspackage均不允許再包含其他子節點。

其中:

  • typeAlias節點用於註冊單個別名映射關係,他有兩個可填參數,type參數指向一個java類型的全限定名稱,爲必填項,alias參數表示該java對象的別名,非必填,默認是使用java類的Class#getSimpleName()方法獲取的.
  • package通常用於批量註冊別名映射關係,他只有一個必填的參數name,該參數指向一個java包名,包下的所有符合規則(默認是Object.class的子類)的類均會被註冊。

XmlConfigBuildertypeAliasesElement方法對這兩種節點的解析工作也比較簡單:

/**
 * 解析配置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節點的aliastype參數的值,並通過反射將type轉換爲實際的java類型,然後將別名註冊的操作轉交給typeAliasRegistry對象來完成,

如果用戶指定了alias參數的值,那就調用TypeAliasRegistryresolveAlias(String,Class)方法來完成別名註冊,該方法我們前面已經瞭解過了。

如果用戶沒有指定alias參數的值,註冊別名的工作就交給TypeAliasRegistryresolveAlias(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節點是如何解析的。

XmlConfigBuilderpackage的解析工作,在得到packagename參數值之後,就完全交給了TypeAliasRegistryregisterAliases(String)方法來完成後續的流程。

// 根據 package 來批量解析別名,別名默認取值爲實體類的SimpleName
String typeAliasPackage = child.getStringAttribute("name");
// 註冊別名映射關係到別名註冊表
configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);

TypeAliasRegistryregisterAliases(String)方法中,又直接將工作轉交給了registerAliases(String,Class)方法來完成:

/**
 * 註冊指定包下指定類型及其子實現的別名映射關係
 *
 * @param packageName 指定包名稱
 * @param superType   指定類型
 */
public void registerAliases(String packageName, Class<!--?--> superType) {
    // 獲取指定包下所有superType的子類或者實現類
    ResolverUtil<class<?>&gt; resolverUtil = new ResolverUtil&lt;&gt;();
    resolverUtil.find(new ResolverUtil.IsA(superType), packageName);

    // 返回當前已經找到的類
    Set<class<? extends class<?>&gt;&gt; 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() &amp;&amp; !type.isInterface() &amp;&amp; !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>&gt;類型的matches屬性負責存放所有滿足條件的Class集合,ResolverUtil對外暴露了他的getter方法:

/**
 * 滿足條件的類型集合
 */
private Set<class<? extends t>&gt; matches = new HashSet&lt;&gt;();

/**
 * 獲取所有匹配條件的類型集合
 *
 * @return 所有匹配條件的類型集合
 */
public Set<class<? extends t>&gt; 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 &amp;&amp; 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 &amp;&amp; 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元素的解析工作也已經完成了。

關注我,一起學習更多知識

關注我

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章