Java本身是一种设计的非常简单,非常精巧的语言,所以Java背后的原理也很简单,归结起来就是两点:
1、JVM的内存管理
理解了这一点,所有和对象相关的问题统统都能解决
2、JVM Class Loader
理解了这一点,所有和Java相关的配置问题,包括各种App Server的配置,应用的发布问题统统都能解决
App Class Loader
|----- EJB Class Loader
|----- Web App Class Loader
如果在App Class Loader级别配置,是全局可见的。如果打包在EJB里面,那么就不会影响到Web Application,反之亦然,如果你在WEB-INF下面放置Hibernate,也不会影响到EJB。放在EJB Class Loader或者放在Web App Class Loader级别主要就是在局部范围内有效,不影响到其它的应用。
试想,如果在一个Weblogic上面配置多个虚拟域,你使用www.bruce.com域名,开发你的网站,我使用www.fankai.com开发我的网站,那么当然不希望我们的Hibernate相互干扰,所以就可以放在 EJB Class Loader级别来配置Hibernate。
进一步阐述一下EJB Class Loader的问题:
先再次强调一下,Hibernate和EJB,和App Server不存在兼容性问题,他们本来就是不相关的东西,就好像JDBC,相信没有人会认为JDBC和EJB不兼容吧,Hibernate也是一样,它只和JDBC驱动,和数据库有兼容性问题,而和EJB,和App Server完全是不搭界的两回事。凡是认为Hibernate和EJB不兼容的人,其实是都是因为对EJB学习的不到家,把责任推到Hibernate身上了。
我前面的帖子提到过Class Loader的层次,这里不重复了,总之我们先来看看Class Loader的作用范围:
BootStrap Class Loader:
load JRE\lib\rt.jar, sunrsasign.jar, charsets.jar, jce.jar, jsse.jar, plugin.jar
Ext Class Loader:
load JRE\lib\ext目录下的库文件, load JRE\classes目录下的类
App Class Loader:
load CLASSPATH变量指定路径下的类
以上的load路径都是写死在JVM的C++源代码里面的,不能改变,详细请见王森的《Java深度历险》
在一个特定的App Server上,Class Loader会继续向下继承,继承的层次会根据不同的App Server有所不同,但是肯定不会变的就是:
EJB Class Loader:
继承自App Class Loader,继承层次根据App Server有所不同,一个EJB Class Loader它的load Class的范围仅限于JAR或者EAR范围之内。
Web App Class Loader:
继承自App Class Loader,继承层次根据App Server有所不同,一个Web App Class Loader:它的load Class的范围在 WEB-INF\lib下的库文件和WEB-INF\classes目录下的class文件。
Web App Class Loader很好理解,大家毕竟用的很多,App Server上的一个Web Application会创建一个Web App Class Loader的实例去负责load class,所以如果你想让Hibernate只在这个Web Application内生效,把它放到WEB-INF\lib下去就好了。
如果你把Hibernate放到了CLASSPATH变量指定的路径下,而你在WEB-INF\lib也放了一份,那么Web App Class Loader由于load范围所限,它会首先找到WEB-INF\lib下的那份Hibernate,按照它的配置来初始化Hibernate。
如果你把Hibernate放到了CLASSPATH变量指定的路径下,但你在WEB-INF\lib什么都没有放,那么Web App Class Loader由于load范围所限,它根本什么都找不到,于是它把load Hibernate的责任交给上一级的Class Loader,这样直到App Class Loader,它找到了Hibernate,按照它的配置来初始化Hibernate。
EJB Class Loader稍微复杂一点,不那么容易理解。App Server会针对每一个EJB包文件创建一个EJB Class Loader的实例,例如:
HelloRobbin.jar
HelloBruce.jar
当你把这两个jar发布到App Server上以后,会创建两个EJB Class Loader的实例,分别去load这两个EJB包,比如说:
CLEJB_Robbin是load HelloRobbin.jar的
CLEJB_Bruce是load HelloBruce.jar的
那么CLEJB_Robbin的load范围就仅仅限于HelloRobbin.jar之内,它load不到HelloRobbin.jar之外的任何文件,当然它也load不到HelloBruce.jar。
说到这里,我相信大家应该已经明白为什么EJB规范不允许EJB有IO操作了吧?因为EJB Class Loader根本找不到jar包之外的文件!!!
如果现在你想实现HelloRobbin.jar和HelloBruce.jar的互相调用,那么该怎么办?他们使用了不同的EJB Class Loader,相互之间是找不到对方的。解决办法就是使用EAR。
现在假设HelloRobbin.jar和HelloBruce.jar都使用了Hibernate,看看该怎么打包和发布:
HelloEJB.ear
|------ HelloRobbin.jar
|------ HelloBruce.jar
|------ Hibernate2.jar
|------ pojo.jar (定义所有的持久对象和hbm文件的jar包)
|------ cglib-asm.jar
|------ commons-beanutils.jar
|------ commons-collections.jar
|------ commons-lang.jar
|------ commons-logging.jar
|------ dom4j.jar
|------ odmg.jar
|------ log4j.jar
|------ jcs.jar
|------ hibernate.properties
|------ log4j.properties
|------ cache.ccf
|------ META-INF\application.xml (J2EE规范的要求,定义EAR包里面包括了哪几个EJB)
除此之外,按照EJB规范要求,HelloRobbin.jar和HelloBruce.jar还必须指出调用jar包之外的类库的名称,这需要在jar包的manifest文件中定义:
HelloRobbin.jar
|------ META-INF\MANIFEST.MF
MANIFEST.MF中必须包括如下一行:
Class-Path: log4j.jar hibernate2.jar cglib-asm.jar commons-beanutils.jar commons-collections.jar commons-lang.jar
commons-logging.jar dom4j.jar jcs.jar odmg.jar jcs.jar pojo.jar
这样就OK了,当把HelloEJB.ear发布到App Server上以后,App Server创建一个EJB Class Loader实例load EAR包里面的EJB,再根据EJB的jar包里面的MANIFEST.MF指出的Class-Path去寻找相应的jar包之外的类库。
所以一个EAR包有点类似一个Web Application,EJB Class Loader的load范围也就是EAR范围之内,它load不到EAR之外的文件。除非把Hibernate定义到CLASSPATH指定的路径下,在这种情况下,EJB Class Loader找不到Hibernate,只能交给上一级的Class Loader,最后由App Class Loader找到Hibernate,进行初始化。
没有写完,继续说...
由于EAR这样load Class规则,假设Robbin和Bruce都在同一个Weblogic上运行自己的网站,而我们都不希望自己的程序里面的Hibernate配置被对方的搞乱掉,那么我们就可以这样来做:
Robbin's Website:
Robbin.ear
|-------- robbin.war (把Web Application打包)
|-------- robbin.jar (把开发的EJB打包)
|-------- Hibernate2.jar
..........................
|-------- META-INF\application.xml
Bruce's Website:
Bruce.ear
|-------- bruce.war (把Web Application打包)
|-------- bruce.jar (把开发的EJB打包)
|-------- Hibernate2.jar
..........................
|-------- META-INF\application.xml
这样在同一个App Server上运行,就可以互相不干扰。
-----------------------------------------------------------------------------
ClassLoader主要对类的请求提供服务,当JVM需要某类时,它根据名称向ClassLoader要求这个类,然后由ClassLoader返回这个类的class对象。 1.1 几个相关概念ClassLoader负责载入系统的所有Resources(Class,文件,来自网络的字节流等),通过ClassLoader从而将资源载入JVM
每个class都有一个reference,指向自己的ClassLoader。Class.getClassLoader()
array的ClassLoader就是其元素的ClassLoader,若是基本数据类型,则这个array没有ClassLoader
1.2 主要方法和工作过程Java1.1及从前版本中,ClassLoader主要方法:
Class loadClass( String name, boolean resolve ); ClassLoader.loadClass() 是 ClassLoader 的入口点
defineClass 方法是 ClassLoader 的主要诀窍。该方法接受由原始字节组成的数组并把它转换成 Class 对象。原始数组包含如从文件系统或网络装入的数据。
findSystemClass 方法从本地文件系统装入文件。它在本地文件系统中寻找类文件,如果存在,就使用 defineClass 将原始字节转换成 Class 对象,以将该文件转换成类。当运行 Java 应用程序时,这是 JVM 正常装入类的缺省机制。
resolveClass可以不完全地(不带解析)装入类,也可以完全地(带解析)装入类。当编写我们自己的 loadClass 时,可以调用 resolveClass,这取决于 loadClass 的 resolve 参数的值
findLoadedClass 充当一个缓存:当请求 loadClass 装入类时,它调用该方法来查看 ClassLoader 是否已装入这个类,这样可以避免重新装入已存在类所造成的麻烦。应首先调用该方法
一般load方法过程如下:
调用 findLoadedClass 来查看是否存在已装入的类。
如果没有,那么采用某种特殊的神奇方式来获取原始字节。(通过IO从文件系统,来自网络的字节流等)
如果已有原始字节,调用 defineClass 将它们转换成 Class 对象。
如果没有原始字节,然后调用 findSystemClass 查看是否从本地文件系统获取类。
如果 resolve 参数是 true,那么调用 resolveClass 解析 Class 对象。
如果还没有类,返回 ClassNotFoundException。
否则,将类返回给调用程序。
1.3 委托模型自从JDK1.2以后,ClassLoader做了改进,使用了委托模型,所有系统中的ClassLoader组成一棵树,ClassLoader在载入类库时先让Parent寻找,Parent找不到才自己找。
JVM在运行时会产生三个ClassLoader,Bootstrap ClassLoader、Extension ClassLoader和App ClassLoader。其中,Bootstrap ClassLoader是用C++编写的,在Java中看不到它,是null。它用来加载核心类库,就是在lib下的类库,Extension ClassLoader加载lib/ext下的类库,App ClassLoader加载Classpath里的类库,三者的关系为:App ClassLoader的Parent是Extension ClassLoader,而Extension ClassLoader的Parent为Bootstrap ClassLoader。加载一个类时,首先BootStrap进行寻找,找不到再由Extension ClassLoader寻找,最后才是App ClassLoader。
将ClassLoader设计成委托模型的一个重要原因是出于安全考虑,比如在Applet中,如果编写了一个java.lang.String类并具有破坏性。假如不采用这种委托机制,就会将这个具有破坏性的String加载到了用户机器上,导致破坏用户安全。但采用这种委托机制则不会出现这种情况。因为要加载java.lang.String类时,系统最终会由Bootstrap进行加载,这个具有破坏性的String永远没有机会加载。
委托模型还带来了一些问题,在某些情况下会产生混淆,如下是Tomcat的ClassLoader结构图:
Bootstrap
|
System
|
Common
/
Catalina Shared
/
Webapp1 Webapp2 ...
由 Common 类装入器装入的类决不能(根据名称)直接访问由 Web 应用程序装入的类。使这些类联系在一起的唯一方法是通过使用这两个类集都可见的接口。在这个例子中,就是包含由 Java servlet 实现的 javax.servlet.Servlet。
如果在lib或者lib/ext等类库有与应用中同样的类,那么应用中的类将无法被载入。通常在jdk新版本出现有类库移动时会出现问题,例如最初我们使用自己的xml解析器,而在jdk1.4中xml解析器变成标准类库,load的优先级也高于我们自己的xml解析器,我们自己的xml解析器永远无法找到,将可能导致我们的应用无法运行。
相同的类,不同的ClassLoader,将导致ClassCastException异常
1.4 线程中的ClassLoader每个运行中的线程都有一个成员contextClassLoader,用来在运行时动态地载入其它类,可以使用方法Thread.currentThread().setContextClassLoader(...);更改当前线程的contextClassLoader,来改变其载入类的行为;也可以通过方法Thread.currentThread().getContextClassLoader()来获得当前线程的ClassLoader。
实际上,在Java应用中所有程序都运行在线程里,如果在程序中没有手工设置过ClassLoader,对于一般的java类如下两种方法获得的ClassLoader通常都是同一个
this.getClass.getClassLoader();
Thread.currentThread().getContextClassLoader();
方法一得到的Classloader是静态的,表明类的载入者是谁;方法二得到的Classloader是动态的,谁执行(某个线程),就是那个执行者的Classloader。对于单例模式的类,静态类等,载入一次后,这个实例会被很多程序(线程)调用,对于这些类,载入的Classloader和执行线程的Classloader通常都不同。
1.5 Web应用中的ClassLoader回到上面的例子,在Tomcat里,WebApp的ClassLoader的工作原理有点不同,它先试图自己载入类(在ContextPath/WEB-INF/...中载入类),如果无法载入,再请求父ClassLoader完成。
由此可得:
对于WEB APP线程,它的contextClassLoader是WebAppClassLoader
对于Tomcat Server线程,它的contextClassLoader是CatalinaClassLoader
1.6 获得ClassLoader的几种方法可以通过如下3种方法得到ClassLoader
this.getClass.getClassLoader(); // 使用当前类的ClassLoader
Thread.currentThread().getContextClassLoader(); // 使用当前线程的ClassLoader
ClassLoader.getSystemClassLoader(); // 使用系统ClassLoader,即系统的入口点所使用的ClassLoader。(注意,system ClassLoader与根ClassLoader并不一样。JVM下system ClassLoader通常为App ClassLoader)
1.7 几种扩展应用用户定制自己的ClassLoader可以实现以下的一些应用
安全性。类进入JVM之前先经过ClassLoader,所以可以在这边检查是否有正确的数字签名等
加密。java字节码很容易被反编译,通过定制ClassLoader使得字节码先加密防止别人下载后反编译,这里的ClassLoader相当于一个动态的解码器
归档。可能为了节省网络资源,对自己的代码做一些特殊的归档,然后用定制的ClassLoader来解档
自展开程序。把java应用程序编译成单个可执行类文件,这个文件包含压缩的和加密的类文件数据,同时有一个固定的ClassLoader,当程序运行时它在内存中完全自行解开,无需先安装
动态生成。可以生成应用其他还未生成类的类,实时创建整个类并可在任何时刻引入JVM
2.0 资源载入
所有资源都通过ClassLoader载入到JVM里,那么在载入资源时当然可以使用ClassLoader,只是对于不同的资源还可以使用一些别的方式载入,例如对于类可以直接new,对于文件可以直接做IO等。 2.1 载入类的几种方法假设有类A和类B,A在方法amethod里需要实例化B,可能的方法有3种。对于载入类的情况,用户需要知道B类的完整名字(包括包名,例如"com.rain.B")
1. 使用Class静态方法 Class.forName
Class cls = Class.forName("com.rain.B");
B b = (B)cls.newInstance();
2. 使用ClassLoader
/* Step 1. Get ClassLoader */
ClassLoader cl; // 如何获得ClassLoader参考1.6
/* Step 2. Load the class */
Class cls = cl.loadClass("com.rain.B"); // 使用第一步得到的ClassLoader来载入B
/* Step 3. new instance */
B b = (B)cls.newInstance(); // 有B的类得到一个B的实例
3. 直接new
B b = new B();
2.2 文件载入(例如配置文件等)假设在com.rain.A类里想读取文件夹 /com/rain/config 里的文件sys.properties,读取文件可以通过绝对路径或相对路径,绝对路径很简单,在Windows下以盘号开始,在Unix下以"/"开始
对于相对路径,其相对值是相对于ClassLoader的,因为ClassLoader是一棵树,所以这个相对路径和ClassLoader树上的任何一个ClassLoader相对比较后可以找到文件,那么文件就可以找到,当然,读取文件也使用委托模型
1. 直接IO
/**
* 假设当前位置是 "C:/test",通过执行如下命令来运行A "java com.rain.A"
* 1. 在程序里可以使用绝对路径,Windows下的绝对路径以盘号开始,Unix下以"/"开始
* 2. 也可以使用相对路径,相对路径前面没有"/"
* 因为我们在 "C:/test" 目录下执行程序,程序入口点是"C:/test",相对路径就
* 是 "com/rain/config/sys.properties"
* (例子中,当前程序的ClassLoader是App ClassLoader,system ClassLoader = 当前的
* 程序的ClassLoader,入口点是"C:/test")
* 对于ClassLoader树,如果文件在jdk lib下,如果文件在jdk lib/ext下,如果文件在环境变量里,
* 都可以通过相对路径"sys.properties"找到,lib下的文件最先被找到
*/
File f = new File("C:/test/com/rain/config/sys.properties"); // 使用绝对路径
//File f = new File("com/rain/config/sys.properties"); // 使用相对路径
InputStream is = new FileInputStream(f);
如果是配置文件,可以通过java.util.Properties.load(is)将内容读到Properties里,Properties默认认为is的编码是ISO-8859-1,如果配置文件是非英文的,可能出现乱码问题。
2. 使用ClassLoader
/**
* 因为有3种方法得到ClassLoader,对应有如下3种方法读取文件
* 使用的路径是相对于这个ClassLoader的那个点的相对路径,此处只能使用相对路径
*/
InputStream is = null;
is = this.getClass().getClassLoader().getResourceAsStream(
"com/rain/config/sys.properties"); //方法1
//is = Thread.currentThread().getContextClassLoader().getResourceAsStream(
"com/rain/config/sys.properties"); //方法2
//is = ClassLoader.getSystemResourceAsStream("com/rain/config/sys.properties"); //方法3
如果是配置文件,可以通过java.util.Properties.load(is)将内容读到Properties里,这里要注意编码问题。
3. 使用ResourceBundle
ResourceBundle bundle = ResourceBundle.getBoundle("com.rain.config.sys");
这种用法通常用来载入用户的配置文件,关于ResourceBunlde更详细的用法请参考其他文档
总结:有如下3种途径来载入文件
1. 绝对路径 ---> IO
2. 相对路径 ---> IO
---> ClassLoader
3. 资源文件 ---> ResourceBundle
2.3 如何在web应用里载入资源在web应用里当然也可以使用ClassLoader来载入资源,但更常用的情况是使用ServletContext,如下是web目录结构
ContextRoot
|- JSP、HTML、Image等各种文件
|- [WEB-INF]
|- web.xml
|- [lib] Web用到的JAR文件
|- [classes] 类文件
用户程序通常在classes目录下,如果想读取classes目录里的文件,可以使用ClassLoader,如果想读取其他的文件,一般使用ServletContext.getResource()
如果使用ServletContext.getResource(path)方法,路径必须以"/"开始,路径被解释成相对于ContextRoot的路径,此处载入文件的方法和ClassLoader不同,举例"/WEB-INF/web.xml","/download/WebExAgent.rar"