【Mybatis源码解析】 parameterType类型别名的运用和实现原理(包含<typeAliases>、@Alias)

前言

mybatis在处理parameterType(同resultType)属性时,通过设置@Alias注解或配置<typeAliases>标签后,就可以为类型别名和Java类型增加一种映射关系,从而在parameterType输入类型别名时,能获取到该Java类型。

那么,在我们没有设置如上两种配置,直接输入类名时,为什么也有可能通过这个类型别名获取到Java类型呢?

本文由该问题引出。


思路:

1. 大家熟知的设置类型别名的方式

2. parameterType将在何时使用类型别名

3. 这些类型别名又是如何添加映射关系的

4. parameterType使用类型别名的整个流程


类型别名的设置方式

下面是官方文档对类型别名的一些描述:

 参考:类型别名(typeAliases)

可以看到官方的文档中,主要由=@Alias注解和<typeAliases>来设置别名的,其实还有第三种隐含的设置。

1. 在mybatis-config.xml中设置<typeAliases>标签内容

2. 为mybatis.type-aliases-package包下的类里添加注解@Alias

3. yml中为mybatis.type-aliases-package设置包名后,会默认将该包下的类设置类型别名。

第三种方法文档没提到,源码中实现的,这也是为什么我们在没有设置前两项时,类型别名依旧生效的原因。OK,接下来就是源码的学习过程。


parameterType使用类型别名

在mybatis源码中,xml标签的所有内容会在解析后存放在MappedStatement类中,这个类通过XMLStatementBuilder#parseStatementNode创建,其中关于这两个属性的解析代码如下:

    String parameterType = context.getStringAttribute("parameterType");
    Class<?> parameterTypeClass = resolveClass(parameterType);



    String resultType = context.getStringAttribute("resultType");
    Class<?> resultTypeClass = resolveClass(resultType);

resolveClass则是获取Java类型的关键方法。而上面的方法BaseBuilder#resolveClass实际上是通过TypeAliasRegistry#resolveAlias将类型别名解析为Java类型的。

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);
  }
}

获取Java类型的方式很简单,只有代码提到的两种:

1. 通过类型别名在别名字典中获取。

2. 通过类型的全限定名(例如java.lang.String)在类加载器中获取类型。

第二种方法无需多说,我们只需关注这个预设的类型别名字典就可以,看看它是如何将我们自定义的类注册进去的。


类型别名字典

类型别名字典属于TypeAliasRegistry类,它使用HashMap存储键值对数据。

类型别名字典规范,key:类型别名,默认为类型的简单类名,英文全小写;value:Java类型。

public class TypeAliasRegistry {
private final Map<String, Class<?>> typeAliases = new HashMap<>();
………………
}

typeAliases的真实数据如下:

这里可以通过初始化方式将类型存在别名字典中;也可以在初始化后,通过类型注册的方式将真实类型注册到字典中。

初始化别名字典

TypeAliasRegistry由Configuration创建,且被final修饰,全局共用该类型别名字典。

public class Configuration {
………………
protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();
………………
}

TypeAliasRegistry构造函数中的默认构建的键值对。

除了TypeAliasRegistry构造函数中的键值对外,上下文环境Configuration在初始化时会也会注册一些类型进去:

这些都是这个字典初始化的一部分,属于预设值的一部分。

TypeAliasRegistry注册Java类

在了解如何将用户自定义的类注册前,先看看注册Java类型的实现方法。

注册单个Java类

这里的类型别名默认值是其简单类名的小写(package.MyClass ---> myclass)。

当然这个类型别名可以通过注解@Alias来修改。

public void registerAlias(Class<?> type) {
  String alias = type.getSimpleName();
  Alias aliasAnnotation = type.getAnnotation(Alias.class);
  if (aliasAnnotation != null) {
    alias = aliasAnnotation.value();
  }
  registerAlias(alias, type);
}

传入别名和类型,然后放到类型别名字典中。

public void registerAlias(String alias, Class<?> value) {
………………
  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); // 放进map对象中
}

这里的判断条件可以看出如下两条规则:

1. 可以为同一个Java类注册不同的类型别名。

2. 不能为不同的Java类设置相同的类型别名。


 

