一、类加载
Java 虚拟机使用类加载器来加载来自文件系统、网络或其他来源的类文件。默认有三种类加载器,分别为 Bootstrap ClassLoader
、Extension ClassLoader
、System ClassLoader
(也称为 Application ClassLoader
),它们的区别如下:
-
Bootstrap ClassLoader
:负责加载 JDK 核心类库(rt.jar)的类加载器,同时是所有类加载器的父加载器,并且是最顶层的类加载器,就好像 Object 是最顶层的基类一样。 -
Extension ClassLoader
:加载 Java 的扩展类库,也就是从 jre/lib/ext 目录下或者 java.ext.dirs 系统属性指定的目录下加载类,该加载器是 Bootstrap ClassLoader 的子加载器。 -
System ClassLoader
:负责从 classpath 环境变量中加载类文件,是 Extension ClassLoder 的子加载器,通常应用代码就是由该加载器加载。
测试案例:
import com.sun.java.accessibility.AccessBridge;
public class ClassLoaderTest {
public static void main(String[] args) {
// java.lang 包中的类属于核心类库,类加载器为 Bootstrap ClassLoader
ClassLoader bootCl = Integer.class.getClassLoader();
System.out.println(bootCl);
// AccessBridge属于扩展类库中的类,类加载器为 Extension ClassLoader
ClassLoader extCl = AccessBridge.class.getClassLoader();
System.out.println(extCl);
// 应用程序类的类加载器为 Application ClassLoader
ClassLoader appCl = ClassLoaderTest.class.getClassLoader();
System.out.println(appCl);
}
}
执行结果:
null // null 就代表 Bootstrap ClassLoader
sun.misc.Launcher$ExtClassLoader@5305068a
sun.misc.Launcher$AppClassLoader@58644d46
根据自身需要,开发人员也可以通过继承 java.lang.ClassLoader
的方式自定义类加载器,类加载器有个运作模式,叫 “双亲委派模式”,如下图所示:
在双亲委派模式中,加载类文件时,子加载器会先委托父加载器,父加载器先检查是否已加载该类,如果是则加载过程结束;否则继续委托给上一层的父加载器,当顶层的加载器也没有检查到该类是否已加载,则尝试从对应路径中加载该类文件,如果加载失败,则由子加载器继续尝试加载,直到发起加载请求的子加载器为止。
双亲委派模式可以保证:
(1)子加载器可以使用父加载器加载过的类,避免类的重复加载,而父加载器无法使用子加载器已加载的类。
(2)父加载器已加载过的类无法被子加载器再次加载,其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为 java.lang.Integer
的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心 Java API 发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改,保证 JVM 的安全性和稳定性。
二、资源加载模块简介
资源加载模块实现了对类加载器进行封装,确定类加载器的使用顺序,并提供了加载类文件和其他资源文件的功能,相关的类位于 org.apache.ibatis.io
包中,如下图所示:
类图如下:
三、ClassLoaderWrapper
3.1 初始化
public class ClassLoaderWrapper {
ClassLoader defaultClassLoader; // 默认 ClassLoader
ClassLoader systemClassLoader; // 系统 ClassLoader,一般为AppClassLoader
ClassLoaderWrapper() {
try {
systemClassLoader = ClassLoader.getSystemClassLoader();
} catch (SecurityException ignored) {
// AccessControlException on Google App Engine
}
}
}
3.2 getClassLoaders
【功能】获取类加载器的数组,按照使用优先级顺序返回。
【源码】
ClassLoader[] getClassLoaders(ClassLoader classLoader) {
return new ClassLoader[]{
classLoader, // 根据传入的参数指定
defaultClassLoader, // 不设null,则为 BootstrapClassLoader
Thread.currentThread().getContextClassLoader(), // 当前线程上下文的类加载器
getClass().getClassLoader(), // 加载当前类的加载器,应用代码一般为AppClassLoader
systemClassLoader}; // 系统类加载器 AppClassLoader
}
【解析】
加载器数组中的类加载器的使用优先级为:指定的类加载器 > 默认类加载器 > 当前线程上下文的类加载器 > 加载当前类的加载器 > 系统类加载器。由上面的案例可知,核心类库的加载器的值为 null
,所以不能用 BootstrapClassLoader 来加载类;如果用 ExtClassLoader
加载非扩展类库中的类,会报错 Class not found,所以如果没有自定义类加载器,实际只有 AppClassLoader
可用,这里我认为更多地还是考虑扩展性,因为在一些服务器中如 Tomcat、JBoss
,是会自定义类加载器来满足自身的类加载需求的。
3.3 getResourceAsURL
【功能】获取指定资源并以一个 URL
对象的形式返回。
【源码与注解】
public URL getResourceAsURL(String resource) {
return getResourceAsURL(resource, getClassLoaders(null));
}
public URL getResourceAsURL(String resource, ClassLoader classLoader) {
return getResourceAsURL(resource, getClassLoaders(classLoader));
}
URL getResourceAsURL(String resource, ClassLoader[] classLoader) {
URL url;
for (ClassLoader cl : classLoader) {
if (null != cl) {
// 尝试找到传参进来的资源
url = cl.getResource(resource);
// 有一些类加载器需要资源路径以"/"开头,所以如果上面加载失败的话,这里再补上"/"重新尝试一下加载资源
if (null == url) {
url = cl.getResource("/" + resource);
}
// 假如已加载到资源了,直接返回URL对象,否则尝试用下一个类加载器加载
if (null != url) {
return url;
}
}
}
// 没有加载到资源返回null
return null;
}
3.4 getResourceAsStream
【功能】获取指定资源并返回一个 InputStream
对象。
【源码与注解】
public InputStream getResourceAsStream(String resource) {
return getResourceAsStream(resource, getClassLoaders(null));
}
public InputStream getResourceAsStream(String resource, ClassLoader classLoader) {
return getResourceAsStream(resource, getClassLoaders(classLoader));
}
InputStream getResourceAsStream(String resource, ClassLoader[] classLoader) {
for (ClassLoader cl : classLoader) {
if (null != cl) {
// 尝试找到传参进来的资源
// try to find the resource as passed
InputStream returnValue = cl.getResourceAsStream(resource);
// 有一些类加载器需要资源路径以"/"开头,所以如果上面加载失败的话,这里再补上"/"重新尝试一下加载资源
if (null == returnValue) {
returnValue = cl.getResourceAsStream("/" + resource);
}
// 假如已加载到资源了,直接返回URL对象,否则尝试用下一个类加载器加载
if (null != returnValue) {
return returnValue;
}
}
}
return null;
}
3.5 classForName
【功能】根据类的权限定名加载 Class 对象。
【源码与注解】
public Class<?> classForName(String name) throws ClassNotFoundException {
return classForName(name, getClassLoaders(null));
}
public Class<?> classForName(String name, ClassLoader classLoader) throws ClassNotFoundException {
return classForName(name, getClassLoaders(classLoader));
}
Class<?> classForName(String name, ClassLoader[] classLoader) throws ClassNotFoundException {
for (ClassLoader cl : classLoader) {
if (null != cl) {
try {
Class<?> c = Class.forName(name, true, cl);
// 如果加载到Class马上返回,否则接着由下一个加载器尝试加载
if (null != c) {
return c;
}
} catch (ClassNotFoundException e) {
}
}
}
// 如果最终没加载到Class,抛出异常
throw new ClassNotFoundException("Cannot find class: " + name);
}
四、Resources
Resources
是一个基于 ClassLoaderWrapper 的加载功能进行封装的类,并实现将资源转换为更多的形式返回的方法,以满足更多的使用场景和需求。
4.1 初始化
public class Resources {
private static ClassLoaderWrapper classLoaderWrapper = new ClassLoaderWrapper();
private static Charset charset;
Resources() {
}
public static ClassLoader getDefaultClassLoader() {
return classLoaderWrapper.defaultClassLoader;
}
public static void setDefaultClassLoader(ClassLoader defaultClassLoader) {
classLoaderWrapper.defaultClassLoader = defaultClassLoader;
}
public static Charset getCharset() {
return charset;
}
public static void setCharset(Charset charset) {
Resources.charset = charset;
}
// other code
}
【解析】
定义了两个成员变量,classLoaderWrapper
是功能方法实现依赖的底层对象,charset
是一些方法时需要指定的字符集。
4.2 getResourceURL
【功能】加载指定路径的资源,以 URL
形式返回。
public static URL getResourceURL(String resource) throws IOException {
// issue #625
return getResourceURL(null, resource);
}
public static URL getResourceURL(ClassLoader loader, String resource) throws IOException {
URL url = classLoaderWrapper.getResourceAsURL(resource, loader);
// 若加载不带资源抛出IO异常
if (url == null) {
throw new IOException("Could not find resource " + resource);
}
return url;
}
4.3 getResourceAsStream
【功能】加载指定路径的资源,以 InputStream
形式返回。
public static InputStream getResourceAsStream(String resource) throws IOException {
return getResourceAsStream(null, resource);
}
public static InputStream getResourceAsStream(ClassLoader loader, String resource) throws IOException {
InputStream in = classLoaderWrapper.getResourceAsStream(resource, loader);
if (in == null) {
throw new IOException("Could not find resource " + resource);
}
return in;
}
4.4 getResourceAsProperties
【功能】一般用来加载指定路径下的 properties 文件,返回 Properties
对象。
public static Properties getResourceAsProperties(String resource) throws IOException {
Properties props = new Properties();
InputStream in = getResourceAsStream(resource);
props.load(in); // 先得到InputStream对象,再调用 Properties.load 加载属性文件
in.close();
return props;
}
public static Properties getResourceAsProperties(ClassLoader loader, String resource) throws IOException {
Properties props = new Properties();
InputStream in = getResourceAsStream(loader, resource);
props.load(in);
in.close();
return props;
}
4.5 getResourceAsReader
【功能】加载指定路径的资源,以 Reader
的形式返回。
public static Reader getResourceAsReader(String resource) throws IOException {
Reader reader;
if (charset == null) {
reader = new InputStreamReader(getResourceAsStream(resource));
} else {
reader = new InputStreamReader(getResourceAsStream(resource), charset);
}
return reader;
}
public static Reader getResourceAsReader(ClassLoader loader, String resource) throws IOException {
Reader reader;
if (charset == null) {
reader = new InputStreamReader(getResourceAsStream(loader, resource));
} else {
reader = new InputStreamReader(getResourceAsStream(loader, resource), charset);
}
return reader;
}
4.6 getResourceAsFile
【功能】加载指定路径的资源,以 File
的形式返回。
public static File getResourceAsFile(String resource) throws IOException {
return new File(getResourceURL(resource).getFile());
}
public static File getResourceAsFile(ClassLoader loader, String resource) throws IOException {
return new File(getResourceURL(loader, resource).getFile());
}
4.7 getUrlAs*
【功能】从网络加载资源,并转化为指定的形式返回。
// 以 InputStream 形式返回
public static InputStream getUrlAsStream(String urlString) throws IOException {
URL url = new URL(urlString);
URLConnection conn = url.openConnection();
return conn.getInputStream();
}
// 以 Reader 形式返回
public static Reader getUrlAsReader(String urlString) throws IOException {
Reader reader;
if (charset == null) {
reader = new InputStreamReader(getUrlAsStream(urlString));
} else {
reader = new InputStreamReader(getUrlAsStream(urlString), charset);
}
return reader;
}
// 以 Properties 形式返回
public static Properties getUrlAsProperties(String urlString) throws IOException {
Properties props = new Properties();
InputStream in = getUrlAsStream(urlString);
props.load(in);
in.close();
return props;
}
4.8 classForName
【功能】根据类的全限定名加载得到 Class
对象。
public static Class<?> classForName(String className) throws ClassNotFoundException {
return classLoaderWrapper.classForName(className);
}
五、ResolverUtil
ResolverUtil
用来找出指定包中指定类型或者被指定注解标注的类。
5.1 Test
【功能】ResolverUtil 的内部类,用来判断被测试的类是否符合要求。
public static interface Test {
boolean matches(Class<?> type);
}
5.2 IsA
【功能】判断一个类是否为指定的类型。
public static class IsA implements Test {
private Class<?> parent; // 指定判断的类
public IsA(Class<?> parentType) {
this.parent = parentType;
}
@Override
public boolean matches(Class<?> type) {
return type != null && parent.isAssignableFrom(type);
}
@Override
public String toString() {
return "is assignable to " + parent.getSimpleName();
}
}
5.3 AnnotatedWith
【功能】判断一个类是否被指定注解标注
public static class AnnotatedWith implements Test {
private Class<? extends Annotation> annotation; // 指定判断的注解
public AnnotatedWith(Class<? extends Annotation> annotation) {
this.annotation = annotation;
}
@Override
public boolean matches(Class<?> type) {
return type != null && type.isAnnotationPresent(annotation);
}
@Override
public String toString() {
return "annotated with @" + annotation.getSimpleName();
}
}
5.4 初始化
private static final Log log = LogFactory.getLog(ResolverUtil.class);
private Set<Class<? extends T>> matches = new HashSet<Class<? extends T>>();
private ClassLoader classloader;
public Set<Class<? extends T>> getClasses() {
return matches;
}
public ClassLoader getClassLoader() {
return classloader == null ? Thread.currentThread().getContextClassLoader() : classloader;
}
public void setClassLoader(ClassLoader classloader) {
this.classloader = classloader;
}
ResolverUtil
的成员 matches
用来保存符合匹配条件的类,classloader
是用于加载类的加载器。
5.5 find
【功能】找到指定包中符合指定类型的类。
【源码与注解】
public ResolverUtil<T> find(Test test, String packageName) {
// (1)将包名转化为包路径
String path = getPackagePath(packageName);
try {
// (2)通过 VFS 接口获取包路径中的所有资源文件
List<String> children = VFS.getInstance().list(path);
for (String child : children) {
// (3)只处理class文件
if (child.endsWith(".class")) {
// (4)测试类是否符合条件,是则添加到matches集合中
addIfMatching(test, child);
}
}
} catch (IOException ioe) {
log.error("Could not read package: " + packageName, ioe);
}
return this;
}
【解析】
- (1)将包名转化为包路径,eg:
org.apache.ibatis.io
->org/apache/ibatis/io
,调用#getPackagePath()
方法处理,如下:
protected String getPackagePath(String packageName) {
return packageName == null ? null : packageName.replace('.', '/');
}
- (2)通过 VFS 接口获取包路径中的所有资源文件,List 中的字符串形如
org/apache/ibatis/io/Resources.class
。 - (3)只处理class文件。
- (4)测试类是否符合条件,是则添加到matches集合中,
#addIfMatching()
方法如下:
protected void addIfMatching(Test test, String fqn) {
try {
// 将 org/apache/ibatis/io/Resources.class 转化为 org.apache.ibatis.io.Resources
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);
// 测试是否匹配,是则加入到 matches 中
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());
}
}
5.6 findImplementations
【功能】基于 find
方法实现,查找符合指定类型的多个包中匹配的类。
【源码】
// packageNames 是一个可变参数列表,实际处理时当成 String[] 即可
public ResolverUtil<T> findImplementations(Class<?> parent, String... packageNames) {
if (packageNames == null) {
return this;
}
Test test = new IsA(parent);
for (String pkg : packageNames) {
find(test, pkg);
}
return this;
}
5.7 findAnnotated
【功能】基于 find
方法实现,查找符合指定注解标注的多个包中匹配的类。
【源码】
public ResolverUtil<T> findAnnotated(Class<? extends Annotation> annotation, String... packageNames) {
if (packageNames == null) {
return this;
}
Test test = new AnnotatedWith(annotation);
for (String pkg : packageNames) {
find(test, pkg);
}
return this;
}
5.8 测试案例
public class ResolverUtilTest {
@Test
public void test_find() {
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
resolverUtil.find(new ResolverUtil.IsA(ResolverUtil.Test.class), "org.apache.ibatis.io");
for (Class<?> clazz : resolverUtil.getClasses()) {
System.out.println(clazz);
}
}
@Test
public void test_findImplementations() {
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
resolverUtil.findImplementations(ResolverUtil.Test.class, "org.apache.ibatis.io");
for (Class<?> clazz : resolverUtil.getClasses()) {
System.out.println(clazz);
}
}
}
执行结果:
六、VFS
VFS
提供了用于访问服务器内资源的非常简单的API,这是一个抽象类,框架内置了 DefaultVFS
、JBoss6VFS
两个子类,默认使用 DefaultVFS
。
6.1 数据结构
// 框架内建的实现类
public static final Class<?>[] IMPLEMENTATIONS = { JBoss6VFS.class, DefaultVFS.class };
// 通过调用 #addImplClass(Class) 方法添加的用户实现类
public static final List<Class<? extends VFS>> USER_IMPLEMENTATIONS = new ArrayList<Class<? extends VFS>>();
// 使用单例模式
private static VFS instance;
-
IMPLEMENTATIONS
:框架内建的实现类。 -
USER_IMPLEMENTATIONS
:通过调用#addImplClass(Class)
方法添加的用户实现类,该方法如下:
public static void addImplClass(Class<? extends VFS> clazz) {
if (clazz != null) {
USER_IMPLEMENTATIONS.add(clazz);
}
}
-
instance
:单例模式,获取 VFS 实例要调用#getInstance
方法,使得调用者无需关心底层实现类的逻辑。
6.2 getInstance
【功能】获取 VFS
类实例。
【源码】
public static VFS getInstance() {
// 如果单例已经初始化了直接返回
if (instance != null) {
return instance;
}
// 添加实现类到 impls 列表中,并且用户实现类优先于内置实现类
List<Class<? extends VFS>> impls = new ArrayList<Class<? extends VFS>>();
impls.addAll(USER_IMPLEMENTATIONS);
impls.addAll(Arrays.asList((Class<? extends VFS>[]) IMPLEMENTATIONS));
// 遍历每个实现类知道找到合法的实现类
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()) {
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());
}
// 给单例赋值
VFS.instance = vfs;
return VFS.instance;
}
【解析】
这里有两点要注意的地方,第一是单例采用懒加载的模式,但是由于 instance
被声明为 static
所以不需要额外加锁来保证线程安全,这是一种优雅的写法值得学习;第二是实例化对象时,会优先选择用户实现类实例化,因为在不同的环境中可能会使用不同的,为了检验 VFS 的运行环境是否合法,还需要调用 #isValid()
方法校验,正常实例化且校验通过后,赋值给单例并返回。
6.3 反射操作
VFS 还提供了反射操作相关方法,如下:
// 1. 通过类的全限定名加载类
protected static Class<?> getClass(String className) {
try {
return Thread.currentThread().getContextClassLoader().loadClass(className);
} catch (ClassNotFoundException e) {
if (log.isDebugEnabled()) {
log.debug("Class not found: " + className);
}
return null;
}
}
// 2. 通过方法名和参数类型获取类方法 Method 对象
protected static Method getMethod(Class<?> clazz, String methodName, Class<?>... parameterTypes) {
if (clazz == null) {
return null;
}
try {
return clazz.getMethod(methodName, parameterTypes);
} catch (SecurityException e) {
log.error("Security exception looking for method " + clazz.getName() + "." + methodName + ". Cause: " + e);
return null;
} catch (NoSuchMethodException e) {
log.error("Method not found " + clazz.getName() + "." + methodName + "." + methodName + ". Cause: " + e);
return null;
}
}
// 3. 通过反射调用类方法
protected static <T> T invoke(Method method, Object object, Object... parameters)
throws IOException, RuntimeException {
try {
return (T) method.invoke(object, parameters);
} catch (IllegalArgumentException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
if (e.getTargetException() instanceof IOException) {
throw (IOException) e.getTargetException();
} else {
throw new RuntimeException(e);
}
}
}
6.4 list
【功能】找到指定路径下的所有类对象,并以字符串形式返回类的路径。
【源码与注解】
public List<String> list(String path) throws IOException {
// 保存加载到的类路径的列表
List<String> names = new ArrayList<String>();
// (1)调用 #getResources(String path) 加载指定路径下的资源,返回值是一个URL对象列表
for (URL url : getResources(path)) {
// (2)解析URL对象中的类并添加到names中
names.addAll(list(url, path));
}
return names;
}
- (1)调用
#getResources(String path)
加载指定路径下的资源,返回值是一个URL对象列表,该方法就是使用类加载器去加载资源,如下:
protected static List<URL> getResources(String path) throws IOException {
return Collections.list(Thread.currentThread().getContextClassLoader().getResources(path));
}
- (2)解析URL下所有类路径并添加到names中,调用
#list(URL url, String forPath)
方法实现,在 VFS 中该方法是抽象方法,具体实现逻辑取决于 VFS 的子类。
protected abstract List<String> list(URL url, String forPath) throws IOException;
6.5 DefaultVFS
DefaultVFS
是 VFS 的默认实现子类,它的 #list
方法的实现中,主要分两个方向,一种是直接从提供的类路径中加载类;另一种是从 Jar 路径中加载类,此时会先加载路径中的 jar 包,再加载 jar 包中的资源列表。
6.5.1 list
@Override
public List<String> list(URL url, String path) throws IOException {
InputStream is = null;
try {
List<String> resources = new ArrayList<String>();
// 尝试从 URL 路径中找到 JAR 包文件,文件中会包含需要加载的资源
// (1)如果找到了jar包,调用 #listResources 加载资源
URL jarUrl = findJarForResource(url);
if (jarUrl != null) {
is = jarUrl.openStream();
if (log.isDebugEnabled()) {
log.debug("Listing " + url);
}
resources = listResources(new JarInputStream(is), path);
}
else {
List<String> children = new ArrayList<String>();
try {
// (2)在有些情况下 URL 引用的资源实际上并不是一个 jar,但是打开一个URL的连接
// 返回的流对象是一个JAR的流
if (isJar(url)) {
// Some versions of JBoss VFS might give a JAR stream even if the resource
// referenced by the URL isn't actually a JAR
is = url.openStream();
JarInputStream jarInput = new JarInputStream(is);
if (log.isDebugEnabled()) {
log.debug("Listing " + url);
}
for (JarEntry entry; (entry = jarInput.getNextJarEntry()) != null;) {
if (log.isDebugEnabled()) {
log.debug("Jar entry: " + entry.getName());
}
children.add(entry.getName());
}
jarInput.close();
}
else {
// (3)最常见的情况,通常是加载工程中的资源,不用去加载jar包
// 这里可能有两种情况:
// (3.1) url 指向的可能是一个路径,那么下面的line就是每个资源文件名
// (3.2) url 指向的可能是一个文件,那么下面的line就是文件中每行的文本内容
is = url.openStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
List<String> lines = new ArrayList<String>();
for (String line; (line = reader.readLine()) != null;) {
if (log.isDebugEnabled()) {
log.debug("Reader entry: " + line);
}
lines.add(line);
// (3.3)如果line为文件文本,则这里不会加载到内容,清空lines,并跳出循环
// lines是用来保存资源文件名的列表
if (getResources(path + "/" + line).isEmpty()) {
lines.clear();
break;
}
}
if (!lines.isEmpty()) {
if (log.isDebugEnabled()) {
log.debug("Listing " + url);
}
children.addAll(lines);
}
}
} catch (FileNotFoundException e) {
/*
* For file URLs the openStream() call might fail, depending on the servlet
* container, because directories can't be opened for reading. If that happens,
* then list the directory directly instead.
*/
if ("file".equals(url.getProtocol())) {
File file = new File(url.getFile());
if (log.isDebugEnabled()) {
log.debug("Listing directory " + file.getAbsolutePath());
}
if (file.isDirectory()) {
if (log.isDebugEnabled()) {
log.debug("Listing " + url);
}
children = Arrays.asList(file.list());
}
}
else {
// No idea where the exception came from so rethrow it
throw e;
}
}
// (3.4)给 url 路径补上 /
String prefix = url.toExternalForm();
if (!prefix.endsWith("/")) {
prefix = prefix + "/";
}
// (3.5)child指向路径有可能是一个目录,也是一个文件
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) {
// Ignore
}
}
}
}
【解析】
- (1)调用
#listResources
加载资源,试从 URL 路径中找到 JAR 包文件,文件中会包含需要加载的资源。 - (2)在有些情况下 URL 引用的资源实际上并不是一个 jar,但是打开一个URL的连接返回的流对象是一个JAR的流。
- (3)最常见的情况,通常是加载工程中的资源,不用去加载jar包
- (3.1) url 指向的可能是一个路径,那么下面的line就是每个资源文件名。
- (3.2)url 指向的可能是一个文件,那么下面的line就是文件中每行的文本内容。
- (3.3)如果line为文件文本,则这里不会加载到内容,清空
lines
,并跳出循环。 - (3.4)给 url 路径补上 /。
- (3.5)
child
指向路径有可能是一个目录,也是一个文件,将child
添加到要返回的resources
列表中,再递归调用#list
方法加载子目录下的资源,如果文件中的每行存储了指向资源的路径列表,也会加载对应的资源。
6.5.2 findJarForResource
【功能】如果URL中有指向一个Jar文件资源,则返回该Jar Resource,否则返回null。
protected URL findJarForResource(URL url) throws MalformedURLException {
if (log.isDebugEnabled()) {
log.debug("Find JAR URL: " + url);
}
// 这段代码看起来比较神奇,虽然看起来没有 break 的条件,但是是通过 MalformedURLException 异常进行
// 正如上面英文注释,如果 URL 的文件部分本身就是 URL ,那么该 URL 可能指向 JAR
try {
for (;;) {
url = new URL(url.getFile());
if (log.isDebugEnabled()) {
log.debug("Inner URL: " + url);
}
}
} catch (MalformedURLException e) {
// This will happen at some point and serves as a break in the loop
}
// 找到 url 中的jar文件的路径部分
StringBuilder jarUrl = new StringBuilder(url.toExternalForm());
int index = jarUrl.lastIndexOf(".jar");
if (index >= 0) {
jarUrl.setLength(index + 4);
if (log.isDebugEnabled()) {
log.debug("Extracted JAR URL: " + jarUrl);
}
}
else {
if (log.isDebugEnabled()) {
log.debug("Not a JAR: " + jarUrl);
}
return null;
}
// 创建jar文件路径对应的URL对象,并测试是否为一个真实的jar
try {
URL testUrl = new URL(jarUrl.toString());
if (isJar(testUrl)) {
return testUrl;
}
else {
// WebLogic fix: check if the URL's file exists in the filesystem.
if (log.isDebugEnabled()) {
log.debug("Not a JAR: " + jarUrl);
}
jarUrl.replace(0, jarUrl.length(), testUrl.getFile());
File file = new File(jarUrl.toString());
// 处理路径编码问题
if (!file.exists()) {
try {
file = new File(URLEncoder.encode(jarUrl.toString(), "UTF-8"));
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("Unsupported encoding? UTF-8? That's unpossible.");
}
}
// 处理完编码问题再次再次确认文件是否存在
if (file.exists()) {
if (log.isDebugEnabled()) {
log.debug("Trying real file: " + file.getAbsolutePath());
}
testUrl = file.toURI().toURL();
if (isJar(testUrl)) {
return testUrl;
}
}
}
} catch (MalformedURLException e) {
log.warn("Invalid JAR URL: " + jarUrl);
}
if (log.isDebugEnabled()) {
log.debug("Not a JAR: " + jarUrl);
}
return null;
}
6.5.3 listResources
【功能】遍历 Jar Resources。
protected List<String> listResources(JarInputStream jar, String path) throws IOException {
// Include the leading and trailing slash when matching names
// 前后补齐 /
if (!path.startsWith("/")) {
path = "/" + path;
}
if (!path.endsWith("/")) {
path = path + "/";
}
// 遍历条目并收集以请求路径开头的条目
// Iterate over the entries and collect those that begin with the requested path
List<String> resources = new ArrayList<String>();
for (JarEntry entry; (entry = jar.getNextJarEntry()) != null;) {
if (!entry.isDirectory()) {
// Add leading slash if it's missing
String name = entry.getName();
if (!name.startsWith("/")) {
name = "/" + name;
}
// Check file name
if (name.startsWith(path)) {
if (log.isDebugEnabled()) {
log.debug("Found resource: " + name);
}
// Trim leading slash
resources.add(name.substring(1));
}
}
}
return resources;
}
6.5.4 isJar
【功能】判断一个 URL 指向的资源是否是一个Jar资源。
protected boolean isJar(URL url) {
return isJar(url, new byte[JAR_MAGIC.length]);
}
protected boolean isJar(URL url, byte[] buffer) {
InputStream is = null;
try {
is = url.openStream();
// 根据Jar包前几个特殊的标识符,判断文件是否是一个Jar
is.read(buffer, 0, JAR_MAGIC.length);
if (Arrays.equals(buffer, JAR_MAGIC)) {
if (log.isDebugEnabled()) {
log.debug("Found JAR: " + url);
}
return true;
}
} catch (Exception e) {
// Failure to read the stream means this is not a JAR
} finally {
if (is != null) {
try {
is.close();
} catch (Exception e) {
// Ignore
}
}
}
return false;
}
6.6 测试案例
6.6.1 案例1
@Test
public void test_listProjResource() throws IOException {
VFS vfs = VFS.getInstance();
List<String> resources = vfs.list("org/apache/ibatis");
for (String resource : resources) {
System.out.println(resource);
}
}
在 VFS 中会根据传入路径加载 URL 对象列表,再调用 DefaultVFS.list
处理。
很明显这个 url 指向的不是一个 jar resource,看看
#findJarForResource
的处理。这里返回 null,进入下面的分支,就会调用
#isJar
判断是否 url 是否为一个 jar 文件。不匹配,进入下面分支的第二个分支,加载 url 对应的目录资源下的 .class 文件。
得到由包路径和class文件组成的资源字符串,这也是
VFS.list
最后返回的资源字符串列表中返回字符串的形式。除了指向class文件的资源字符串,字符串还可能是目录和其他文件资源,如下:
6.6.2 案例2
@Test
public void test_listLibResource() throws IOException {
VFS vfs = VFS.getInstance();
List<String> resources = vfs.list("net/sf/cglib/beans");
for (String resource : resources) {
System.out.println(resource);
}
}
如果要加载的资源路径是 Jar 包里面的,则要先加载Jar包,类加载器加载该路径时,可能得到带 jar 包的绝对路径的 URL。
将 URL 路径
file:/E:/maven_repository/cglib/cglib/3.2.2/cglib-3.2.2.jar!/net/sf/cglib/beans
中的 file:/E:/maven_repository/cglib/cglib/3.2.2/cglib-3.2.2.jar
解析出来,创建一个指向 Jar 包的新的 URL 对象。加载 Jar 包中的资源并过滤掉非指定路径前缀的资源。
逐个资源处理判断,最后放到列表返回,测试案例的结果如下:
还有最后一个分支,是URL 引用的资源实际上并不是一个 jar,但是打开一个URL的连接返回的流对象是一个JAR的流,常规资源加载就上面两种情况,这种可能是通过网络去加载一些资源。
网友评论