2.5.加载自定义的文件访问系统和日志系统
在parseConfiguration
方法中我们通过propertiesElement
和settingsAsProperties
两个方法已经完成了对mybatis属性配置的解析和准备工作。
//issue #117 read properties first
// 加载资源配置文件,并覆盖对应的属性[properties节点]
propertiesElement(root.evalNode("properties"));
// 将settings标签内的内容转换为Properties,并进行校验。
Properties settings = settingsAsProperties(root.evalNode("settings"));
// 根据settings的配置确定访问资源文件的方式
loadCustomVfs(settings);
// ...
接下来的需要做的就是使用前面获取到的属性来对mybatis的运行环境进行简单的配置,主要包括初始化访问虚拟文件系统的VFS
实例,以及日志Log
实例。
// 根据settings的配置确定访问资源文件的方式
loadCustomVfs(settings);
// 根据settings的配置确定日志处理的方式
loadCustomLogImpl(settings);
VFS
配置虚拟文件访问系统VFS
的方法loadCustomVfs
实现比较简单:
/**
* 加载访问系统虚拟文件系统的实现类
*
* @param props 系统全局配置
* @throws ClassNotFoundException 未找到实现类
*/
private void loadCustomVfs(Properties props) throws ClassNotFoundException {
// 获取配置的vfsImpl属性,如果存在的话,将会覆盖默认的虚拟文件系统访问实现类
String value = props.getProperty("vfsImpl");
if (value != null) {
// 多个实现类以","分隔
String[] clazzes = value.split(",");
for (String clazz : clazzes) {
if (!clazz.isEmpty()) {
// 通过类名称获取类定义
@SuppressWarnings("unchecked")
Class<? extends VFS> vfsImpl = (Class<? extends VFS>) Resources.classForName(clazz);
// 更新Mybatis访问虚拟文件系统的实现类
// 这就意味着当时用逗号作为分隔符定义了多个实现类时,最后一个实现类生效
configuration.setVfsImpl(vfsImpl);
}
}
}
}
用户可以通过全局配置文件来配置VFS
的实现类,如果有多个VFS
实现,可以使用英文的逗号进行分隔:
<properties>
<property name="vfsImpl" value="实现1,实现2"/>
</properties>
VFS
实现类的具体注册工作是由Configuration
对象的setVfsImpl
方法来完成的:
/**
* 新增一个VFS实例
*
* @param vfsImpl VFS实例
*/
public void setVfsImpl(Class<? extends VFS> vfsImpl) {
if (vfsImpl != null) {
this.vfsImpl = vfsImpl;
// 添加一个新的VFS实例
VFS.addImplClass(this.vfsImpl);
}
}
setVfsImpl
方法实现也比较简单,首先刷新了Configuration
对象的vfsImpl
属性的值,之后调用VFS
的addImplClass
方法完成自定义VFS
实例的注册工作。
Configuration
对象的vfsImpl
属性用于简单的记录当前使用的VFS
实例类型:
/**
* 虚拟文件系统,提供了一个访问系统文件资源的简单API,
* 对当前使用的VFS实例类型进行记录
*/
protected Class<? extends VFS> vfsImpl;
VFS
的addImplClass
方法则用来缓存用户自定义的VFS
实现类。
说了这么久,这个VFS具体是做什么的呢?
VFS
全称是Virtual File System
,他是一个虚拟的文件系统,为读取不同存储介质中的文件和数据提供一个统一的API方法。
在mybatis中,VFS
定义了访问程序宿主机资源的API接口,这些资源包括:jar包,class文件,配置文件等。
mybatis内置了两种VFS
实现:JBoss6VFS
和DefaultVFS
:
/**
* 内置的VFS实现类
*/
public static final Class<?>[] IMPLEMENTATIONS = {JBoss6VFS.class, DefaultVFS.class};
其中JBoss6VFS
的实现依赖于JBoss 6
的VFS
API,DefaultVFS
则是适用于大多数应用程序服务器的VFS
的默认实现。
在VFS
中定义了两个常量,其中IMPLEMENTATIONS
我们已经看过了,他用于记录mybatis内置的VFS
实现类。
另一个常量USER_IMPLEMENTATIONS
属性用来保存用户自定义VFS
实现集合:
/**
* 用户自定义VFS实现集合。
* <p>
* 通过{@link #addImplClass(Class)}方法添加的用户自定义实现。
*/
public static final List<Class<? extends VFS>> USER_IMPLEMENTATIONS = new ArrayList<>();
我们刚才提到的VFS
的addImplClass
方法的具体作用就是把用户自定义的VFS
实现类保存到USER_IMPLEMENTATIONS
集合中:
/**
* 添加用户自定义的VFS实现,自定义实现优先级高于内置的实现
*
* @param clazz 被添加的{@link VFS}实现
*/
public static void addImplClass(Class<? extends VFS> clazz) {
if (clazz != null) {
USER_IMPLEMENTATIONS.add(clazz);
}
}
其实在VFS
中还定义了一个名为VFSHolder
的单例对象,该对象的作用是获取VFS
实例。
/**
* VFS 单例实例持有者
*/
private static class VFSHolder {
static final VFS INSTANCE = createVFS();
@SuppressWarnings("unchecked")
static VFS createVFS() {
// Try the user implementations first, then the built-ins
List<Class<? extends VFS>> impls = new ArrayList<>();
// 优先使用用户自己加载的
impls.addAll(USER_IMPLEMENTATIONS);
// 使用系统默认的
impls.addAll(Arrays.asList((Class<? extends VFS>[]) IMPLEMENTATIONS));
// 遍历所有实现类,获取一个有效的
VFS vfs = null;
for (int i = 0; vfs == null || !vfs.isValid(); i++) {
// 当获取不到vfs对象时或者找到有效的vfs对象时结束.
// 获取vfs实例类型
Class<? extends VFS> impl = impls.get(i);
try {
// 实例化vfs
vfs = impl.newInstance();
if (vfs == null || !vfs.isValid()) {
if (log.isDebugEnabled()) {
log.debug("VFS implementation " + impl.getName() +
" is not valid in this environment.");
}
}
} catch (InstantiationException e) {
log.error("Failed to instantiate " + impl, e);
return null;
} catch (IllegalAccessException e) {
log.error("Failed to instantiate " + impl, e);
return null;
}
}
if (log.isDebugEnabled()) {
log.debug("Using VFS adapter " + vfs.getClass().getName());
}
return vfs;
}
}
方法很长,但是逻辑很简单,就是通过反射机制获取VFS
实例对象,其中用户自定义实现优先级要高于系统默认实现。
单例也是常用的设计模式之一,我们通常称之为单例模式,他是一种创建型模式,他的作用是确保指定的对象在一定的范围内是独一无二的,这个范围通常就是整个系统内。
在这里VFSHolder
是一个饿汉型的单例模式的应用。
VFS
对外暴露了四个方法,其中:
getInstance
方法用于获取VFS
的单例对象:/** * 获取VFS实现 */ public static VFS getInstance() { return VFSHolder.INSTANCE; }
addImplClass
用于添加用户自定义的VFS
实现类/** * 添加用户自定义的VFS实现,自定义实现优先级高于内置的实现 * * @param clazz 被添加的{@link VFS}实现 */ public static void addImplClass(Class<? extends VFS> clazz) { if (clazz != null) { USER_IMPLEMENTATIONS.add(clazz); } }
isValid
方法用于判断当前VFS
实例是否有效,比如JBoss6VFS
在没有引入jboss-vfs
依赖的前提下,他就是无效的。/** * 在当前环境内,该VFS实现是否有效 */ public abstract boolean isValid();
list
方法用于递归获取指定路径下的所有资源列表
关于/** * 递归获取指定路径下的所有资源列表 * * @param path 资源路径 * @return 所有资源 * @throws IOException If I/O errors occur */ public List<String> list(String path) throws IOException { List<String> names = new ArrayList<>(); for (URL url : getResources(path)) { names.addAll(list(url, path)); } return names; }
VFS
的实现类的解析,我们会在后面进行补充。
Log
我们继续看mybatis初始化Log
所做的工作。
用户可以通过mybatis的全局配置文件来指定mybatis系统内部使用的日志实现类:
<properties>
<property name="logImpl" value="日志实现"/>
</properties>
对于logImpl
属性的解析工作是由方法loadCustomLogImpl
来完成的:
/**
* 加载自定义日志实现类
*
* @param props 全局配置
*/
private void loadCustomLogImpl(Properties props) {
// 获取日志实现类
Class<? extends Log> logImpl = resolveClass(props.getProperty("logImpl"));
// 配置日志实现
configuration.setLogImpl(logImpl);
}
该方法的实现也比较简单,调用resolveClass
方法获取日志实现类,并把后续的日志配置操作委托给了configuration
的setLogImpl
方法来完成。
resolveClass
方法是在XMLConfigBuilder
的父类BaseBuilder
中定义的一个方法,它用于将指定的类名转换为对应的JAVA类型,他提供了对mybatis别名机制的支持.
/**
* 读取指定别名的实际类型
*
* @param alias 别名
* @param <T> 实际类型
* @return 指定别名的实际类型
*/
protected <T> Class<? extends T> resolveClass(String alias) {
if (alias == null) {
return null;
}
try {
// 解析别名
return resolveAlias(alias);
} catch (Exception e) {
throw new BuilderException("Error resolving class. Cause: " + e, e);
}
}
具体实现是由借助resolveAlias
方法委托给TypeAliasRegistry
的resolveAlias
方法来完成的:
/**
* 从别名注册表中取出指定的类
*
* @param alias 别名
* @param <T> 指定的类型
*/
protected <T> Class<? extends T> resolveAlias(String alias) {
return typeAliasRegistry.resolveAlias(alias);
}
TypeAliasRegistry
这个类我们在前面稍有提及,他提供了注册类型别名和解析类型别名的能力,在mybatis中担任类型别名注册表的职责。
他的无参构造方法,默认完成了一些常用类型的别名注册工作:
public TypeAliasRegistry() {
registerAlias("string", String.class);
registerAlias("byte", Byte.class);
registerAlias("long", Long.class);
registerAlias("short", Short.class);
registerAlias("int", Integer.class);
registerAlias("integer", Integer.class);
registerAlias("double", Double.class);
registerAlias("float", Float.class);
registerAlias("boolean", Boolean.class);
registerAlias("byte[]", Byte[].class);
registerAlias("long[]", Long[].class);
registerAlias("short[]", Short[].class);
registerAlias("int[]", Integer[].class);
registerAlias("integer[]", Integer[].class);
registerAlias("double[]", Double[].class);
registerAlias("float[]", Float[].class);
registerAlias("boolean[]", Boolean[].class);
registerAlias("_byte", byte.class);
registerAlias("_long", long.class);
registerAlias("_short", short.class);
registerAlias("_int", int.class);
registerAlias("_integer", int.class);
registerAlias("_double", double.class);
registerAlias("_float", float.class);
registerAlias("_boolean", boolean.class);
registerAlias("_byte[]", byte[].class);
registerAlias("_long[]", long[].class);
registerAlias("_short[]", short[].class);
registerAlias("_int[]", int[].class);
registerAlias("_integer[]", int[].class);
registerAlias("_double[]", double[].class);
registerAlias("_float[]", float[].class);
registerAlias("_boolean[]", boolean[].class);
registerAlias("date", Date.class);
registerAlias("decimal", BigDecimal.class);
registerAlias("bigdecimal", BigDecimal.class);
registerAlias("biginteger", BigInteger.class);
registerAlias("object", Object.class);
registerAlias("date[]", Date[].class);
registerAlias("decimal[]", BigDecimal[].class);
registerAlias("bigdecimal[]", BigDecimal[].class);
registerAlias("biginteger[]", BigInteger[].class);
registerAlias("object[]", Object[].class);
registerAlias("map", Map.class);
registerAlias("hashmap", HashMap.class);
registerAlias("list", List.class);
registerAlias("arraylist", ArrayList.class);
registerAlias("collection", Collection.class);
registerAlias("iterator", Iterator.class);
registerAlias("ResultSet", ResultSet.class);
}
registerAlias
方法负责完成类型别名和类型映射关系的具体注册工作:
/**
* 将指定类型和别名注册到别名注册表
*
* @param alias 别名
* @param value 指定类型
*/
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);
// 不可重复注册,校验是否已经注册过
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);
}
他的实现也比较简单,只有三步操作,将别名小写,校验别名是否被重复注册,保存到TYPE_ALIASES
集合内。
TYPE_ALIASES
是TypeAliasRegistry
中唯一的一个属性,他被定义为Map<String, Class<?>>
类型,负责维护类型别名和具体的类型的关系。
/**
* 负责维护类型别名和具体的类型的关系
*/
private final Map<String, Class<?>> TYPE_ALIASES = new HashMap<>();
在简单的了解TypeAliasRegistry
对象之后,我们回到XMLConfigBuilder
的loadCustomLogImpl
方法上来,看一下他被BaseBuilder
调用的resolveAlias(String)
方法:
/**
* 将类型别名解析为具体的类型
*
* @param string 类型别名或者完全限定名
*/
@SuppressWarnings("unchecked")
// throws class cast exception as well if types cannot be assigned
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 (TYPE_ALIASES.containsKey(key)) {
// 优先从别名注册表加载
value = (Class<T>) TYPE_ALIASES.get(key);
} else {
// 假设string是一个全限定类名称,通过反射获取
value = (Class<T>) Resources.classForName(string);
}
return value;
} catch (ClassNotFoundException e) {
throw new TypeException("Could not resolve type alias '" + string + "'. Cause: " + e, e);
}
}
该方法的实现也比较简单,在将类型别名小写化后,会优先尝试从现有类型别名映射关系中获取其对应的java类型,如果没有的话,则将该别名作为一个全限定名称通过反射获取其对应的java类型。
在获取到具体的日志实现类型之后,XMLConfigBuilder
会将该日志实现类型作为参数传递给Configuration
的setLogImpl
方法,完成后续日志实现的配置.
在Configuration
的setLogImpl
方法中,先简单的记录了当前日志实现类,之后就将具体更新日志实现的操作交给了LogFactory
的useCustomLogging
方法:
public void setLogImpl(Class<? extends Log> logImpl) {
if (logImpl != null) {
// 缓存记录当前日志实现类
this.logImpl = logImpl;
// 更新具体的日志实现
LogFactory.useCustomLogging(this.logImpl);
}
}
在看LogFactory
的useCustomLogging
方法之前,我们先简单了解一下LogFactory
。
LogFactory
是一个工厂类,他的作用是获取Log
接口的实例对象,Log
接口提供了mybatis中操作日志的统一接口定义。
在LogFactory
内部定义了一个静态代码块用于加载默认的Log
接口实现类:
static {
// 尝试加载一个有效的日志实现
// SL4J
tryImplementation(LogFactory::useSlf4jLogging);
// COMMONS-LOGGING
tryImplementation(LogFactory::useCommonsLogging);
// LOG4J2
tryImplementation(LogFactory::useLog4J2Logging);
// LOG4J
tryImplementation(LogFactory::useLog4JLogging);
// JDK-LOGGING
tryImplementation(LogFactory::useJdkLogging);
// NO-LOGGING
tryImplementation(LogFactory::useNoLogging);
}
加载的顺序以及采用日志实现的优先级依次是:SL4J
,COMMONS-LOGGING
,LOG4J2
,LOG4J
,JDK-LOGGING
,NO-LOGGING
。 上面涉及到的以use
开头的各个方法实际上只是对setImplementation
方法的一层简单封装:
public static synchronized void useCustomLogging(Class<? extends Log> clazz) {
setImplementation(clazz);
}
public static synchronized void useSlf4jLogging() {
setImplementation(org.apache.ibatis.logging.slf4j.Slf4jImpl.class);
}
public static synchronized void useCommonsLogging() {
setImplementation(org.apache.ibatis.logging.commons.JakartaCommonsLoggingImpl.class);
}
public static synchronized void useLog4JLogging() {
setImplementation(org.apache.ibatis.logging.log4j.Log4jImpl.class);
}
public static synchronized void useLog4J2Logging() {
setImplementation(org.apache.ibatis.logging.log4j2.Log4j2Impl.class);
}
public static synchronized void useJdkLogging() {
setImplementation(org.apache.ibatis.logging.jdk14.Jdk14LoggingImpl.class);
}
public static synchronized void useStdOutLogging() {
setImplementation(org.apache.ibatis.logging.stdout.StdOutImpl.class);
}
public static synchronized void useNoLogging() {
setImplementation(org.apache.ibatis.logging.nologging.NoLoggingImpl.class);
}
因此实现加载日志实现的方法tryImplementation
实际上也是对setImplementation
方法的一层包装,他忽略掉了setImplementation
可能抛出的异常:
/**
* 尝试加载日志实现类
*/
private static void tryImplementation(Runnable runnable) {
if (logConstructor == null) {
try {
runnable.run();
} catch (Throwable t) {
// ignore
}
}
}
logConstructor
属性记录了当前使用的Log
实现类的构造方法,调用tryImplementation
方法的时候,只有在logConstructor
的值没有被指定时,才会尝试加载新的日志实现类。
在setImplementation
方法中,LogFactory
会尝试获取Log
实现类中入参为String
的构造方法并赋值给logConstructor
属性,如果Log
实现没有入参为String
的构造方法 ,将会导致异常的发生。
/**
* 配置日志实现类
*
* @param implClass 日志实现
*/
private static void setImplementation(Class<? extends Log> implClass) {
try {
// 获取构造
Constructor<? extends Log> candidate = implClass.getConstructor(String.class);
// 生成实例
Log log = candidate.newInstance(LogFactory.class.getName());
if (log.isDebugEnabled()) {
log.debug("Logging initialized using '" + implClass + "' adapter.");
}
logConstructor = candidate;
} catch (Throwable t) {
throw new LogException("Error setting Log implementation. Cause: " + t, t);
}
}
对于mybatis的日志解析,到这里就暂时告一段落,关于更具体的日志实现,我们会在后面进行补充。
当XMLConfigBuilder
的parseConfiguration
方法完成VFS
和LOG
的配置工作之后,我们就迎来了typeAliases
元素的解析工作。