前言
说到mybatis,不得不说的是它的类型别名机制,mybatis本身使用了很多类型别名(typeAliases),开发者在实际开发中,也会自己定义别名,例如一般会给“实体类”配置类型别名,通过这些类型别名,我们将复杂冗长的类全限定名使用简单的名称来表示。在mapper映射文件中,我们可以用"string"代替"java.lang.String",例如我们一个"idin.sun.study.model.Student"可以用"student"来代替,是不是方便了很多。但是如果我们配置不当,就会导致异常:org.apache.ibatis.builder.BuilderException: Error parsing SQL Mapper Configuration. Cause: org.apache.ibatis.type.TypeException: The alias 'Student' is already mapped to the value 'idin.sun.study.model.Student'。下文将通过分析typeAliases的相关源码,来解释该异常。
源码分析
先看与别名类型相关的类:TypeAliasRegistry
/// <TypeAliasRegistry>类
public class TypeAliasRegistry {
// map集合用于存储别名的映射,key为别名,value为对应的类型的Class对象
// 此变量为final类型,防止外部使用时更改该对象的引用
private final Map<String, Class<?>> TYPE_ALIASES = new HashMap<String, Class<?>>();
// 构造函数,在构造函数中,完成了String、基本包装类型、
// 基本数组包装类型、基本类型、基本数组类型、
// 日期数字型、集合型以及ResultSet类型的别名注册
public TypeAliasRegistry() {
registerAlias("string", String.class);
// 篇幅缘故省略其他,读者可以自行查看源码
registerAlias("byte", Byte.class);
}
// 根据别名获得相应类型
public <T> Class<T> resolveAlias(String string) {
try {
if (string == null) {
return null;
}
// 将key转成小写,原因是在注册类别名时将key转成了小写存储的
// 见下文注册别名的代码。
// 此处还可以得知,类型别名是不区分大小写的,
// 不能通过大小写来区分别名!!!
String key = string.toLowerCase(Locale.ENGLISH);
Class<T> value;
// 从"TYPE_ALIASES"里查找对应的键值是否存在
// 存在时根据键值("类型别名")获得类型别名对应类的Class对象
if (TYPE_ALIASES.containsKey(key)) {
// map.get(key)
value = (Class<T>) TYPE_ALIASES.get(key);
} else {
// 如果“TYPE_ALIASES”中取不到相应类型时,
// 尝试将参数"string"作为类名,加载到JVM
value = (Class<T>) Resources.classForName(string);
}
return value;
} catch (ClassNotFoundException e) {
throw new TypeException("Could not resolve type alias '" + string + "'. Cause: " + e, e);
}
}
// 根据包名注册包下所有类的类型别名
public void registerAliases(String packageName){
registerAliases(packageName, Object.class);
}
// registerAliases(String packageName)的具体实现方法,
// 扫描并注册包下所有继承自superType类的类型别名
public void registerAliases(String packageName, Class<?> superType){
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
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
//排除包下内部类、接口和package-info.java
if (!type.isAnonymousClass() && !type.isInterface() && !type.isMemberClass()) {
registerAlias(type);
}
}
}
// 在没给别名的情况下,注册类型别名
public void registerAlias(Class<?> type) {
// 获得Class对象的简单类名称作为key(即默认别名)
String alias = type.getSimpleName();
// 因为mybatis支持注解方式(@Alias("xxx"))的别名设置方式,
// 所以在配置文件找不到别名时,有可能是通过注解设置的,
// 故这里需要获得类是否有@Alias注解,有的话获取其中的值作为key(即类型别名)
// 同时发现注解方式离不开<typeAliases>配置
Alias aliasAnnotation = type.getAnnotation(Alias.class);
if (aliasAnnotation != null) {
alias = aliasAnnotation.value();
}
// 调用registerAlias(String,Class<?>)方法
registerAlias(alias, type);
}
// 注册别名的最终操作
public void registerAlias(String alias, Class<?> value) {
// 别名不允许为 null
if (alias == null) {
throw new TypeException("The parameter alias cannot be null");
}
// issue #748
// 将别名转成小写字母与上文中根据别名获得类型相对应。
String key = alias.toLowerCase(Locale.ENGLISH);
// 注册类型别名时排除:1、别名是否已经被使用,
// 2.别名对应的类型为null的,3、已经注册过的
if (TYPE_ALIASES.containsKey(key) &&
TYPE_ALIASES.get(key) != null &&
!TYPE_ALIASES.get(key).equals(value)) {
throw new TypeException("The alias '" + alias + "' is already mapped to the value '"
+ TYPE_ALIASES.get(key).getName() + "'.");
}
// Map#put(key,value),将类型别名和类型保存到TYPE_ALIASES(Map集合)中。
TYPE_ALIASES.put(key, value);
}
// 这种注册方式:先见value代表的类加载到jvm然后
// 再调用registerAlias(String,Class<?>)方法注册别名
public void registerAlias(String alias, String value) {
try {
// 调用registerAlias(String,Class<?>)方法
registerAlias(alias, Resources.classForName(value));
} catch (ClassNotFoundException e) {
throw new TypeException("Error registering type alias "+alias+" for "+value+". Cause: "
+ e, e);
}
}
/**
* @since 3.2.2
*/
// 获得别名集合,安全考虑,该集合不允许修改
public Map<String, Class<?>> getTypeAliases() {
return Collections.unmodifiableMap(TYPE_ALIASES);
}
}
TypeAliasRegistry的源码比较简单,作者通过源码注释的方式,把源码给读者梳理了一番,对类型别名的操作,无非两种:
一个是根据类型别名获取实际类的Class对象,另一个是注册别名,实际的操作时对底层Map集合的get/put操作。
在根据类型别名获取实际类的Class对象时,根据给定的类型别名去TYPE_ALIASES中去对应Class对象,存在则返回对应的值,不存在时,尝试把这个类型别名当做类名,进行加载。
注册别名时,比较复杂,我们分情况讨论一下:
1.在mybatis文件配置中使用 <typeAlia>
<typeAliases>
<!-- alias属性可以不设置,通过源码分析我们知道,当不设置时,使用类的简单名称作为别名 -->
<typeAlias type="idin.sun.study.model.Student" alias="student"/>
<!-- 该方式下,默认Book类的类型别名是 book -->
<typeAlias type="idin.sun.study.model.Book"/>
</typeAliases>
2.在mybatis文件配置中使用<package>,且类中不使用@Alias注解时
<typeAliases>
<!-- 将该包下所有符合条件的类,依次进行别名注册,采用默认别名(类的简单名) -->
<package name="idin.sun.study.model"/>
</typeAliases>
3.使用@Alias注解,需要说明一下,单独配置注解时别名是不生效的,注解方式依赖于上述两种方式,且注解方式设置的别名优先级低于<typeAlias >中设置的"alias"中的别名,高于默认别名。原因是当在<typeAlias >指定"alias"时,程序不会进入registerAlias(Class<?>)方法,通过上文源码分析,可知注解方式是在此方法中解析的,同时在registerAlias(Class<?>)方法中,先是将类型别名设置为默认别名,再去判断是否存在注解方式,有注解时,则会使用注解中设置的别名替换默认别名。
异常分析
分析完类型别名的解析原理以及配置方式,我们来分析下产生文章开头异常的原因:很明显异常信息是说别名重复映射了,出现这种异常的情况:
-
用户配置别名时,通过大小写来区分别名,导致两个或多个别名使用的是同样的字符串,只是大小写不一样。
-
用户使用<package name="">的方式配置别名,且设置了多个包名称,在不同包下有两个命名一样的类。
-
用户配置了多个相同的别名
对于第一种情况:通过源码,我们知道在TYPE_ALIASES中存储的别名,在存储时,"key"全部被转化成小写字母,所以通过大小写来区分别名时行不通的,所以这种方式是不可行的。
对于第二中情况:在不指定别名的情况下,默认类型别名是采用该类的简单类名,虽然两个命名相同的类在不同的包下,但是获得的简单类名是一样的,故在向TYPE_ALIASE集合中存储时,会存在"key"冲突。可以通过使用注解指定别名来区分这两个类。
对于第三中情况,需要用户细心,避免发生这种低级错误。
总结
分析了mybatis的源码,我们发现即使是用户量巨大的mybatis,也存在设计的"缺陷",我们只有深入mybatis的底层才能发现这些坑。才能避免在开发中采坑。关注作者,第一时间获得mybatis源码的后续讲解。