加载自定义的文件访问系统和日志系统
在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的VFSAPI,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元素的解析工作。
关注我,一起学习更多知识