注册整个包

TypeAliasRegistry还提供了整包注册的方法,通过获取包下所有Object.class的子类来获取所有类,类型别名为@Alias的值或类名。

public void registerAliases(String packageName) {
  registerAliases(packageName, Object.class);
}
public void registerAliases(String packageName, Class<?> superType) {
  ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
  resolverUtil.find(new ResolverUtil.IsA(superType), packageName); // 找到包下所有Object.class的子类
  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); // 注册单个类
    }
  }
}

目前为止,我们分析了设置类型别名的方式、parameterType获取真实类型的流程,现在就差为自定义Java类注册别名这一中间过程了。这应该是这个流程中最为复杂的部分,因此,特意将其提到了最后分析。

加载类型别名与设置类型别名的方式一一对应,它们最终都将通过这一节的TypeAliasRegistry#registerAlias方法进行类名注册。

1. 加载mybatis-config.xml中<typeAliases>标签内容

2. 加载@Alias注解内容(这个前面的代码已经有涉及)

3. 加载mybatis.type-aliases-package属性的包


类型别名注册的实现过程

<typeAliases>标签的注册

通过这种方式注册,首先得启用mybatis配置文件,添加config-location属性,如下:

mybatis:
#  configuration与config-location同时只能生效一个
#  configuration:
#    # 缓存仅对单个statement起作用
##    local-cache-scope: statement
#    # mybatis会话执行的所有语句共享一个缓存,默认设置
#    local-cache-scope: session
  mapper-locations: classpath*:mapper/**/*.xml
  type-aliases-package: mybatisreading.domain, mybatisreading.do2
  type-handlers-package:
  config-location: classpath:mybatis-config.xml

1. 设置完config-location属性后,由SqlSessionFactoryBean#buildSqlSessionFactory执行

Spring通过SqlSessionFactoryBean工厂创建mybatis-config.xml的解析类XMLConfigBuilder,该类会初始化一个上下文环境Configuration。与其他集成mybatis框架的加载过程可能有所区别,请注意区分。

有关SqlSessionFactoryBean可以参考:MyBatis-Spring

XMLConfigBuilder xmlConfigBuilder = null;
………………
} else if (this.configLocation != null) { //加载config-location配置
  xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties);
  targetConfiguration = xmlConfigBuilder.getConfiguration();
}
………………

2. 解析mybatis-config.xml的内容

 加载config-location后,开始对文件内容进行解析

if (xmlConfigBuilder != null) {
  try {
    xmlConfigBuilder.parse();
    LOGGER.debug(() -> "Parsed configuration file: '" + this.configLocation + "'");
  } catch (Exception ex) {
    throw new NestedIOException("Failed to parse config resource: " + this.configLocation, ex);
  } finally {
    ErrorContext.instance().reset();
  }
}

3. 解析<configuration>标签

XMLConfigBuilder#parseConfiguration方法将会严格根据标签名,逐个解析mybatis-config.xml的每个标签。我们只需要关注<configuration>标签中的<typeAliases>解析就行。

public Configuration parse() {
  if (parsed) {
    throw new BuilderException("Each XMLConfigBuilder can only be used once.");
  }
  parsed = true;
  parseConfiguration(parser.evalNode("/configuration"));
  return configuration;
}
private void parseConfiguration(XNode root) {
………………
    typeAliasesElement(root.evalNode("typeAliases"));
………………
}

4. 获取<typeAliases>标签的两个属性,并其数据注册到类型别名字典中。

由于 <typeAliases>标签可以设置两种子标签,他们的处理方式如下: 

