2-1 类型别名功能源码分析
上一节中我们知道,Mybatis通过XMLConfigBuilder将XML文件解析成为一个Configuration类型的对象。Mybatis提供了大量的XXXBuilder来进行XML配置文件的解析,他们都是BaseBuilder的子类。BaseBuilder提供了如下功能:
- 基础工具功能的实现
- 类型别名配置解析 以及 类型解析器解析
- 核心配置类存储
由于这部分功能是各个解析器都需要的功能,所以Mybatis有了这样的设计。本节我们将对与BaseBuilder的第二个功能中的类型别名配置解析功能进行分析,方便读者更好的理解BaseBuilder。
2-1.1 类型别名功能
对于Java来说,在同一个类加载器中,使用一个全限定类名唯一确定一个类,但是全限定类名是很长并且很难记的。Mybatis为了降低冗余的全限定类名书写,提供了类型别名功能。
你可以在XML中使用如下配置:
<typeAliases>
<typeAlias alias="Author" type="domain.blog.Author"/>
</typeAliases>
当这样配置时,Author 可以用在任何使用 domain.blog.Author的地方。然而如果你一个项目中有很多这样的类,进行一一书写确实会比较麻烦,因此,Mybatis支持通过包名扫描Java的类型,你可以这样使用这个特性:
<typeAliases>
<package name="domain.blog"/>
</typeAliases>
每一个在包 domain.blog 中的 Java Bean,在没有注解的情况下,会使用 Bean 的首字母小写的非限定类名来作为它的别名,除非你使用@Alias注解为其注释。
更多使用细节请查看Mybatis文档中对类型别名的介绍。
2-1.2 类型别名功能源码分析
在描述typeAlias功能时,笔者写到了如下一句话:
Author 可以用在`任何使用` domain.blog.Author的地方。
注意这里指出了是任何使用domain.blog.Author的地方,因此类型别名的注册表是各个解析器都需要使用的,因此Mybatis将这个配置的注册表交给了BaseBuilder管理。解析这部分配置的代码在XmlConfigBuilder.parseConfiguration(XNode root):
private void parseConfiguration(XNode root) {
try {
// issue #117 read properties first
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
// 代码点1 类型别名配置解析
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
// 代码点2 类型处理器配置解析
typeHandlerElement(root.evalNode("typeHandlers"));
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
其中代码点1的位置负责类型别名的解析,即XmlConfigBuilder.typeAliasesElement(XNode parent)方法。
private void typeAliasesElement(XNode parent) {
if (parent != null) {
for (XNode child : parent.getChildren()) {
// 代码点3 处理使用包名的情况
if ("package".equals(child.getName())) {
String typeAliasPackage = child.getStringAttribute("name");
configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
} else {
// 代码点4 处理通常的别名的情况
String alias = child.getStringAttribute("alias");
String type = child.getStringAttribute("type");
try {
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);
}
}
}
}
}
其中代码点3处用于处理基于package设置别名的情况,代码点4处用于处理使用全限定类名设置类型别名的情况,这里我们对代码点3、4的代码一一分析:
代码点3,指定包名,让mybatis自动扫描类型别名的处理逻辑:
// 解析package名称
String typeAliasPackage = child.getStringAttribute("name");
// 通过包名注册一组类型别名
configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
代码点4,处理一般使用key-value形式的配置指定类型别名的处理逻辑:
// 代码点4 处理通常的别名的情况
// 获取typeAlias标签的alias属性
String alias = child.getStringAttribute("alias");
// 获取typeAlias标签的type属性
String type = child.getStringAttribute("type");
try {
//根据type属性获取对应的class对象
Class<?> clazz = Resources.classForName(type);
// 注册class对象到类型别名注册表(typeAliasRegistry)中
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);
}
}
可以看到对于上述两处代码逻辑相同,都是:
- 解析对应XML配置
- 交给TypeAliasRegistry进行类型别名注册:
- 注册整个包中的类时通过
registerAliases(String packageName)方法处理。 - 通过key-value形式注册类型别名通过
registerAlias(String alias, Class<?> value)处理。 - 不提供key,只提供value时,通过
registerAlias(Class<?> type)处理。
- 注册整个包中的类时通过
所以接下来,我们对TypeAliasRegistry的源码进行分析,看一下具体的类型别名是如何存储的。打开TypeAliasRegistry的源码可以看到,它只有一个属性:
public class TypeAliasRegistry {
private final Map<String, Class<?>> typeAliases = new HashMap<>();
}
所以类型别名注册表实际上只是一个简单的HashMap。在它的构造器中,默认注册了许多的类型别名,例如:
registerAlias("string", String.class);
registerAlias("byte", Byte.class);
registerAlias("long", Long.class);
...
registerAlias("byte[]", Byte[].class);
registerAlias("long[]", Long[].class);
...
registerAlias("_byte", byte.class);
registerAlias("_long", long.class);
...
registerAlias("date", Date.class);
registerAlias("decimal", BigDecimal.class);
registerAlias("bigdecimal", BigDecimal.class);
registerAlias("biginteger", BigInteger.class);
registerAlias("object", Object.class);
...
registerAlias("map", Map.class);
registerAlias("hashmap", HashMap.class);
...
registerAlias("ResultSet", ResultSet.class);
这里注册了如下几种类型:
- 基本类型包装类及其数组
- 基本类型及其数组
- 常用时间类型、大数值类型及其数组
- 基本集合类型
- ResultSet
关于更多细节,大家可以去参考源码(TypeAliasRegistry类构造器)。
由于TypeAliasRegistry是一个注册表,因此,我们会分析它的两类方法:
- 注册方法
- 通过key-value形式注册类型别名通过
registerAlias(String alias, Class<?> value)处理。 - 不提供key,只提供value时,通过
registerAlias(Class<?> type)处理 - 注册整个包中的类时通过
registerAliases(String packageName)方法处理。
- 通过key-value形式注册类型别名通过
- 解析方法
- resolveAlias(String string)
2-1.2.1 注册方法解析
在TypeAliasRegistry的三个注册方法中,第一个方法,即registerAlias(String alias, Class<?> value)逻辑最简单,而且,后面两种方法都是通过该方法实现的,所以优先分析该方法:
public void registerAlias(String alias, Class<?> value) {
if (alias == null) {
throw new TypeException("The parameter alias cannot be null");
}
// issue #748
String key = alias.toLowerCase(Locale.ENGLISH);
if (typeAliases.containsKey(key) && typeAliases.get(key) != null && !typeAliases.get(key).equals(value)) {
throw new TypeException("The alias '" + alias + "' is already mapped to the value '" + typeAliases.get(key).getName() + "'.");
}
typeAliases.put(key, value);
}
可以看到,该方法的实现逻辑极其简单:
- 将类型别名(alias)转为小写字符
- 进行存在性检验
- 如果不存在放入HashMap中
- 如果存在比较新旧数据差异,如果存在差异则抛出异常。
可以看到类型别名注册表的核心注册逻辑相对比较简单,其实方法2,即registerAlias(Class<?> 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);
}
可以看到方法2其实并不是用于真正注册别名的,而是为类型获取真正的别名属性,然后交给方法1进行注册,获取别名的逻辑如下:
- 如果Class被Alias注解修饰,那么就获取Alias注解中的value属性中的值作为别名
- 如果上述方法获取不到别名,那么则使用类的短类名作为别名。
直接注册Class对象的方法我们已经分析完了,那么接下来可以看一下通过包名进行注册的情况,即registerAliases(String packageName)方法:
public void registerAliases(String packageName) {
registerAliases(packageName, Object.class);
}
// 方法4 注册指定包中,父类为superType的类
public void registerAliases(String packageName, Class<?> 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);
}
}
}
这里引入了一个新方法,即方法4:(registerAliases(String packageName, Class<?> superType)),该方法负责将指定包中,超类为superType的所有子类注册到类型别名注册表中,真正用于注册的方法仍然是方法2,所以这里我们更关心方法4是如何找到需要注册的类的,注意查找到的结果集中的接口、匿名类、内部类是会被跳过的。
可以看到,方法4是通过如下代码找到要注册的类的:
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
Set<Class<? extends Class<?>>> typeSet = resolverUtil.getClasses();
这种包扫描的功能其实经常出现,例如Spring的包扫描,这里我们看一下Mybatis是如何实现这个功能的。
- 创建一个ResolverUtil
- 通过ResolverUtil提供的isA条件,筛选出所有超类是superType的类
- 返回查找的结果。
笔者已经提过,Mybatis中很多工具都将处理结果放到工具类中,这里的ResolverUtil也一样,查看其源码,发现该类有如下属性:
public class ResolverUtil<T> {
// 匹配结果
private Set<Class<? extends T>> matches = new HashSet<>();
// 类加载器
private ClassLoader classloader;
}
接下来我们主要分析ResolverUtil.find(Test test, String packageName)方法,该方法负责完成具体的查找工作:
public ResolverUtil<T> find(Test test, String packageName) {
// 获取包名对应的文件路径
String path = getPackagePath(packageName);
try {
// 通过VFS(虚拟文件系统)列出包文件路径下的所有文件名
List<String> children = VFS.getInstance().list(path);
for (String child : children) {
// 如果文件名以.class结尾,并且符合查询条件
// 则将class对象放入到matches结果集中
if (child.endsWith(".class")) {
addIfMatching(test, child);
}
}
} catch (IOException ioe) {
log.error("Could not read package: " + packageName, ioe);
}
return this;
}
protected void addIfMatching(Test test, String fqn) {
try {
// 获取全限定类名
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对象
Class<?> type = loader.loadClass(externalName);
// 判断class对象是否符合条件
// 如果符合,则加入到matches结果集中
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());
}
}
可以看到,包扫描功能的实现并不是很复杂,仅仅通过如下几个步骤:
- 包名转文件路径
- 类加载器加载class对象
- 通过class对象+反射判断是否满足条件
- 返回匹配结果
至此,Mybatis的类型别名注册功能已经分析完毕了,接下来是解析功能。
2-1.2.2 解析功能
TypeAliasRegistry中使用resolveAlias(String string)方法进行解析,通过别名换取对应的class类型对象。解析逻辑相对比较简单,源码如下:
public <T> Class<T> resolveAlias(String string) {
try {
if (string == null) {
return null;
}
// issue #748
String key = string.toLowerCase(Locale.ENGLISH);
Class<T> value;
if (typeAliases.containsKey(key)) {
value = (Class<T>) typeAliases.get(key);
} else {
value = (Class<T>) Resources.classForName(string);
}
return value;
} catch (ClassNotFoundException e) {
throw new TypeException("Could not resolve type alias '" + string + "'. Cause: " + e, e);
}
}
类型别名注册表的解析逻辑相对简单:
- 将别名转换为小写
- 如果注册表中包含该key,直接返回
- 否则通过类加载器加载对应的Class对象,然后返回。
因此,无论是否配置了别名,都可以通过类型别名注册表获取对应的class对象。
至此,类型别名功能就全部分析完毕了。
注意
之前笔者提到过,Mybatis的所有配置都存在一个巨大的核心配置类Configuration中,那么这个Configuration中是否保存着类型别名注册表呢?事实上,确实存储了。在Configuration中有如下一条属性:
public class Configuration {
...
protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();
...
}
但XmlConfigBuilder使用时,使用的是BaseBuilder中的typeAliasRegistry,那么这两者是怎么同步的呢?事实上,这两部分数据是一部分,不需要同步,考察BaseBuilder的构造器:
public BaseBuilder(Configuration configuration) {
this.configuration = configuration;
this.typeAliasRegistry = this.configuration.getTypeAliasRegistry();
this.typeHandlerRegistry = this.configuration.getTypeHandlerRegistry();
}
可以看到BaseBuilder中的别名类型注册表与Configuration中的是同一个引用。在后面介绍更多的XXXBuilder时,你会看到很多这样的情况。
总结
本篇文章主要对Mybatis的类型别名功能进行介绍,主要介绍了如下几点:
- 类型别名功能的基本使用
- 类型别名功能配置文件的解析过程
- Mybatis包扫描功能的具体逻辑
- BaseBuilder与Configuration中相同属性之间的关系
TypeAliasRegistry是Mybatis中很多位置都会用到的注册表,例如解析Mapper时,理解了这部分可以让读者阅读之后的配置解析源码分析不是那么困惑。