核心类介绍
在使用 ClassLoader
加载资源时,必须会涉及到以下几个核心组件:
Launcher
Launcher.AppClassLoader
,App类加载器Launcher.ExtClassLoader
,Ext类加载器Launcher.BootClassPathHolder
,Boot类加载器
URLClassLoader
,在ClassLoader
基础上,增加了资源查找能力URLClassPath
,类路径对象URLClassPath.Loader
URLClassPath.JarLoader
,用来处理Jar包类型的资源URLClassPath.FileLoader
,用来处理File类型的资源
资源查找原理
资源初始化
我们通过类加载器查找资源时,所有的资源都是来源于类路径下的。常见的类路径有以下几个:
sun.boot.class.path
,BootClassLoader
负责的类路径java.ext.dirs
,ExtClassLoader
负载的类路径java.class.path
,AppClassLoader
负责的类路径。这个比较常见,就是我们通过-classpath
指定的路径
这些类路径在各个类加载器中是以 URLClassPath
的形式存在,每个 URLClassPath
可以包含多个资源路径,每个资源路径都会有一个对应的加载器( Loader
)。除此之外,它还有 lmap
、 loaders
、 path
、 url
等重要的属性:
lmap
:保存URL
和对应加载器Loader
的映射loaders
:保存Loader
的列表,和urls
的下标一一对应。urls[0]
的Loader
是loaders[0]
。path
:保存URL
的列表,这个列表通常不会更改urls
:保存URL
的列表,这个列表记录了还没有处理的URL
,一开始数量是和path
一样;但是在生成Loader
时,会一个个被弹出来,主要是为了保证一个urls
的下标对应loaders
的下标。
URLClassPath
是在初始化各个类加载器的时候创建的,比如拿 ExtClassLoader
的构造器来说:
// ExtClassLoader 继承 URLClassLoader
public ExtClassLoader(File[] dirs) throws IOException {
// arg1: java.ext.dirs 属性指定的路径
// arg2: 父加载器,因为Ext之上是Boot,所以这里传null
// arg3: 资源加载协议处理器工厂,用来创建对应协议的Handler
// 比如我要处理jar:开头的路径,就用工厂创建sun.net.www.protocol.jar.Handler
// 比如我要处理file:开头的路径,就用工厂创建sun.net.www.protocol.file.Handler
// 具体的可以看最后的拓展
super(getExtURLs(dirs), null, factory);
// 无关紧要的内容
SharedSecrets.getJavaNetAccess().
getURLClassPath(this).initLookupCache(this);
}
// 处理外部的资源路径,总之拿到的资源路径肯定都来自于 java.ext.dirs
private static File[] getExtDirs() {
String s = System.getProperty("java.ext.dirs");
File[] dirs;
if (s != null) {
StringTokenizer st =
new StringTokenizer(s, File.pathSeparator);
int count = st.countTokens();
dirs = new File[count];
for (int i = 0; i < count; i++) {
dirs[i] = new File(st.nextToken());
}
} else {
dirs = new File[0];
}
return dirs;
}
接着我们看一下 URLClassLoader
是怎么处理这些资源路径的:
public URLClassLoader(URL[] urls, ClassLoader parent,
URLStreamHandlerFactory factory) {
// 这个和我们的主题无关
super(parent);
// 安全管理器,和我们的主题也没多大关系
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkCreateClassLoader();
}
acc = AccessController.getContext();
// 重点来啦~~用urls、factory、acc构建了一个URLClassPath
ucp = new URLClassPath(urls, factory, acc);
}
URLClassPath
的加载器如下所示:
public URLClassPath(URL[] urls,
URLStreamHandlerFactory factory,
AccessControlContext acc) {
// 记录URL列表,这个列表用来保持原始数据
for (int i = 0; i < urls.length; i++) {
path.add(urls[i]);
}
// 记录URL列表,这个列表会根据处理进程发生变动
push(urls);
// 构建jar协议的处理器
if (factory != null) {
jarHandler = factory.createURLStreamHandler("jar");
}
// 是否启用安全管理器。和我们目前的主题无关
if (DISABLE_ACC_CHECKING)
this.acc = null;
else
this.acc = acc;
}
至此, ExtClassLoader
的资源就准备好了, AppClassLoader
的处理逻辑和这个一样,唯独 BootClassLoader
有点特别,因为它是在 JVM
层被初始化的,在应用层没有具体的类,全都是用 null
表示的,所以为了也能加载到 BootClassLoader
的资源,在 Launcher
类里还有一个类用来保存 BootClassLoader
的类路径—— BootClassPathHolder
:
// 用来保存BootClassLoader的类路径
private static class BootClassPathHolder {
static final URLClassPath bcp;
// 在被使用的时候自动创建 bcp
static {
URL[] urls;
if (bootClassPath != null) {
urls = AccessController.doPrivileged(
new PrivilegedAction<URL[]>() {
public URL[] run() {
File[] classPath = getClassPath(bootClassPath);
int len = classPath.length;
Set<File> seenDirs = new HashSet<File>();
for (int i = 0; i < len; i++) {
File curEntry = classPath[i];
// Negative test used to properly handle
// nonexistent jars on boot class path
if (!curEntry.isDirectory()) {
curEntry = curEntry.getParentFile();
}
if (curEntry != null && seenDirs.add(curEntry)) {
MetaIndex.registerDirectory(curEntry);
}
}
return pathToURLs(classPath);
}
}
);
} else {
urls = new URL[0];
}
bcp = new URLClassPath(urls, factory, null);
bcp.initLookupCache(null);
}
}
现在我们通过 sun.misc.Launcher#getBootstrapClassPath()
也能获取到 BootClassLoader
的资源路径
// sun.misc.Launcher#getBootstrapClassPath
public static URLClassPath getBootstrapClassPath() {
return BootClassPathHolder.bcp;
}
资源封装
基于 ClassLoader
的资源查找并不是立刻马上查找的,而是需要使用时才会真正去查找。这一阶段主要就是对资源进行封装,通过 Enumeration
把资源路径暴露给调用者。简单来说就是“懒查找”(意思类比一下懒加载)。下面是资源封装的方法调用示意图:
点击查看【processon】
当我们调用
XX.class.getClassLoader().getResources()
时,首先会通过AppClassLoader
的getResources()
去查询,所有类加载器的getResources()
都是使用的父类ClassLoader
的方法:// java.lang.ClassLoader#getResources()
public Enumeration<URL> getResources(String name) throws IOException {
// 构建一个长度为2的数组,一个用来保存父类的资源;另一个保存自己的资源,最后合并到一起返回
@SuppressWarnings("unchecked")
Enumeration<URL>[] tmp = (Enumeration<URL>[]) new Enumeration<?>[2];
// 判断是否存在父加载器,获取父加载器的资源
if (parent != null) {
tmp[0] = parent.getResources(name);
} else {
// 如果父加载器为null,那就获取BootClassPath
tmp[0] = getBootstrapResources(name);
}
// 查找自己的资源路径
tmp[1] = findResources(name);
// 合并到一个Enumeration里,Compound表示组合
return new CompoundEnumeration<>(tmp);
}
AppClassLoader
会调用ExtClassLoader#getResources()
来获取资源,ExtClassLoader#getResources()
方法和上面的一样,都是ClassLoader#getResources()
。因为ExtClassLoader
的parent
为null
,即Boot
。所以会先通过getBootstrapResources()
获取Boot
的资源路径:private static Enumeration<URL> getBootstrapResources(String name)
throws IOException
{
// getBootstrapClassPath() 用来获取Boot的URLClassPath,下称bcp
// 通过bcp#getResources(),获取相关的资源,因为这里获取到的是Enumeration<Resource>
// 下面还需要再封装成Enumeration<URL>才能使用
final Enumeration<Resource> e =
getBootstrapClassPath().getResources(name);
// 将资源封装成Enumeration
return new Enumeration<URL> () {
public URL nextElement() {
return e.nextElement().getURL();
}
public boolean hasMoreElements() {
return e.hasMoreElements();
}
};
}
通过深入源码查看
bcp#getResources()
,我们可以发现它只是单纯的返回了一个Enumeration<Resource>
,并没有做任何解析~其实ExtClassLoader
、AppClassLoader
的思路大体都是这样:// sun.misc.URLClassPath#getResources(java.lang.String, boolean)
public Enumeration<Resource> getResources(final String name,
final boolean check) {
// 把自己的资源通过Enumeration<Resource>暴露出去
// 注意这里返回的是 Enumeration<Resource> !!!!
return new Enumeration<Resource>() {
private int index = 0;
private int[] cache = getLookupCache(name);
private Resource res = null;
// 判断是否存在下一个资源
private boolean next() {
// 如果存在该资源,立刻返回true
if (res != null) {
return true;
} else {
// 资源加载器
Loader loader;
// 通过缓存查找一下是否已经加载过这个资源路径;如果没有会新建一个Loader
while ((loader = getNextLoader(cache, index++)) != null) {
// 通过Loader尝试获取一下指定的资源
res = loader.getResource(name, check);
// 如果获取到了,res!=null,就能返回true
if (res != null) {
return true;
}
}
return false;
}
}
public boolean hasMoreElements() {
return next();
}
public Resource nextElement() {
if (!next()) {
throw new NoSuchElementException();
}
// this.res 后续还要判断用,先设置给r
Resource r = res;
// 然后把res设置为null
res = null;
// 返回r
return r;
}
};
}
疑惑:这里的
Boot
的URLClassPath
为什么要先获取Resource
再转成URL
?不直接findResources()
获取Resource
?随后
ExtClassLoader
会通过URLClassPath#findResources()
获取自己的资源路径,这个方法和URLClassPath#getResources()
差不多,就是前者获取URL
,后者获取Resource
:public Enumeration<URL> findResources(final String name)
throws IOException
{
// 这里调用的是URLClassPath.findResources(),Boot那边调的是getResources()
// 不明白啥意思,但总之都通过这方法获取
final Enumeration<URL> e = ucp.findResources(name, true);
return new Enumeration<URL>() {
private URL url = null;
private boolean next() {
if (url != null) {
return true;
}
do {
// 做了一些安全处理,可以忽略
URL u = AccessController.doPrivileged(
new PrivilegedAction<URL>() {
public URL run() {
if (!e.hasMoreElements())
return null;
return e.nextElement();
}
}, acc);
if (u == null)
break;
url = ucp.checkURL(u);
} while (url == null);
return url != null;
}
public URL nextElement() {
if (!next()) {
throw new NoSuchElementException();
}
URL u = url;
url = null;
return u;
}
public boolean hasMoreElements() {
return next();
}
};
}
最后把父加载器的资源路径和自己的资源路径合并到一起,返回给调用者
AppClassLoader
。接着,AppClassLoader
会去获取自己的资源路径,原理和ExtClassLoader
一样~- 最后把
AppClassLoader
自己的资源路径和父加载器ExtClassLoader
的资源路径(ExtClassLoader
的资源路径已经整合了BootClassLoader
的资源路径)合并到一起返回回去。
获取资源
前面已经把所有的资源封装成一个 Enumeration<URL>
,这一步就是真正的去查询获取资源,先看这样一段代码:
// 资源初始化+封装
Enumeration<URL> resources = ClassLoaderTest.class.getClassLoader().getResources("cn/codeleven");
// 开始真正的查询、获取资源
while(resources.hasMoreElements()){
URL url = resources.nextElement();
System.out.println(url);
}
- 首先去查询是否存在名字为
cn/codeleven
的资源(这里的名称匹配规则后面讲) - 如果有就获取对应的
URL
并打印到控制台,反之继续,直到退出while
循环
下面是 resources
变量的数据结构:
点击查看【processon】
构造Loader
- 当我们通过
hasMoreElements()
方法去查询时,本质上是调用URLClassPath#findResources()
的匿名函数对象的hasMoreElements()
。在next()
方法里,首先要查询获取Loader
: ```java // 正在查询的资源路径的下标 private int index = 0; // 查询指定的name是否有缓存,如果有缓存,会返回一个整数数组,元素表示 指定名字的资源 在所有资源路径里的下标 private int[] cache = getLookupCache(name); // 要返回的url private URL url = null;
public boolean hasMoreElements() { return next(); } private boolean next() { if (url != null) { return true; } else { Loader loader; // index表示当前要查找的资源路径,cache表示ClassLoader缓存下来的资源路径下标 // 如果ClassLoader缓存过 [a.jar, b.jar, c.jar, d.jar],且foo包仅存在于a.jar和c.jar // 那么getLookupCache()会返回 [0, 2] while ((loader = getNextLoader(cache, index++)) != null) { url = loader.findResource(name, check); if (url != null) { return true; } } return false; } }
2. 它首先通过 `getNextLoader()` 去查找当前 `index` 的资源路径对应的 `Loader` :
```java
private synchronized Loader getNextLoader(int[] cache, int index) {
// URLClassLoader如果已经关闭了就没必要查了
if (closed) {
return null;
}
// 如果存在cache,就按cache给的下标去取
if (cache != null) {
// 这里对这个缓存的原理有些疑惑,这一块不太清楚为啥这么写,只能知道是通过缓存查Loader
if (index < cache.length) {
// cache已经给了缓存过 index下标的资源路径的Loader的下标地址
// 所以我只需要通过cache[index]获取Loader的下标地址
Loader loader = loaders.get(cache[index]);
if (DEBUG_LOOKUP_CACHE) {
System.out.println("HASCACHE: Loading from : " + cache[index]
+ " = " + loader.getBaseURL());
}
return loader;
} else {
return null; // finished iterating over cache[]
}
} else {
// 如果没有缓存,就去自己创建一个
return getLoader(index);
}
}
如果缓存里不存在该资源的
Loader
,那么就创建一个Loader
的原理如下所示:private synchronized Loader getLoader(int index) { if (closed) { return null; } // 如果加载器数量没超过我们查询的index,那么说明目前没有加载器。就需要创建一个 while (loaders.size() < index + 1) { // Pop the next URL from the URL stack URL url; synchronized (urls) { if (urls.empty()) { return null; } else { // 从urls属性里获取一个URL url = urls.pop(); } } // 处理URL字符串 String urlNoFragString = URLUtil.urlNoFragString(url); // lmap保存了URL和Loader的关系,判断该URL是否存在对应的Loader if (lmap.containsKey(urlNoFragString)) { continue; } // 若该URL不存在Loader,就创建一个 Loader loader; try { // 根据url开头的协议,决定创建FileLoader还是JarLoader,还是普通的Loader loader = getLoader(url); // 这条语句,只针对JarLoader,其余的Loader返回的都是null // 这是因为Jar包里面还会有其他的资源路径,统统需要取出来,保存到urls里 URL[] urls = loader.getClassPath(); if (urls != null) { push(urls); } } catch (IOException e) { continue; } catch (SecurityException se) { continue; } // 这个也是缓存相关的,不太清楚,C源码一直没找到 validateLookupCache(loaders.size(), urlNoFragString); // 增加loader到loaders loaders.add(loader); // 建立关系 lmap.put(urlNoFragString, loader); } if (DEBUG_LOOKUP_CACHE) { System.out.println("NOCACHE: Loading from : " + index ); } // 获取loader,并返回 return loaders.get(index); }
当
Loader
构造完毕,就可以去通过Loader
查询资源里的东西了。
查询资源
Loader
分为三种:
FileLoader
,加载文件的Loader
JarLoader
,加载Jar包的Loader
Loader
,普通的Loader
我拿 FileLoader
举个例子,这里省掉了中间代码,直接放最后的代码:
Resource getResource(final String name, boolean check) {
final URL url;
try {
// getBaseURL() 表示这个Loader对应的资源,这里就追加了一个 .
URL normalizedBase = new URL(getBaseURL(), ".");
// 将我们查询的name,处理后拼接在 getBaseURL() 后边
url = new URL(getBaseURL(), ParseUtil.encodePath(name, false));
// 通过这个url获取 filename ,查看filename是否以 normalizedBase 开头
// 如果为true,那么没问题,就是这个url;如果为false,说明这个资源路径包含了 “../..”的字符串
if (url.getFile().startsWith(normalizedBase.getFile()) == false) {
// requested resource had ../..'s in path
return null;
}
// 是否要进行安全检查
if (check)
URLClassPath.check(url);
final File file;
// 判断要查找的资源名称里是否包含 “..”,若包含则特殊处理一下
if (name.indexOf("..") != -1) {
file = (new File(dir, name.replace('/', File.separatorChar)))
// 获取不包含..的规范路径
.getCanonicalFile();
if ( !((file.getPath()).startsWith(dir.getPath())) ) {
/* outside of base dir */
return null;
}
} else {
// 构建File对象
file = new File(dir, name.replace('/', File.separatorChar));
}
// 如果这个File对象存在,则返回对应的Resource
if (file.exists()) {
return new Resource() {
public String getName() { return name; };
public URL getURL() { return url; };
public URL getCodeSourceURL() { return getBaseURL(); };
public InputStream getInputStream() throws IOException
{ return new FileInputStream(file); };
public int getContentLength() throws IOException
{ return (int)file.length(); };
};
}
} catch (Exception e) {
return null;
}
return null;
}
到此,如何查询资源也已经讲完了~其余的两个 Loader
,读者有兴趣就自己看看~
资源查找总结
因为资源查找最终都是依靠 Loader
进行的:
FileLoader
,基于ClassPath + 查询的资源路径/名称
查找资源JarLoader
,是通过使用JarFile
来获取的~目前还是空着的,等用到了来补充
总而言之,所有的路径查找都是基于 ClassPath
进行的。
案例1
该方法用来获取类路径中以 ~~`path` 结尾的路径,如果出现相同路径内出现多个 `path` ,选择长度最长的。比如说,我查找 `org/apache` :~~
/G:/workspace/mybatis-3/target/test-classes/org/apache
/G:/workspace/mybatis-3/target/classes/org/apache
file:/E:/OfflineRepository/org/slf4j/slf4j-log4j12/1.7.29/slf4j-log4j12-1.7.29.jar!/org/apache
file:/E:/OfflineRepository/log4j/log4j/1.2.17/log4j-1.2.17.jar!/org/apache
file:/E:/OfflineRepository/org/apache/logging/log4j/log4j-core/2.12.1/log4j-core-2.12.1.jar!/org/apache
file:/E:/OfflineRepository/org/apache/logging/log4j/log4j-api/2.12.1/log4j-api-2.12.1.jar!/org/apache
file:/E:/OfflineRepository/commons-logging/commons-logging/1.2/commons-logging-1.2.jar!/org/apache
file:/E:/OfflineRepository/org/apache/velocity/velocity-engine-core/2.1/velocity-engine-core-2.1.jar!/org/apache
file:/E:/OfflineRepository/org/apache/commons/commons-lang3/3.8.1/commons-lang3-3.8.1.jar!/org/apache
file:/E:/OfflineRepository/org/apache/commons/commons-compress/1.19/commons-compress-1.19.jar!/org/apache
类似 ~~`file:/E:/OfflineRepository/log4j/log4j/1.2.17/log4j-1.2.17.jar!/org/apache` 就仅仅查找到最后是 `org/apache` 就结束了。而对于 `file:/E:/OfflineRepository/org/apache/logging/log4j/log4j-core/2.12.1/log4j-core-2.12.1.jar!/org/apache` 这种路径,包含两种满足结尾是 `org/apache` 的情况:~~
~~file:/E:/OfflineRepository/org/apache~~
~~file:/E:/OfflineRepository/org/apache/logging/log4j/log4j-core/2.12.1/log4j-core-2.12.1.jar!/org/apache~~
但是要选一个更加“准确”(长)的,所以就是第二种情况
通过今天的学习,彻底颠覆了以往对 ClassLoader.getResources()
的理解,之所以只获取到了file:/E:/OfflineRepository/org/apache/logging/log4j/log4j-core/2.12.1/log4j-core-2.12.1.jar!/org/apache
是因为log4j-core-2.12.1.jar
这个库是在 ClassPath
里面的,所以,去查找 /org/apache
时,能找到 file:/E:/OfflineRepository/org/apache/logging/log4j/log4j-core/2.12.1/log4j-core-2.12.1.jar!/org/apache
而不是 file:/E:/OfflineRepository/org/apache
!
案例2
在Spring
的每个模块下(比如spring-aop.jar
、spring-webmvc.jar
等)都会包含一个文件:META-INF/spring.handlers
它的内容如下所示:
http\://www.springframework.org/schema/aop=org.springframework.aop.config.AopNamespaceHandler
该文件保存了该模块所属的命名空间,以及该命名空间所需的标签解析器。在解析配置文件时会用到ClassLoader
来加载资源。
以下是 Spring
加载所有 META-INF/spring.handlers
的代码:
// BeanDefinitionParserDelegate#parseCustomElement(Element, BeanDefinition)
public BeanDefinition parseCustomElement(Element ele, @Nullable BeanDefinition containingBd) {
...
// 根据命名空间找到对应的NamespaceHandlerspring
NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);
...
}
// DefaultNamespaceHandlerResolver#resolve()
public NamespaceHandler resolve(String namespaceUri) {
// 获取所有已经配置好的handler映射
Map<String, Object> handlerMappings = getHandlerMappings();
...
}
// DefaultNamespaceHandlerResolver#getHandlerMappings()
private Map<String, Object> getHandlerMappings() {
...
// this.handlerMappingsLocation在构造函数中已经被初始化为META-INF/Spring.handlers
Properties mappings = PropertiesLoaderUtils.loadAllProperties(this.handlerMappingsLocation, this.classLoader)
...
}
// PropertiesLoaderUtils#loadAllProperties(String, ClassLoader)
public static Properties loadAllProperties(String resourceName, @Nullable ClassLoader classLoader) throws IOException {
Enumeration<URL> urls = (classLoaderToUse != null ? classLoaderToUse.getResources(resourceName) :
ClassLoader.getSystemResources(resourceName));
Properties props = new Properties();
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
URLConnection con = url.openConnection();
ResourceUtils.useCachesIfNecessary(con);
try (InputStream is = con.getInputStream()) {
if (resourceName.endsWith(XML_FILE_EXTENSION)) {
props.loadFromXML(is);
}
else {
props.load(is);
}
}
}
return props;
}
Loader
会加载所有类路径上的文件路径(如果是jar
包也会进入分析),将满足条件的路径筛选出来,最后用Properties
进行加载。
拓展
URLStreamHandlerFactory
这货就是一个接口,定义了获取资源流 URLStreamHandler
的方法:
public interface URLStreamHandlerFactory {
URLStreamHandler createURLStreamHandler(String protocol);
}
这个具体实现也比较简单的,在 Launcher
里有一个 Factory
:
private static class Factory implements URLStreamHandlerFactory {
// 包前缀,可以去查一下这个包,这个包下面有常见协议的所有处理器
private static String PREFIX = "sun.net.www.protocol";
// 实现了接口,根据协议获取处理器
public URLStreamHandler createURLStreamHandler(String protocol) {
// 根据协议,定位对应的Handler
String name = PREFIX + "." + protocol + ".Handler";
try {
Class<?> c = Class.forName(name);
return (URLStreamHandler)c.newInstance();
} catch (ReflectiveOperationException e) {
throw new InternalError("could not load " + protocol +
"system protocol handler", e);
}
}
}
MetaIndex
今天学习 ClassLoader
加载资源原理时,发现了 MetaIndex
这个类,然后得知了在 jdk
的库里有这么一个文件 meta-index
:
这文件里面保存当前路径下( java/jre8/lib
),所有 Jar
包提供的包名信息:
% VERSION 2
% WARNING: this file is auto-generated; do not edit
% UNSUPPORTED: this file and its format may change and/or
% may be removed in a future release
# charsets.jar
sun/nio
sun/awt
# jce.jar
javax/crypto
sun/security
META-INF/ORACLE_J.RSA
META-INF/ORACLE_J.SF
# jfr.jar
oracle/jrockit/
jdk/jfr
com/oracle/jrockit/
! jsse.jar
sun/security
com/sun/net/
! management-agent.jar
@ resources.jar
com/sun/java/util/jar/pack/
META-INF/services/sun.util.spi.XmlPropertiesProvider
META-INF/services/javax.print.PrintServiceLookup
com/sun/corba/
META-INF/services/javax.sound.midi.spi.SoundbankReader
...
如果没有接触过 ClassLoader
的,基本都不知道这货的作用。这 meta-index
文件是用来提供索引的,能帮助快速检索包。
ClassLoader的findResources和getResources的区别
getResources()
在查找资源时会结合父加载器的资源findResources()
仅仅只查找当前类加载器的资源