mybatis中类型别名(typeAliases)的源码解析

前言

说到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源码的后续讲解。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值