前言
mybatis在处理parameterType(同resultType)属性时,通过设置@Alias注解或配置<typeAliases>标签后,就可以为类型别名和Java类型增加一种映射关系,从而在parameterType输入类型别名时,能获取到该Java类型。
那么,在我们没有设置如上两种配置,直接输入类名时,为什么也有可能通过这个类型别名获取到Java类型呢?
本文由该问题引出。
思路:
1. 大家熟知的设置类型别名的方式
2. parameterType将在何时使用类型别名
3. 这些类型别名又是如何添加映射关系的
4. parameterType使用类型别名的整个流程
类型别名的设置方式
下面是官方文档对类型别名的一些描述:
可以看到官方的文档中,主要由=@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>标签。
这种豁然开朗的感觉真八错,有时候看源码真是一种享受,比看自己和同事的垃圾代码好太多了。