2.6.解析 typeAliases 元素 完成类型别名的注册工作

优质
小牛编辑
120浏览
2023-12-01

点击查看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>
 * 如果指定了{@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<?>> 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元素的解析工作也已经完成了。