解析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>
* 如果指定了{@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
元素的解析工作也已经完成了。
网友评论