private void typeAliasesElement(XNode parent) {
  if (parent != null) {
    for (XNode child : parent.getChildren()) {
      if ("package".equals(child.getName())) { // 处理package子标签
        String typeAliasPackage = child.getStringAttribute("name");
        configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
      } else { // 处理<typeAlias>子标签
        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);
          }
………………
}

1. 通过添加子标签<typeAlias>方式设置别名时,只需要将<typeAlias>标签的内容注册进别名字典。

<typeAliases>
    <typeAlias type="mybatisreading.domain.Course" alias="alisaForCourse" />
</typeAliases>

2. 通过添加子标签<package>方式设置,则是直接进行整包注册。

<typeAliases>
    <package name="mybatisreading.domain" />
</typeAliases>

通过@Alias注解注册

这种方法主要用在通过类型设置类型别名的方法中,前文已经多次提到过了。

唯一需要注意的是,如果这个类不在mybatis.type-aliases-package配置的包中或不在<typeAliases>两个子标签中时,那么只设置@Alias注解是不会生效的。

public void registerAlias(Class<?> type) {
  String alias = type.getSimpleName();
  Alias aliasAnnotation = type.getAnnotation(Alias.class);
  if (aliasAnnotation != null) {
    alias = aliasAnnotation.value();
  }
  registerAlias(alias, type);
}

 @Alias注解的运用

在入参的类上添加@Alias注解,那么parameterType属性就可以通过该别名成功识别到该类型了。

mybatis.type-aliases-package包下的默认注册

 mybatis.type-aliases-package这个配置由于是必须设置的,所有很多时候,我们都容易忽略它的功能。

其实它的作用即是:搜索包下的实体类,并生成类型别名。这个配置是允许通配符 “*” 的,用",; \t\n"中的一个进行分隔。

mybatis:
  mapper-locations: classpath*:mapper/**/*.xml
  type-aliases-package: mybatisreading.domain, mybatisreading.do2

流程:通过包名扫描和解析实体类,将所有实体类逐一注册到类型别名字典中。

代码分析如下:

1. 获取typeAliasesPackage属性,也是由SqlSessionFactoryBean#buildSqlSessionFactory执行

扫描包中的所有类,这里解析完成后,会把匿名类、接口、内部类排除在外,并不会对他们进行别名注册。

if (hasLength(this.typeAliasesPackage)) {
  scanClasses(this.typeAliasesPackage, this.typeAliasesSuperType).stream()
      // 过滤匿名类、接口、内部成员类
      .filter(clazz -> !clazz.isAnonymousClass()).filter(clazz -> !clazz.isInterface())
      .filter(clazz -> !clazz.isMemberClass())
      // 进行类名注册
      .forEach(targetConfiguration.getTypeAliasRegistry()::registerAlias);
}

2. 类扫描,最终生成Java类集合。

private Set<Class<?>> scanClasses(String packagePatterns, Class<?> assignableType) throws IOException {
  Set<Class<?>> classes = new HashSet<>();
  String[] packagePatternArray = tokenizeToStringArray(packagePatterns,
      ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS); // 获取包数组
  for (String packagePattern : packagePatternArray) {
    // 获取该包下的类
    Resource[] resources = RESOURCE_PATTERN_RESOLVER.getResources(ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX
        + ClassUtils.convertClassNameToResourcePath(packagePattern) + "/**/*.class");
    for (Resource resource : resources) {
      try {
        ClassMetadata classMetadata = METADATA_READER_FACTORY.getMetadataReader(resource).getClassMetadata();
        Class<?> clazz = Resources.classForName(classMetadata.getClassName());
        if (assignableType == null || assignableType.isAssignableFrom(clazz)) {
          classes.add(clazz);
        }
      } catch (Throwable e) {
        LOGGER.warn(() -> "Cannot load the '" + resource + "'. Cause by " + e.toString());
      }
    }
  }
  return classes;
}
String CONFIG_LOCATION_DELIMITERS = ",; \t\n";

3. 最终这些包下的类,都成功注册到上下文环境中的typeAliasRegistry属性中。

forEach(targetConfiguration.getTypeAliasRegistry()::registerAlias)

总结

抽下班的空余时间终于把这篇文章写完了,看完源码才发现,除了不知道parameterType通过类型别名获取Java类型如何实现外,还有好多细节都没注意过。

索性就将这些东西记录下来,在未来有需要的时候查阅。

application.yml中configuration与config-location同时使用报错的原因也找到了;

@Alias的用法原理也明白了;

type-aliases-package包的作用也清楚了;

甚至复习了如何使用<typeAliases>标签。

这种豁然开朗的感觉真八错,有时候看源码真是一种享受,比看自己和同事的垃圾代码好太多了。

  • 5
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值