Mybatis源码之美:2.6.解析typeAliases元素,完成类型别名的注册工作

原创
2020/06/27 10:21
阅读数 69

解析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元素的解析工作也已经完成了。

关注我,一起学习更多知识

关注我

展开阅读全文
打赏
0
0 收藏
分享
加载中
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部