MyBatis 学习笔记(四)---源码分析篇---配置文件的解析过程(二)
概述
接上一篇MyBatis 学习笔记(四)—源码分析篇—配置文件的解析过程(一) 。上一篇我们介绍了properties 和settings配置的解析过程,今天我们接着来看看其他常用属性的解析过程,重点介绍typeAliases,environments等配置的解析。
typeAliases的解析过程
一个简单的别名配置如下:
<typeAliases>
<typeAlias type="com.jay.chapter2.entity.ClassRoom" alias="ClassRoom"/>
<typeAlias type="com.jay.chapter2.entity.Student" alias="Student"/>
</typeAliases>
//or
<typeAliases>
<package name="domain.blog"/>
</typeAliases>
如上,typeAliases配置的使用也比较简单,该配置主要是减少在映射文件中填写全限定名的冗余。下面我们来看看解析过程
//* XMLConfigBuilder
private void typeAliasesElement(XNode parent) {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
//如果是package
String typeAliasPackage = child.getStringAttribute("name");
//(一)调用TypeAliasRegistry.registerAliases,去包下找所有类,然后注册别名(有@Alias注解则用,没有则取类的simpleName)
configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
} else {
//如果是typeAlias
String alias = child.getStringAttribute("alias");
String type = child.getStringAttribute("type");
try {
Class<?> clazz = Resources.classForName(type);
//根据Class名字来注册类型别名
//(二)调用TypeAliasRegistry.registerAlias
if (alias == null) {
//alias可以省略
typeAliasRegistry.registerAlias(clazz);
} else {
typeAliasRegistry.registerAlias(alias, clazz);
}
} catch (ClassNotFoundException e) {
throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e);
}
}
}
}
}
如上,该入口程序方法执行流程如下:
- 根据节点名称判断是否是package,如果是的话则调用TypeAliasRegistry.registerAliases,去包下找所有类,然后注册别名(有@Alias注解则用,没有则取类的simpleName)
- 如果不是的话,则进入另外一个分支,则根据Class名字来注册类型别名。
接下来我们按照两个分支进行分析。
package解析分支
按照前面说的如果配置的是package的话,那么首先去包下找所有的类,然后注册别名。
那么它是如何找到包下的所有类的呢?带着疑问我们来看看源码。
//* TypeAliasRegistry
public void registerAliases(String packageName){
registerAliases(packageName, Object.class);
}
public void registerAliases(String packageName, Class<?> superType){
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
//扫描并注册包下所有继承于superType的类型别名
resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
Set<Class<? extends Class<?>>> typeSet = resolverUtil.getClasses();
for(Class<?> type : typeSet){
//
if (!type.isAnonymousClass() && !type.isInterface() && !type.isMemberClass()) {
registerAlias(type);
}
}
}
//注册类型别名
public void registerAlias(Class<?> type) {
//如果没有类型别名,用Class.getSimpleName来注册
String alias = type.getSimpleName();
//或者通过Alias注解来注册(Class.getAnnotation)
Alias aliasAnnotation = type.getAnnotation(Alias.class);
if (aliasAnnotation != null) {
alias = aliasAnnotation.value();
}
registerAlias(alias, type);
}
如上,流程主要有两部分
- 通过ResolverUtil的find方法找到该包下所有的类,传入的父类是Object
- 循环注册别名,只有非匿名类及非接口及内部类及非成员类才能注册。
- 注册别名最终还是调用registerAlias(alias, type)完成的。
接着我们再来看看ResolverUtil到底是如何查找包下的所有类的。
//主要的方法,找一个package下满足条件的所有类,被TypeHanderRegistry,MapperRegistry,TypeAliasRegistry调用
public ResolverUtil<T> find(Test test, String packageName) {
String path = getPackagePath(packageName);
try {
//通过VFS来深入jar包里面去找一个class
List<String> children = VFS.getInstance().list(path);
for (String child : children) {
if (child.endsWith(".class")) {
//将.class的class对象放入Set集合中,供后面调用
addIfMatching(test, child);
}
}
} catch (IOException ioe) {
log.error("Could not read package: " + packageName, ioe);
}
return this;
}
如上,核心是通过VFS来找到packageName下面的子包,包下面的class以及子包下面的class。PS: VFS 是虚拟文件系统,用来读取服务器里的资源。在此处我们不做分析。
根据Class名字注册解析分支
//注册类型别名
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);
//如果已经存在key了,且value和之前不一致,报错
//这里逻辑略显复杂,感觉没必要,一个key对一个value呗,存在key直接报错不就得了(与系统内置的类型别名相同的别名直接报错)
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() + "'.");
}
TYPE_ALIASES.put(key, value);
}
public <T> Class<T> resolveAlias(String string) {
try {
if (string == null) {
return null;
}
//这里转个小写也有bug?见748号bug(在google code上) https://code.google.com/p/mybatis/issues
//比如如果本地语言是Turkish,那i转成大写就不是I了,而是另外一个字符(İ)。这样土耳其的机器就用不了mybatis了!这是一个很大的bug,但是基本上每个人都会犯......
String key = string.toLowerCase(Locale.ENGLISH);
Class<T> value;
//原理就很简单了,从HashMap里找对应的键值,找到则返回类型别名对应的Class
if (TYPE_ALIASES.containsKey(key)) {
value = (Class<T>) TYPE_ALIASES.get(key);
} else {
//找不到,再试着将String直接转成Class(这样怪不得我们也可以直接用java.lang.Integer的方式定义,也可以就int这么定义)
value = (Class<T>) Resources.classForName(string);
}
return value;
} catch (ClassNotFoundException e) {
throw new TypeException("Could not resolve type alias '" + string + "'. Cause: " + e, e);
}
}
至此,我们的类型别名就注册和解析就全部完成了。
environments的解析过程
我们都知道MyBatis中environments配置主要是用来配置数据源信息,是MyBatis中一定会有的配置。首先我们还是来看看environments配置的使用。
<!-- 设置一个默认的连接环境信息 -->
<environments default="development">
<!--连接环境信息,取一个任意唯一的名字 -->
<environment id="development">
<!-- mybatis使用jdbc事务管理方式 -->
<transactionManager type="JDBC">
<property name="..." value="..."/>
</transactionManager>
<!-- mybatis使用连接池方式来获取连接 -->
<dataSource type="POOLED">
<!-- 配置与数据库交互的4个必要属性 -->
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
如上,配置了连接环境信息,我们心中肯定会有个疑问,${} 这种参数是如何解析的?我一会再分析。
下面我们就来看看这个配置的解析过程。
private void environmentsElement(XNode context) throws Exception {
if (context != null) {
if (environment == null) {
environment = context.getStringAttribute("default");
}
for (XNode child : context.getChildren()) {
String id = child.getStringAttribute("id");
//循环比较id是否就是指定的environment
if (isSpecifiedEnvironment(id)) {
//1 创建事务工厂TransactionFactory
TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
//2创建数据源
DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
DataSource dataSource = dsFactory.getDataSource();
//3.构建Environment对象
Environment.Builder environmentBuilder = new Environment.Builder(id)
.transactionFactory(txFactory)
.dataSource(dataSource);
// 将创建的Environment对象设置到configuration中
configuration.setEnvironment(environmentBuilder.build());
}
}
}
}
如上,解析environments 的流程有三个:
- 创建事务工厂TransactionFactory
- 创建数据源
- 创建Environment对象
我们看看第一步和第二步的代码
//* XMLConfigBuilder
private TransactionFactory transactionManagerElement(XNode context) throws Exception {
if (context != null) {
String type = context.getStringAttribute("type");
Properties props = context.getChildrenAsProperties();
//根据type="JDBC"解析返回适当的TransactionFactory
TransactionFactory factory = (TransactionFactory) resolveClass(type).newInstance();
factory.setProperties(props);
return factory;
}
throw new BuilderException("Environment declaration requires a TransactionFactory.");
}
protected Class<?> resolveClass(String alias) {
if (alias == null) {
return null;
}
try {
return resolveAlias(alias);
} catch (Exception e) {
throw new BuilderException("Error resolving class. Cause: " + e, e);
}
}
//*Configuration
typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
JDBC 通过别名解析器解析之后会JdbcTransactionFactory工厂实例。数据源的解析与此类似最终得到的是PooledDataSourceFactory工厂实例。
下面我们来看看之前说过的类似${driver}的解析。其实是通过PropertyParser的parse来处理的。下面我们来看个时序图。
这里最核心的就是第五步,我们来看看源码
public static String parse(String string, Properties variables) {
VariableTokenHandler handler = new VariableTokenHandler(variables);
GenericTokenParser parser = new GenericTokenParser("${", "}", handler);
return parser.parse(string);
}
//就是一个map,用相应的value替换key
private static class VariableTokenHandler implements TokenHandler {
private Properties variables;
public VariableTokenHandler(Properties variables) {
this.variables = variables;
}
@Override
public String handleToken(String content) {
if (variables != null && variables.containsKey(content)) {
return variables.getProperty(content);
}
return "${" + content + "}";
}
}
如上,在VariableTokenHandler 会将${driver} 作为key,其需要被替换的值作为value。传入GenericTokenParser中。然后通过GenericTokenParser 类的parse进行替换。
至此,我们environments配置就解析完了。
总结
本文主要介绍typeAliases和environments配置的解析。然后还说下${driver} 这种属性的处理。希望对读者朋友们有所帮助。
源代码
https://github.com/XWxiaowei/mybatis
作者:码农飞哥
微信公众号:码农飞哥