-
- 一源码分析
- 二类加载器ClassLoader
- 1 双亲委派模型
- 2 Java 中的类加载器
- 三Tomcat 类加载器架构
- 四解决 ObjectclassgetResourceAsStream 问题
- 六总结
- 七参考
最近遇到一个问题:tomcat 服务器中通过
Object.class.getResourceAsStream("ss.properties")
加载 webapps 下某 webapp 中某个文件,服务器上可以正常加载,本地运行却不能正确加载,提示找不到文件。
在接下来排查问题的过程中,发现服务器和本地的配置不同:
- 服务器:通过 tomcat 的
脚本将bin/setclasspath.sh
所在的目录设置为了 classpath;ss.properties
- 本地:将
放置在了ss.properties
文件夹下,也就是 WAR 文件格式规定的 classpath 目录之一。webapps/webapp1/WEB-INF/classes
为什么这样配置的不同,就导致了文件不能被正确加载。
一、源码分析
为了找出原因,我们第一步看
Object.class.getResourceAsStream("ss.properties")
的源码:
// Class.java
public InputStream getResourceAsStream(String name) {
name = resolveName(name);
ClassLoader cl = getClassLoader0();// 获取加载该Class的ClassLoader
if (cl==null) {
// A system class.
return ClassLoader.getSystemResourceAsStream(name);
}
return cl.getResourceAsStream(name);
}
从 javadoc 文档和源码中可以看出:
-
代理给了加载该 class 的 ClassLoader 去实现,调用Class.getResourceAsStream
;ClassLoader.getResourceAsStream
- 如果该类的 ClassLoader 为 null,说明该 class 一个系统 class,所以委托给
。ClassLoader.getSystemResourceAsStream
通过源码的分析,可以看出来加载资源的动作和该类的类加载器有关,所以下面我们需要介绍什么是类加载器。
二、类加载器(ClassLoader)
我们都知道 Java 文件被运行,第一步,需要通过
javac
编译器编译为 class 文件;第二步,JVM 运行 class 文件,实现跨平台。而 JVM 虚拟机第一步肯定是 加载 class 文件,所以,类加载器实现的就是(来自《深入理解Java虚拟机》):
通过一个类的全限定名来获取描述此类的二进制字节流
类加载器有几个重要的特性:
- 每个类加载器都有自己的预定义的搜索范围,用来加载 class 文件;
- 每个类和加载它的类加载器共同确定了这个类的唯一性,也就是说如果一个 class 文件被不同的类加载器加载到了 JVM 中,那么这两个类就是不同的类,虽然他们都来自同一份 class 文件;
- 双亲委派模型。
2.1 双亲委派模型
- 所有的类加载器都是有层级结构的,每个类加载器都有一个父类类加载器(通过组合实现,而不是继承),除了启动类加载器(Bootstrap ClassLoader);
- 当一个类加载器接收到一个类加载请求时,首先将这个请求委派给它的父加载器去加载,所以每个类加载请求最终都会传递到顶层的启动类加载器,如果父加载器无法加载时,子类加载器才会去尝试自己去加载;
通过双亲委派模型就实现了类加载器的三个特性:
- 委派(delegation):子类加载器委派给父类加载器加载;
- 可见性(visibility):子类加载器可访问父类加载器加载的类,父类不能访问子类加载器加载的类;
- 唯一性(uniqueness):可保证每个类只被加载一次,比如
类是被 Bootstrap ClassLoader 加载的,因为有了双亲委派模型,所有的 Object 类加载请求都委派到了 Bootstrap ClassLoader,所以保证了只被加载一次。Object
以上就是类加载器的一些特性,那么在 Java 中类加载器是如何实现的呢?
2.2 Java 中的类加载器
从 JVM 虚拟机的角度来看,只存在两种不同的类加载器:
- 启动类加载器(Bootstrap ClassLoader),是虚拟机自身的一部分;
- 所有其他的类加载器,独立于虚拟机外部,都继承自抽象类
。java.lang.ClassLoader
而绝大多数 Java 应用都会用到如下 3 中系统提供的类加载器:
- 启动类加载器(Bootstrap/Primordial/NULL ClassLoader):顶层的类加载器,没有父类加载器。负责加载 /lib 目录下的,或则被 -Xbootclasspath 参数所指定路径中的,并被 JVM 识别的(仅按文件名识别,如 rt.jar,名字不符合的类库即使放在 lib 目录也不会被加载)类库加载到虚拟机内存中。所有被 Bootstrap classloader 加载的类,它的
方法返回的都是Class.getClassLoader
,所以也称作 NULL ClassLoader。null
- 扩展类加载器(Extension CLassLoader):由
实现,负责加载sun.misc.Launcher$ExtClassLoader
目录下,或被<JAVA_HOME>/lib/ext
系统变量所指定的目录下的所有类库;java.ext.dirs
- 应用程序类加载器(Application/System ClassLoader):由
实现。它是sun.misc.Launcher$AppClassLoader
方法的默认返回值,所以也称为系统类加载器(System ClassLoader)。它负责加载 classpath 下所指定的类库,如果应用程序没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。ClassLoader.getSystemClassLoader()
如下,就是 Java 程序中的类加载器层级结构图:
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsICdzFWRoRXdvN1LclHdpZXYyd2LcBzNvwVZ2x2bzNXak9CX90TQNNkRrFlQKBTSvwFbslmZvwFMwQzLcVmepNHdu9mZvwFVywUNMZTY18CX052bm9CX9ElaNlXWq10MZR1T4VkekZXUYpVd1kmYr50MZV3YyI2cKJDT29GRjBjUIF2LcRHelR3LcJzLctmch1mclRXY39TNxQDMygDMwIDNxEDM3EDMy8CX0Vmbu4GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.jpg)
以上,我们介绍了 Java 系统的类加载器,但是我们的应用是运行在 tomcat 中的,那么我们当然也应该探究 tomcat 是如何加载类的。
三、Tomcat 类加载器架构
根据 Class Loader HOW-TO 中的描述,tomcat7 主要有如下的类加载器层级:
Bootstrap
|
Extension
|
System
|
Common
/ \
Webapp1 Webapp2 ...
从图中可以看出,除了系统类加载器(Bootstrap、Extension、System),tomcat 还自定义了自己的类加载器(Common、Webapp等)。
- Bootstrap 和 Extension:和前面介绍 Java 系统类加载器一样,这里不再赘述;
- System:从
系统变量指定的目录中加载类库。该加载器加载的类对 tomcat 本身和 web 应用都可见。但是,标准的 tomcat 启动脚本(CLASSPATH
or$CATALINA_HOME/bin/catalina.sh
)都会忽略系统变量%CATALINA_HOME%\bin\catalina.bat
的值,而会使用如下的类库来创建 System 类加载器(CLASSPATH
脚本设置的setclasspath
变量对 tomcat 有用):CLASSPATH
- $CATALINA_HOME/bin/bootstrap.jar
- $CATALINA_BASE/bin/tomcat-juli.jar 或 $CATALINA_HOME/bin/tomcat-juli.jar
- $CATALINA_HOME/bin/commons-daemon.jar
- Common:通过该类加载器加载的类库可被 Tomcat 和所有的 Web 应用共同使用。该类加载器的搜索位置是通过 $CATALINA_BASE/conf/catalina.properties 文件中的
属性指定的,默认包括如下位置:common.loader
-
下未打包的类和资源;$CATALINA_BASE/lib
-
下的 jar 包;$CATALINA_BASE/lib
-
下未打包的类和资源;$CATALINA_HOME/lib
-
下的 jar 包。$CATALINA_HOME/lib
-
- WebappX:每个 Web 应用自己的类加载器,能够加载
和/WEB-INF/classes
下的类和资源。能够被此 Web 应用使用,但对其他 Web 应用不可见。/WEB-INF/lib
对于 WebappX 类加载器,它并不是双亲委派模型的。当 WebappX 类接收到一个类加载请求时,它会先尝试自己去加载,自己不能加载时,再委派给父类加载器。但是,例外就是JRE 相关的类不能被覆盖。除了,WebappX 类加载器,其他的类加载器都符合通常的双亲委派模型。
四、解决 Object.class.getResourceAsStream 问题
有了以上有关类加载器的知识,现在应该能够解决为什么配置不同,导致文件不能被正确加载的问题了。
第一步,看源码:
// Class.java
public InputStream getResourceAsStream(String name) {
name = resolveName(name);
ClassLoader cl = getClassLoader0();// 获取加载该Class的ClassLoader
if (cl==null) {
// A system class.
return ClassLoader.getSystemResourceAsStream(name);
}
return cl.getResourceAsStream(name);
}
// ClassLoader.java
public static InputStream getSystemResourceAsStream(String name) {
URL url = getSystemResource(name);
try {
return url != null ? url.openStream() : null;
} catch (IOException e) {
return null;
}
}
public static URL getSystemResource(String name) {
ClassLoader system = getSystemClassLoader();// 获取 System ClassLoader
if (system == null) {
return getBootstrapResource(name);
}
return system.getResource(name);
}
现在,我们来分析整个加载过程:
- 获取该类的类加载器,判断是否为 null,为 null 说明是 JRE 相关的类(此处是
,所以类加载器是 null),那么委派给Object
;ClassLoader.getSystemResourceAsStream(String)
- 获取 System 类加载器,通过 System 类加载器去加载文件;
现在,我们知道问题就出在这个 System 类加载器上:
- 服务器:通过设置 CLASSPATH 变量,所以 System 类加载器能够找到
;ss.properties
- 本地:本地环境下,
最终是放在 Web 应用下的ss.properties
文件夹下,不能被 System 类加载器获取到,所以加载失败。/WEB-INF/classes
解决方案:通过
getClass().getResouceAsStream(String)
去加载资源,这样首先就在 WebappX 类加载器中去寻找资源,所以无论如何都能找到。
六、总结
通过解决一个文件加载的问题,学习了 Java 应用的类加载器和 Tomcat 的类加载器架构,了解了加载的底层原理,很有成就感。
在解决该问题的过程中,有几点小心得:
- 出现问题时,从源头找问题,比如从源码去看加载的逻辑;
- 学会看源码,可能很多问题,一看源码就解决了;
- 多 Google,看资料时融汇贯通,比如解决这个问题时,最主要的帮助来自于:1、源码;2、《深入理解 Java 虚拟机》;3、Tomcat ClassLoader 的文档。
七、参考
- 《深入理解 Java 虚拟机》
- Class Loader HOW-TO