Mybatis源码阅读(一):Mybatis初始化1.2 —— 解析别名、插件、对象工厂、反射工具箱、环境
分享一波:程序员赚外快-必看的巅峰干货
接上一节 上一节:解析properties和settings
解析typeAliases
typeAliases节点用于配置别名。别名在mapper中使用resultType时会使用到,是对实体类的简写。
别名有两种配置方式
通过package,直接扫描指定包下所有的类,注册别名
通过typeAliase,指定某个类为其注册别名
别名注册代码如下
/**
* 解析typeAliases节点
*
* @param parent
*/
private void typeAliasesElement(XNode parent) {
if (parent != null) {
// 遍历所有子节点
// typeAliases节点有两个子节点,分别是package和typeAlias
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
// 获取name属性,package的name属性指定的是包名
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);
}
} catch (ClassNotFoundException e) {
throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e);
}
}
}
}
}
注册别名
扫包后获取到包下所有的类之后,会为这些类生成别名,并将其注册到Configuration中
/**
* 指定包名,将这个包下所有的类都注册别名
*
* @param packageName
*/
public void registerAliases(String packageName) {
registerAliases(packageName, Object.class);
}
/**
* 为指定包下所有的类注册别名
*
* @param packageName
* @param superType
*/
public void registerAliases(String packageName, Class<?> superType) {
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
// 扫描指定包下所有继承了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);
}
}
}
/**
* 注册指定类的别名
* @param type
*/
public void registerAlias(Class<?> type) {
// 得到类的简写名称,即不带包名的名称
// 因此在mybatis扫描包下,不允许有同样类名的类存在
// 否则在启动时就会报错
String alias = type.getSimpleName();
Alias aliasAnnotation = type.getAnnotation(Alias.class);
if (aliasAnnotation != null) {
// 如果有Alias注解,就以Alias注解指定的别名为准
// 该注解可以用于解决被扫描包下含有相同名称类的问题
alias = aliasAnnotation.value();
}
registerAlias(alias, type);
}
/**
* 注册别名
* @param alias 别名
* @param value 指定的类
*/
public void registerAlias(String alias, Class<?> value) {
if (alias == null) {
throw new TypeException("The parameter alias cannot be null");
}
// 别名转为小写
String key = alias.toLowerCase(Locale.ENGLISH);
// 如果已经有了这个别名,并且这个别名中取到的值不为null,并且取到的值和传进来的类不相同就报错
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中。key是别名,因此别名不可以重复
typeAliases.put(key, value);
}
在注册别名时,会使用到ResolverUtil工具类。该工具类可以根据指定的条件去查找指定包下的类。该类有个内部接口Test,接口中只有一个matches方法,用于根据指定的规则去匹配。Test接口有两个实现。ISA用于检测该类是否继承了指定的类或者接口,而AnnotatedWith则用于检测是否添加了指定的注释,代码比较简单这里就不贴了。这里使用到了find方法,用于匹配指定包下所有继承了superType的类
public ResolverUtil<T> find(Test test, String packageName) {
String path = getPackagePath(packageName);
try {
// 获取指定包下所有的文件名
List<String> children = VFS.getInstance().list(path);
for (String child : children) {
if (child.endsWith(".class")) {
addIfMatching(test, child);
}
}
} catch (IOException ioe) {
log.error("Could not read package: " + packageName, ioe);
}
return this;
}
/**
* 如果匹配成功,就添加到matches中
*
* @param test the test used to determine if the class matches
* @param fqn the fully qualified name of a class
*/
@SuppressWarnings("unchecked")
protected void addIfMatching(Test test, String fqn) {
try {
String externalName = fqn.substring(0, fqn.indexOf('.')).replace('/', '.');
ClassLoader loader = getClassLoader();
if (log.isDebugEnabled()) {
log.debug("Checking to see if class " + externalName + " matches criteria [" + test + "]");
}
Class<?> type = loader.loadClass(externalName);
if (test.matches(type)) {
matches.add((Class<T>) type);
}
} catch (Throwable t) {
log.warn("Could not examine class '" + fqn + "'" + " due to a " +
t.getClass().getName() + " with message: " + t.getMessage());
}
}
解析plugins
mybatis拥有强大的插件机制,可以通过配置mybatis拦截器来统一对sql、参数、返回集等进行处理,该功能广泛运用与分页、创建人等字段赋值、逻辑删除、乐观锁等插件的编写中。mybatis的拦截器编写难度比spring mvc高得多,想要熟练地编写mybatis拦截器,需要对源码比较熟悉。
解析拦截器的代码比较简单,plugin节点需要配置一个interceptor属性,该属性是自定义拦截器的全类名。在该方法中会先获取到该属性,通过该属性对应拦截器的默认构造去创建实例,并添加到Configuration中。
/**
* 解析plugins节点
* plugin节点用于配置插件
* 即 mybatis拦截器
*
* @param parent
* @throws Exception
*/
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
// 获取子节点,子节点就是所配置的拦截器
for (XNode child : parent.getChildren()) {
// 获得拦截器全类名
String interceptor = child.getStringAttribute("interceptor");
// 将节点下的节点封装成properties
Properties properties = child.getChildrenAsProperties();
// 根据拦截器的全类名,通过默认构造方法创建一个实例
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
// 将拦截器节点下的properties放到拦截器中
interceptorInstance.setProperties(properties);
// 将拦截器添加到配置中
configuration.addInterceptor(interceptorInstance);
}
}
}
解析ObjectFactory节点
objectFactory用来处理查询得到的结果集,创建对象去将结果集封装到对象中。
mybatis默认的对象工厂是用无参构造或者有参构造去创建对象,而如果开发者想在创建对象前对其进行一些初始化操作或者处理一些业务方面的逻辑,就可以自定义对象工厂并进行配置。对象工厂的解析比较简单,拿到type属性去创建一个实例并添加到Configuration即可。
/**
* 解析objectFactory节点
* objectFactory用来处理查询得到的结果集
* 创建对象去将结果集封装到对象中
* mybatis默认的object工厂是用无参构造或者有参构造去创建对象
* 而如果开发者想在创建对象前对其中的一些属性做初始化操作
* 或者做一些业务方面的逻辑
* 就可以自己去创建对象工厂并进行配置
*
* @param context
* @throws Exception
*/
private void objectFactoryElement(XNode context) throws Exception {
if (context != null) {
// 拿到objectFactory节点的type属性,该属性为对象工厂的全类名
String type = context.getStringAttribute("type");
// 拿到节点下所有的properties
Properties properties = context.getChildrenAsProperties();
// 根据type对应的类,通过默认构造去创建一个实例
ObjectFactory factory = (ObjectFactory) resolveClass(type).getDeclaredConstructor().newInstance();
// 将properties放入对象工厂
factory.setProperties(properties);
// 将对象工厂添加到配置中去
configuration.setObjectFactory(factory);
}
}
解析objectWrapperFactory和reflectorFactory
这两个节点的解析很简单,这里只贴上代码给读者去阅读,很容易就能理解。
/**
* 解析objectWrapperFactory节点
*
* @param context
* @throws Exception
*/
private void objectWrapperFactoryElement(XNode context) throws Exception {
if (context != null) {
// 获取到type属性
String type = context.getStringAttribute("type");
// 根据type属性对应的类去创建对象
ObjectWrapperFactory factory = (ObjectWrapperFactory) resolveClass(type).getDeclaredConstructor().newInstance();
// 将对象放到配置中
configuration.setObjectWrapperFactory(factory);
}
}
/**
* 解析reflectorFactory节点。代码比较简单就不写了。
* 解析流程和objectWrapperFactory一毛一样
*
* @param context
* @throws Exception
*/
private void reflectorFactoryElement(XNode context) throws Exception {
if (context != null) {
String type = context.getStringAttribute("type");
ReflectorFactory factory = (ReflectorFactory) resolveClass(type).getDeclaredConstructor().newInstance();
configuration.setReflectorFactory(factory);
}
}
处理settings节点
在前一篇文章,已经将解析settings节点的代码讲解完毕,该方法则是用来将解析后的settings节点中的配置,一一添加到Configuration。代码简单粗暴,就是一堆set
/**
* 将settings节点的配置set到Configuration中
*
* @param props
*/
private void settingsElement(Properties props) {
configuration.setAutoMappingBehavior(AutoMappingBehavior.valueOf(props.getProperty("autoMappingBehavior", "PARTIAL")));
configuration.setAutoMappingUnknownColumnBehavior(AutoMappingUnknownColumnBehavior.valueOf(props.getProperty("autoMappingUnknownColumnBehavior", "NONE")));
configuration.setCacheEnabled(booleanValueOf(props.getProperty("cacheEnabled"), true));
configuration.setProxyFactory((ProxyFactory) createInstance(props.getProperty("proxyFactory")));
configuration.setLazyLoadingEnabled(booleanValueOf(props.getProperty("lazyLoadingEnabled"), false));
configuration.setAggressiveLazyLoading(booleanValueOf(props.getProperty("aggressiveLazyLoading"), false));
configuration.setMultipleResultSetsEnabled(booleanValueOf(props.getProperty("multipleResultSetsEnabled"), true));
configuration.setUseColumnLabel(booleanValueOf(props.getProperty("useColumnLabel"), true));
configuration.setUseGeneratedKeys(booleanValueOf(props.getProperty("useGeneratedKeys"), false));
configuration.setDefaultExecutorType(ExecutorType.valueOf(props.getProperty("defaultExecutorType", "SIMPLE")));
configuration.setDefaultStatementTimeout(integerValueOf(props.getProperty("defaultStatementTimeout"), null));
configuration.setDefaultFetchSize(integerValueOf(props.getProperty("defaultFetchSize"), null));
configuration.setDefaultResultSetType(resolveResultSetType(props.getProperty("defaultResultSetType")));
configuration.setMapUnderscoreToCamelCase(booleanValueOf(props.getProperty("mapUnderscoreToCamelCase"), false));
configuration.setSafeRowBoundsEnabled(booleanValueOf(props.getProperty("safeRowBoundsEnabled"), false));
configuration.setLocalCacheScope(LocalCacheScope.valueOf(props.getProperty("localCacheScope", "SESSION")));
configuration.setJdbcTypeForNull(JdbcType.valueOf(props.getProperty("jdbcTypeForNull", "OTHER")));
configuration.setLazyLoadTriggerMethods(stringSetValueOf(props.getProperty("lazyLoadTriggerMethods"), "equals,clone,hashCode,toString"));
configuration.setSafeResultHandlerEnabled(booleanValueOf(props.getProperty("safeResultHandlerEnabled"), true));
configuration.setDefaultScriptingLanguage(resolveClass(props.getProperty("defaultScriptingLanguage")));
configuration.setDefaultEnumTypeHandler(resolveClass(props.getProperty("defaultEnumTypeHandler")));
configuration.setCallSettersOnNulls(booleanValueOf(props.getProperty("callSettersOnNulls"), false));
configuration.setUseActualParamName(booleanValueOf(props.getProperty("useActualParamName"), true));
configuration.setReturnInstanceForEmptyRow(booleanValueOf(props.getProperty("returnInstanceForEmptyRow"), false));
configuration.setLogPrefix(props.getProperty("logPrefix"));
configuration.setConfigurationFactory(resolveClass(props.getProperty("configurationFactory")));
}
解析environments
该节点标识环境配置。所谓环境,就是只运行时需要的一系列参数,也可以理解成开发中常说的“开发环境”“测试环境”“生产环境”。环境最直观的就是在不同环境下连接不同的数据库。
environments节点下提供了数据源和事务配置。
/**
* 解析environments节点
* 该节点表示环境配置
* 所谓环境,就是指运行时环境,即开发环境、测试环境、生产环境
* 环境最直观的提现就是在不同环境下数据库不同
* environments节点下就提供了数据源和事务配置
*
* @param context
* @throws Exception
*/
private void environmentsElement(XNode context) throws Exception {
if (context != null) {
if (environment == null) {
// 获取默认的环境id
environment = context.getStringAttribute("default");
}
for (XNode child : context.getChildren()) {
// 拿到子节点的id。父节点的default属性对应的就是子节点id
String id = child.getStringAttribute("id");
if (isSpecifiedEnvironment(id)) {
// 解析事务管理器
TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
// 解析数据工厂
DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
// 从工厂中拿到数据库
DataSource dataSource = dsFactory.getDataSource();
// 创建环境并set到Configuration中去
Environment.Builder environmentBuilder = new Environment.Builder(id)
.transactionFactory(txFactory)
.dataSource(dataSource);
configuration.setEnvironment(environmentBuilder.build());
}
}
}
}
解析的逻辑中,需要一并解析事务工厂和DataSource工厂。代码比较简单,和objectWrapperFactory一样,这里就不贴了
解析databaseIdProvider
该节点用于提供多数据源支持。这里的多数据源并非指多个数据库,而是指多个数据库产品。这里的代码和objectWrapperFactory比较类似,不做过多解释。
/**
* 解析databaseIdProvider节点
* 该节点用于提供多数据源支持
* 这里的多数据源并非指多个数据库
* 而是指多个数据库产品
* 这里的代码和objectFactory类似
*
* @param context
* @throws Exception
*/
private void databaseIdProviderElement(XNode context) throws Exception {
DatabaseIdProvider databaseIdProvider = null;
if (context != null) {
String type = context.getStringAttribute("type");
// awful patch to keep backward compatibility
if ("VENDOR".equals(type)) {
type = "DB_VENDOR";
}
Properties properties = context.getChildrenAsProperties();
databaseIdProvider = (DatabaseIdProvider) resolveClass(type).getDeclaredConstructor().newInstance();
databaseIdProvider.setProperties(properties);
}
// 获取当前环境
Environment environment = configuration.getEnvironment();
if (environment != null && databaseIdProvider != null) {
String databaseId = databaseIdProvider.getDatabaseId(environment.getDataSource());
configuration.setDatabaseId(databaseId);
}
}
结语
昨天加班填坑加到了12点,就没有继续为代码加注释,今天在空闲的时候就继续填坑了。目前对mybatis-config.xml文件的解析基本接近尾声,还差typeHandlers和mappers两个节点没有进行注释。相信看了这两篇文章的读者对于解析配置文件的逻辑已经有了一定的理解,因此自己阅读后面两个节点的代码解析应该不难。明天或者后天会将最后的两个节点的解析补上
*************************************优雅的分割线 **********************************
分享一波:程序员赚外快-必看的巅峰干货
如果以上内容对你觉得有用,并想获取更多的赚钱方式和免费的技术教程
请关注微信公众号:HB荷包
一个能让你学习技术和赚钱方法的公众号,持续更新