类加载器与 Class.getResourceAsStream 问题解决
来源:互联网 发布:公司数据安全管理制度 编辑:程序博客网 时间:2024/06/17 13:50
- 一源码分析
- 二类加载器ClassLoader
- 1 双亲委派模型
- 2 Java 中的类加载器
- 三Tomcat 类加载器架构
- 四解决 ObjectclassgetResourceAsStream 问题
- 六总结
- 七参考
最近遇到一个问题:tomcat 服务器中通过 Object.class.getResourceAsStream("ss.properties")
加载 webapps 下某 webapp 中某个文件,服务器上可以正常加载,本地运行却不能正确加载,提示找不到文件。
在接下来排查问题的过程中,发现服务器和本地的配置不同:
- 服务器:通过 tomcat 的
bin/setclasspath.sh
脚本将ss.properties
所在的目录设置为了 classpath; - 本地:将
ss.properties
放置在了webapps/webapp1/WEB-INF/classes
文件夹下,也就是 WAR 文件格式规定的 classpath 目录之一。
为什么这样配置的不同,就导致了文件不能被正确加载。
一、源码分析
为了找出原因,我们第一步看 Object.class.getResourceAsStream("ss.properties")
的源码:
// Class.javapublic 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.getResourceAsStream
代理给了加载该 class 的 ClassLoader 去实现,调用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):可保证每个类只被加载一次,比如
Object
类是被 Bootstrap ClassLoader 加载的,因为有了双亲委派模型,所有的 Object 类加载请求都委派到了 Bootstrap ClassLoader,所以保证了只被加载一次。
以上就是类加载器的一些特性,那么在 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
,所以也称作 NULL ClassLoader。 - 扩展类加载器(Extension CLassLoader):由
sun.misc.Launcher$ExtClassLoader
实现,负责加载<JAVA_HOME>/lib/ext
目录下,或被java.ext.dirs
系统变量所指定的目录下的所有类库; - 应用程序类加载器(Application/System ClassLoader):由
sun.misc.Launcher$AppClassLoader
实现。它是ClassLoader.getSystemClassLoader()
方法的默认返回值,所以也称为系统类加载器(System ClassLoader)。它负责加载 classpath 下所指定的类库,如果应用程序没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
如下,就是 Java 程序中的类加载器层级结构图:
以上,我们介绍了 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:从
CLASSPATH
系统变量指定的目录中加载类库。该加载器加载的类对 tomcat 本身和 web 应用都可见。但是,标准的 tomcat 启动脚本($CATALINA_HOME/bin/catalina.sh
or%CATALINA_HOME%\bin\catalina.bat
)都会忽略系统变量CLASSPATH
的值,而会使用如下的类库来创建 System 类加载器(setclasspath
脚本设置的CLASSPATH
变量对 tomcat 有用):- $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
下未打包的类和资源;$CATALINA_BASE/lib
下的 jar 包;$CATALINA_HOME/lib
下未打包的类和资源;$CATALINA_HOME/lib
下的 jar 包。
- WebappX:每个 Web 应用自己的类加载器,能够加载
/WEB-INF/classes
和/WEB-INF/lib
下的类和资源。能够被此 Web 应用使用,但对其他 Web 应用不可见。
对于 WebappX 类加载器,它并不是双亲委派模型的。当 WebappX 类接收到一个类加载请求时,它会先尝试自己去加载,自己不能加载时,再委派给父类加载器。但是,例外就是JRE 相关的类不能被覆盖。除了,WebappX 类加载器,其他的类加载器都符合通常的双亲委派模型。
四、解决 Object.class.getResourceAsStream 问题
有了以上有关类加载器的知识,现在应该能够解决为什么配置不同,导致文件不能被正确加载的问题了。
第一步,看源码:
// Class.javapublic 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.javapublic 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 相关的类(此处是
Object
,所以类加载器是 null),那么委派给ClassLoader.getSystemResourceAsStream(String)
; - 获取 System 类加载器,通过 System 类加载器去加载文件;
现在,我们知道问题就出在这个 System 类加载器上:
- 服务器:通过设置 CLASSPATH 变量,所以 System 类加载器能够找到
ss.properties
; - 本地:本地环境下,
ss.properties
最终是放在 Web 应用下的/WEB-INF/classes
文件夹下,不能被 System 类加载器获取到,所以加载失败。
解决方案:通过 getClass().getResouceAsStream(String)
去加载资源,这样首先就在 WebappX 类加载器中去寻找资源,所以无论如何都能找到。
六、总结
通过解决一个文件加载的问题,学习了 Java 应用的类加载器和 Tomcat 的类加载器架构,了解了加载的底层原理,很有成就感。
在解决该问题的过程中,有几点小心得:
- 出现问题时,从源头找问题,比如从源码去看加载的逻辑;
- 学会看源码,可能很多问题,一看源码就解决了;
- 多 Google,看资料时融汇贯通,比如解决这个问题时,最主要的帮助来自于:1、源码;2、《深入理解 Java 虚拟机》;3、Tomcat ClassLoader 的文档。
七、参考
- 《深入理解 Java 虚拟机》
- Class Loader HOW-TO
- 类加载器与 Class.getResourceAsStream 问题解决
- class.getResourceAsStream与class.getClassLoader().getResourceAsStream区别
- class.getResourceAsStream与class.getClassLoader().getResourceAsStream
- Class.getResourceAsStream()与ClassLoader.getResourceAsStream()的区别
- ClassLoader.getResourceAsStream() 与 Class.getResourceAsStream()的区别
- ClassLoader.getResourceAsStream() 与 Class.getResourceAsStream()的区别
- ClassLoader.getResourceAsStream() 与 Class.getResourceAsStream()的区别
- Class.getResourceAsStream与ClassLoader.getResourceAsStream总结
- ClassLoader.getResourceAsStream() 与 Class.getResourceAsStream()的区别
- Class.getResourceAsStream()与ClassLoader.getResourceAsStream()的区别
- 正确使用Class.getResourceAsStream("")与Class.getClassLoader().getResourceAsStream("")
- 关于class.getResourceAsStream() 与class.getClassLoader().getResourceAsStream()区别
- 关于class.getResourceAsStream() 与class.getClassLoader().getResourceAsStream()区别
- this.class.getClassLoader().getResourceAsStream与this.class.getResourceAsStream
- Class.getResourceAsStream和ClassLoader.getResourceAsStream加载文件路径问题
- Class.getResource()与Class.getResourceAsStream()方法
- JAVA 笔记 ClassLoader.getResourceAsStream() 与 Class.getResourceAsStream()的区别
- Class.getResourceAsStream(path)与Thread.currentThread().getContextClassLoader().getResourceAsStream
- 从用户的视角看待网页设计(二)
- 包装类
- 洛谷 P1801 黑匣子_NOI导刊2010提高(06)
- 状态保留之session存储问题
- hdu 1503 最长公共子序列变型
- 类加载器与 Class.getResourceAsStream 问题解决
- 问题二十一:怎么模拟ray tracing图形中不同材料的颜色(diffuse and metal)
- C++类模板应用基础练习
- 交替字符串
- 基于生产者和消费者问题的总结
- 安装memcached 遇到的问题
- redis 入门级操作----demo
- Redis 数据库之哈希键值对(hash)
- 子序列个数