Mybatis 源码学习(12)-资源加载
Posted 凉茶方便面
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Mybatis 源码学习(12)-资源加载相关的知识,希望对你有一定的参考价值。
历史文章:
Mybatis 源码学习(11)-日志模块
Mybatis 中的资源加载依赖于 JVM 的类加载机制,在 JVM 中的类加载机制使用的双亲委派模式。默认情况下,JVM 中存在三种加载器:Bootstrap ClassLoader、Extension ClassLoader、System ClassLoader,我们也可以拓展实现用户自定义的类加载器 UserDefine ClassLoader。
ClassLoaderWrapper
Mybatis 中的资源加载功能均在 org.apache.ibatis.io 中实现,ClassLoaderWrapper 是 ClassLoader 的包装类,其内部有 ClassLoader 变量。ClassLoaderWrapper 内部会维护一个 ClassLoader 的优先级,按照特定的次序返回内部包装的 ClassLoader。
ClassLoaderWrapper 内部定义了默认 ClassLoader 和 systemClassLoader:
ClassLoader defaultClassLoader; // 由应用指定的默认类加载器
ClassLoader systemClassLoader; // SystemClassLoader
ClassLoaderWrapper()
try
// 初始化 SystemClassLoader
systemClassLoader = ClassLoader.getSystemClassLoader();
catch (SecurityException ignored)
// AccessControlException on Google App Engine
ClassLoaderWrapper 的核心功能分为三类,分别是:classForName、getResourceAsStream、getResourceAsURL,这三种方法分别有多个重载,但是最终都调用了对应的 String, ClassLoader[] 参数的方法,它们的逻辑类似,因此仅以常见的 getResourceAsStream 为例。
// 不指定 ClassLoader 加载资源
public InputStream getResourceAsStream(String resource)
return getResourceAsStream(resource, getClassLoaders(null));
// 指定 ClassLoader 加载资源
public InputStream getResourceAsStream(String resource, ClassLoader classLoader)
return getResourceAsStream(resource, getClassLoaders(classLoader));
// ClassLoaderWrapper.getClassLoaders 方法返回 ClassLoader 数组
// 该方法指定了类加载器的优先级(指定的 ClassLoader 优先级最高)
ClassLoader[] getClassLoaders(ClassLoader classLoader)
return new ClassLoader[]
classLoader, // 参数指定的类加载器
defaultClassLoader, // 系统默认的类加载器
Thread.currentThread().getContextClassLoader(), // 当前线程的类加载器(应用上下文类加载器)
getClass().getClassLoader(), // 加载当前类使用的类加载器
systemClassLoader; // SystemClassLoader
InputStream getResourceAsStream(String resource, ClassLoader[] classLoader)
for (ClassLoader cl : classLoader) // 遍历类加载器数组
if (null != cl)
// 使用类加载器加载资源
InputStream returnValue = cl.getResourceAsStream(resource);
// 加载不到资源时,尝试在路径上加上”/”前缀,再加载一次
if (null == returnValue)
returnValue = cl.getResourceAsStream("/" + resource);
// 加载到了资源
if (null != returnValue)
return returnValue;
return null;
Resources
Resources 提供了多个加载和使用资源的工具类,其中的方法均通过内部的 classLoaderWrapper 实现,这里仅以常用的 getResourceAsStream 为例。
// 初始化内部 classLoaderWrapper
private static ClassLoaderWrapper classLoaderWrapper = new ClassLoaderWrapper();
public static InputStream getResourceAsStream(ClassLoader loader, String resource) throws IOException
// 通过 classLoaderWrapper 读取资源(加载过程已经在之前解析过)
InputStream in = classLoaderWrapper.getResourceAsStream(resource, loader);
if (in == null) // 未找到资源则抛异常
throw new IOException("Could not find resource " + resource);
return in;
ResolverUtil
ResolverUtil 提供了一种使用命令的方式过滤并加载指定包内的类,它的内部 Test 接口提供了过滤功能。Test 接口有两个实现:IsA 和 AnnotatedWith 提供了两个基本的实现,分别提供按照类型过滤和按照注解过滤的功能。另外,和 ClassLoaderWrapper 一样,它也允许外部直接传入自定义的 ClassLoader,但是它默认使用应用上下文的 ClassLoader:Thread.currentThread().getContextClassLoader()。
Test 接口提供了 matches 方法用于检查是否符合特定的条件:
public interface Test
// 参数 type 是待检测的类型,在检测时,对于所有待测试的类,都需要调用该方法
// 如果检测符合条件需要返回 true
boolean matches(Class<?> type);
IsA 和 AnnotatedWith 的实现如下:
// IsA 是用来判断一个类是否是指定类的子类
public static class IsA implements Test
private Class<?> parent;
// 创建对象时指定原始的类型
public IsA(Class<?> parentType)
this.parent = parentType;
// 通过反射判断子类是否可以向父类赋值
public boolean matches(Class<?> type)
return type != null && parent.isAssignableFrom(type);
// toString 方法
// AnnotatedWith 是用来判断一个类是否使用了指定注解
public static class AnnotatedWith implements Test
private Class<? extends Annotation> annotation;
// 通过构造器指定需要判定的注解类型
public AnnotatedWith(Class<? extends Annotation> annotation)
this.annotation = annotation;
// 通过反射判断待检测的类是否配置了指定注解
public boolean matches(Class<?> type)
return type != null && type.isAnnotationPresent(annotation);
// toString 方法
使用 ResolverUtil 时仅需指定特定的包,以及需要判定的类,即可获取符合条件的子类集合。
ResolverUtil<org.apache.ibatis.logging.Log> resolverUtil = new ResolverUtil<>();
// 在指定的包内查找 Log 接口的子类
resolverUtil.find(new ResolverUtil.IsA(org.apache.ibatis.logging.Log.class), "org.apache.ibatis.logging");
// 获取查找的结果
log.info("", resolverUtil.getClasses());
除了 find 方法外,ResolverUtil 还提供了 findImplementations 和 findAnnotated,分别用于扫描包内指定类的子类和包内指定注解注释的类,它们都依赖于 find 方法的实现,这里仅以 findImplementations 为例。
// 查找多个包内实现指定类的子类
public ResolverUtil<T> findImplementations(Class<?> parent, String... packageNames)
if (packageNames == null) // 未指定包名,则直接返回
return this;
// 创建 Test 的子类 IsA
Test test = new IsA(parent);
// 遍历使用 find 方法检测
for (String pkg : packageNames)
find(test, pkg);
return this;
// 扫描单个包内,符合条件的实现
public ResolverUtil<T> find(Test test, String packageName)
String path = getPackagePath(packageName); // 获取包名,及其对应路径
try
// 通过 VFS.getInstance().list 查找路径下的所有资源
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;
// fqn 表示类的全限定名称
protected void addIfMatching(Test test, String fqn)
try
// 去除 “.class” 后缀,返回全限定名称
String externalName = fqn.substring(0, fqn.indexOf('.')).replace('/', '.');
ClassLoader loader = getClassLoader();
// 加载指定的类
Class<?> type = loader.loadClass(externalName);
if (test.matches(type)) // 判断是否满足条件
matches.add((Class<T>) type); // 符合条件,则记录到 matchs 集合中
catch (Throwable t)
log.warn(“…”);
VFS
VFS 的全称是 Visual File System,即虚拟文件系统,它主要负责加载指定路径下的资源。VFS 是一个抽象类,Mybatis 默认提供了 DefaultVFS 和 JBoss6VFS,当然用户也可以在初始化时指定自定义的 VFS。
VFS 的核心字段有两个,同时提供了内部类 VFSHolder,用于实现 Lazy 的单例模式(延迟加载占位符单例模式)。
// 记录内建的两个 VFS 的实现类
public static final Class<?>[] IMPLEMENTATIONS = JBoss6VFS.class, DefaultVFS.class ;
// 记录用户自定义的 VFS 实现,VFS.addImplClass 会把对应的实现类添加到 USER_IMPLEMENTATIONS 中
public static final List<Class<? extends VFS>> USER_IMPLEMENTATIONS = new ArrayList<Class<? extends VFS>>();
// 使用 VFSHolder 实现单例,仅 VFSHolder.class 被加载时,自动调用 createVFS,实现一次单例的加载
private static class VFSHolder
static final VFS INSTANCE = createVFS();
static VFS createVFS()
// 优先使用用户自定义的 VFS 实现,如果没有自定义实现,则使用 Mybatis 的默认实现
List<Class<? extends VFS>> impls = new ArrayList<Class<? extends VFS>>();
impls.addAll(USER_IMPLEMENTATIONS);
impls.addAll(Arrays.asList((Class<? extends VFS>[]) IMPLEMENTATIONS));
// 依次检测 impls 中所有的 VFS 实现,直到找到第一个有效的 VFS 实现,并结束循环
VFS vfs = null;
for (int i = 0; vfs == null || !vfs.isValid(); i++)
Class<? extends VFS> impl = impls.get(i);
try
vfs = impl.newInstance();
if (vfs == null || !vfs.isValid()) // vfs.isValid 由子类实现
// … 日志
catch (InstantiationException e)
log.error("Failed to instantiate " + impl, e);
return null;
catch (IllegalAccessException e)
log.error("Failed to instantiate " + impl, e);
return null;
// …日志
return vfs;
VFS 抽象类中提供了两个抽象方法:isValid 和 list,isValid 负责检测在当前环境下 VFS 实现是否有效,list 方法负责查找指定资源名称列表。VFS 默认提供两个 list 方法:list(URL url, String forPath)
和 list(String path)
,而 list(String path)
可以找找指定 path 下的所有资源,对单个资源它实际上是调用了 list(URL url, String forPath)
,接下来以 DefaultVFS.list(URL url, String path)
为例解释其中的逻辑。
public List<String> list(URL url, String path) throws IOException
InputStream is = null;
try
List<String> resources = new ArrayList<String>();
// 如果 url 指向了一个 jar包,则获取 jar 包对应的 url,否则返回 null
URL jarUrl = findJarForResource(url);
if (jarUrl != null)
is = jarUrl.openStream();
// 遍历 jar 包中的资源,返回以指定 path 开头的资源
resources = listResources(new JarInputStream(is), path);
else
List<String> children = new ArrayList<String>();
try
if (isJar(url)) // 读取文件头,判断是否是 jar 包
// 兼容部分 JBoss 虚拟文件系统对非 jar 文件返回 jar 文件格式
is = url.openStream();
JarInputStream jarInput = new JarInputStream(is);
for (JarEntry entry; (entry = jarInput.getNextJarEntry()) != null;)
// 加载所有的资源
children.add(entry.getName());
jarInput.close();
else
// 某些 servlet 容器允许从文本文件中指定目录,每行目录代表一系列资源
// 为了兼容这种情况,仅先读取第一行,如果第一行表示的是资源路径,
// 则表示该文件表示的都是资源路径
is = url.openStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
List<String> lines = new ArrayList<String>();
for (String line; (line = reader.readLine()) != null;)
lines.add(line);
if (getResources(path + "/" + line).isEmpty())
lines.clear();
break;
if (!lines.isEmpty())
children.addAll(lines);
catch (FileNotFoundException e)
// 对于文件夹类型的 url,无法直接在 servlet 容器中读取,需要转化成文件列表读取
if ("file".equals(url.getProtocol()))
File file = new File(url.getFile());
children = Arrays.asList(file.list());
else
throw e;
// 记录下来 url 的前缀,用于递归查找资源
String prefix = url.toExternalForm();
if (!prefix.endsWith("/"))
prefix = prefix + "/";
// 遍历 children 集合,递归查找符合条件的资源
for (String child : children)
String resourcePath = path + "/" + child;
resources.add(resourcePath);
URL childUrl = new URL(prefix + child);
resources.addAll(list(childUrl, resourcePath));
return resources;
finally
if (is != null)
try
is.close();
catch (Exception e)
// 列出 jar 包内的资源列表
protected List<String> listResources(JarInputStream jar, String path) throws IOException
// 如果 path 不是以 / 开头和结束,则拼接上 /
if (!path.startsWith("/"))
path = "/" + path;
if (!path.endsWith("/"))
path = path + "/";
// Iterate over the entries and collect those that begin with the requested path
// 遍历整个 jar 包内的资源,并把以 path 开头的资源记录到 resources 中
List<String> resources = new ArrayList<String>();
for (JarEntry entry; (entry = jar.getNextJarEntry()) != null;)
if (!entry.isDirectory())
// 如果 name 不是以 / 开头和结束,则拼接上 /
String name = entry.getName();
if (!name.startsWith("/"))
name = "/" + name;
// 检测 name 是否以 path 开头
if (name.startsWith(path))
// 记录资源名称
resources.add(name.substring(1));
return resources;
总结
本次内容较多,但是都是 org.apache.ibatis.io 包内的内容,该包提供了资源加载的功能,Mybatis 的资源加载依赖于 ClassLoader。
ClassLoaderWrapper 提供了 ClassLoader 的封装,便于在运行时自动选择 ClassLoader。
Resources 封装了 ClassLoaderWrapper,通过内部字段实现对外的资源加载逻辑。ResolverUtil 提供了根据条件筛选指定 package 内的资源。
VFS 提供了虚拟文件系统,其实就是按照包路径加载资源的抽象,它不仅可以解析 class 文件,还可以解析jar 包内的资源。比较特殊的一点是,VFS 内部使用了延迟资源占位符的方式实现了单例模式。
参考文档:《Mybatis 技术内幕》
本文的基本脉络参考自《Mybatis 技术内幕》,编写文章的原因是希望能够系统地学习 Mybatis 的源码,但是如果仅阅读源码或者仅从官方文档很难去系统地学习,因此希望参考现成的文档,按照文章的脉络逐步学习。
欢迎关注我的公众号:我的搬砖日记,我会定时分享自己的学习历程。
以上是关于Mybatis 源码学习(12)-资源加载的主要内容,如果未能解决你的问题,请参考以下文章