Friday, March 13, 2009

一个简单的自定义ClassLoader的实现

很多时候人们会使用一些自定义的ClassLoader ,而不是使用系统的Class Loader。大多数时候人们这样做的原因是,他们在编译时无法预知运行时会需要那些Class。特别是在那些appserver中,比如tomcat,Avalon-phonix,Jboss中。或是程序提供一些plug-in的功能,用户可以在程序编译好之后再添加自己的功能,比如ant, jxta-shell等。定制一个ClassLoader很简单,一般只需要理解很少的几个方法就可以完成。
一个最简单的自定义的ClassLoader从ClassLoader类继承而来。这里我们要做一个可以在运行时指定路径,加载这个路径下的class的ClassLoader。
通常我们使用ClassLoader.loadClass(String):Class方法,通过给出一个类名,就会得到一个相应的Class实例。因此只要小小的改动这个方法,就可以实现我们的愿望了。
源码:

protected synchronized Class loadClass(String name, boolean resolve) throws ClassNotFoundException { // First, check if the class has already been loaded Class c = findLoadedClass(name); if (c == null) { try { if (parent != null) { c = parent.loadClass(name, false); }else{ c = findBootstrapClass0(name); } }catch(ClassNotFoundException e){ // If still not found, then call findClass in order // to find the class. c = findClass(name); } } if (resolve) { resolveClass(c); } return c;}

Source from ClassLoader.java

First,check JavaAPI doc:上面指出了缺省的loadClass方法所做的几个步骤。
1. 调用findLoadedClass(String):Class 检查一下这个class是否已经被加载过了,由于JVM 规范规定ClassLoader可以cache它所加载的Class,因此如果一个class已经被加载过的话,直接从cache中获取即可。
2. 调用它的parent 的loadClass()方法,如果parent为空,这使用JVM内部的class loader(即著名的bootstrap classloader)。
3. 如果上面两步都没有找到,调用findClass(String)方法来查找并加载这个class。
后面还有一句话,在Java 1.2版本以后,鼓励用户通过继承findClass(String)方法实现自己的class loader而不是继承loadClass(String)方法。
既然如此,那么我们就先这么做:)

public class AnotherClassLoader extends ClassLoader { private String baseDir;private static final Logger LOG = Logger.getLogger(AnotherClassLoader.class); public AnotherClassLoader (ClassLoader parent, String baseDir) { super(parent); this.baseDir = baseDir; } protected Class findClass(String name) throws ClassNotFoundException { LOG.debug("findClass " + name); byte[] bytes = loadClassBytes(name); Class theClass = defineClass(name, bytes, 0, bytes.length);//A if (theClass == null) throw new ClassFormatError(); return theClass; } private byte[] loadClassBytes(String className) throws ClassNotFoundException { try { String classFile = getClassFile(className); FileInputStream fis = new FileInputStream(classFile); FileChannel fileC = fis.getChannel(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); WritableByteChannel outC = Channels.newChannel(baos); ByteBuffer buffer = ByteBuffer.allocateDirect(1024); while (true) { int i = fileC.read(buffer); if (i == 0 || i == -1) { break; } buffer.flip(); outC.write(buffer); buffer.clear(); } fis.close(); return baos.toByteArray(); } catch (IOException fnfe) { throw new ClassNotFoundException(className); } } private String getClassFile(String name) { StringBuffer sb = new StringBuffer(baseDir); name = name.replace('.', File.separatorChar) + ".class"; sb.append(File.separator + name); return sb.toString(); }}

Ps:这里使用了一些JDK1.4的nio的代码:)
很简单的代码,关键的地方就在A处,我们使用了defineClass方法,目的在于把从class文件中得到的二进制数组转换为相应的Class实例。defineClass是一个native的方法,它替我们识别class文件格式,分析读取相应的数据结构,并生成一个class实例。

还没完呢,我们只是找到了发布在某个目录下的class,还有资源呢。我们有时会用Class.getResource():URL来获取相应的资源文件。如果仅仅使用上面的ClassLoader是找不到这个资源的,相应的返回值为null。

同样我们看一下原来的ClassLoader内部的结构。

public java.net.URL getResource(String name) { name = resolveName(name); ClassLoader cl = getClassLoader0();//这里 if (cl==null) { // A system class. return ClassLoader.getSystemResource(name); } return cl.getResource(name);}


原来是使用加载这个class的那个classLoader获取得资源。


public URL getResource(String name) { URL url; if (parent != null) { url = parent.getResource(name); } else { url = getBootstrapResource(name); } if (url == null) { url = findResource(name);//这里 } return url;}




这样看来只要继承findResource(String)方法就可以了。修改以下我们的代码:


//新增的一个findResource方法protected URL findResource(String name) { LOG.debug("findResource " + name); try { URL url = super.findResource(name); if (url != null) return url; url = new URL("file:///" + converName(name)); //简化处理,所有资源从文件系统中获取 return url; } catch (MalformedURLException mue) { LOG.error("findResource", mue); return null; }}private String converName(String name) { StringBuffer sb = new StringBuffer(baseDir); name = name.replace('.', File.separatorChar); sb.append(File.separator + name); return sb.toString();}

Tomcat5 和 ClassLoader

8/12/2005
Tomcat5 和 ClassLoader
许多Java程序员在用Tomcat5进行WEB开发和部署过程中,都会遇到与ClassLoader有关的问题.例如经常出现的
java.lang.NoClassDefFoundError.在本文下面的叙述中,将首先介绍Class Loader(类装载器)的运行机制,然后再介绍Tomcat5中的
ClassLoader,以及在Tomcat5实际操作中遇到的问题和解决方法.


Tomcat5 和 ClassLoader
---作者:张增志


概要
许多Java程序员在用Tomcat5进行WEB开发和部署过程中,都会遇到与ClassLoader有关的问题.例如经常出现的
java.lang.NoClassDefFoundError.在本文下面的叙述中,将首先介绍Class Loader(类装载器)的运行机制,然后再介绍Tomcat5中的
ClassLoader,以及在Tomcat5实际操作中遇到的问题和解决方法.


一. Class Loader(类装载器)
1. 类装载器结构
类装载器(Class Loader)是Java虚拟机的组成部分之一,如图1所示.Java虚拟机有两种类装载器:启动类装载器和用户自定义类装载器.
URL:http://blog.blogchina.com/upload/2004-11-24/20041124104254943028.jpg

图1:Java虚拟机的内部体系结构.
启动类装载器 每个Java虚拟机实现都必须有一个启动类装载器.在Sun的JDK 1.2以后的版本中,启动类装载器只负责在系统类(核心Java API的class文件)的安装路径中查找要装入的类.
用户自定义类装载器 它是普通的Java对象,它的类必须继承自java.lang.ClassLoader类.在Sun的JDK 1.2以后的版本中,用户自定义类装载器负责核心Java API以外的其它class文件的装载.例如,用于安装或下载标准扩展的class文件,在类路径中发现的类库的class文件,用于应用程序运行的class文件,等等.
命名空间 Java虚拟机为每一个类装载器维护一个唯一标识的命名空间.一个Java程序可以多次装载具有同一个全限定名(指类所属的包名加类名,如java.lang.Object就是类Object的全限定名)的多个类(class). Java虚拟机要确定这"多个类"的唯一性,因此,当多个类装载器都装载了同名的类时,为了唯一地标识这个类,还要在类名前加上装载该类的类装载器的标识(指出了类所位于的命名空间).例如:ExtClassLoader 装载了sun.text.resources.DateFormatZoneData_zh_CN类,AppClassLoader装载了sun.text.resources.DateFormatZoneData_zh_HK类, Java虚拟机就认为这两个类位于不同的包中,彼此之间不能访问私有成员.如果AppClassLoader也装载了sun.text.resources.DateFormatZoneData_zh_CN类, 虽然"类名"相同,Java虚拟机也认为它们是不同的类,因为它们处在不同的命名空间中.
2. 委托(Delegation)模型
当Java虚拟机开始运行时,在应用程序开始启动以前,它至少创建一个用户自定义装载器,也可能创建多个.所有这些装载器被连接在一个Parent-Child的委托链中,在这个链的顶端是启动类装载器,末端是被称为"系统类装载器"的类装载器.
例如,假设你写了一个应用程序,在虚拟机上运行它.虚拟机在启动时实例化了两个用户自定义类装载器:一个"扩展类装载器",一个"类路径类装载器".这些类装载器和启动类装载器一起联入一个Parent-Child委托链中,如图2所示.
URL:http://blog.blogchina.com/upload/2004-11-24/2004112410430176267.jpg

图2:Parent-Child类装载器委托链
类路径类装载器的Parent是扩展类装载器, 扩展类装载器的Parent是启动类装载器.在图2中,类路径类装载器就被实例为系统类装载器.假设你的程序实例化它的网络类装载器,它就指明了系统类装载器作为它的Parent.
下面的例程说明了类装载器的父子关系.
例程1:
package test;
import java.net.URL;
import java.net.URLClassLoader;
public class ClassLoaderTest {
private static int count = -1;
public static void testClassLoader(Object obj) {
if (count < 0 && obj == null) {
System.out.println("Input object is NULL");
return;
}
ClassLoader cl = null;
if (obj != null && !(obj instanceof ClassLoader)) {
cl = obj.getClass().getClassLoader();
} else if (obj != null) {
cl = (ClassLoader) obj;
}
count++;
String parent = "";
for (int i = 0; i < count; i++) {
parent += "Parent ";
}
if (cl != null) {
System.out.println(
parent + "ClassLoader name = " + cl.getClass().getName());
testClassLoader(cl.getParent());
} else {
System.out.println(
parent + "ClassLoader name = BootstrapClassLoader");
count = -1;
}
}
public static void main(String[] args) {
URL[] urls = new URL[1];
URLClassLoader urlLoader = new URLClassLoader(urls);
ClassLoaderTest.testClassLoader(urlLoader);
}
}
以上例程的输出为:
ClassLoader name = java.net.URLClassLoader
Parent ClassLoader name = sun.misc.Launcher$AppClassLoader
Parent Parent ClassLoader name = sun.misc.Launcher$ExtClassLoader
Parent Parent Parent ClassLoader name = BootstrapClassLoader
类装载器请求过程
以上例程1为例.将main方法改为:
ClassLoaderTest tc = new ClassLoaderTest();
ClassLoaderTest.testClassLoader(tc);
输出为:
ClassLoader name = sun.misc.Launcher$AppClassLoader
Parent ClassLoader name = sun.misc.Launcher$ExtClassLoader
Parent Parent ClassLoader name = BootstrapClassLoader
程序运行过程中,类路径类装载器发出一个装载ClassLoaderTest类的请求, 类路径类装载器必须首先询问它的Parent---扩展类装载器---来查找并装载这个类,同样扩展类装载器首先询问启动类装载器.由于ClassLoaderTest不是Java API(JAVA_HOME\jre\lib)中的类,也不在已安装扩展路径(JAVA_HOME\jre\lib\ext)上,这两类装载器都将返回而不会提供一个名为ClassLoaderTest的已装载类给类路径类装载器.类路径类装载器只能以它自己的方式来装载ClassLoaderTest,它会从当前类路径上下载这个类.这样,ClassLoaderTest就可以在应用程序后面的执行中发挥作用.
在上例中, ClassLoaderTest类的testClassLoader方法被首次调用,该方法引用了Java API中的类java.lang.String.Java虚拟机会请求装载ClassLoaderTest类的类路径类装载器来装载java.lang.String.就像前面一样,类路径类装载器首先将请求传递给它的Parent类装载器,然后这个请求一路被委托到启动类装载器.但是,启动类装载器可以将java.lang.String类返回给类路径类装载器,因为它可以找到这个类,这样扩展类装载器就不必在已安装扩展路径中查找这个类,类路径类装载器也不必在类路径中查找这个类.扩展类装载器和类路径类装载器仅需要返回由启动类装载器返回的类java.lang.String.从这一刻开始,不管何时ClassLoaderTest类引用了名为java.lang.String的类,虚拟机就可以直接使用这个java.lang.String类了.
在上述过程中也可能会发生错误,在本文下面的例子中将会涉及.
3. 装载 连接及初始化
在一个Java类的生命周期中,装载,连接和初始化只是其开始阶段.只有开始阶段结束以后,类才可以被实例化并被使用.整个开始阶段必须按以下顺序进行:
1)装载 把二进制形式的Java class读入虚拟机中.
2)连接 把已经读入虚拟机的二进制形式的类数据合并到虚拟机的运行状态中去.连接阶段分为验证,准备和解析三个子步骤.
3)初始化 给类变量赋以适当的初始值.
Java虚拟机允许类装载器(启动或用户自定义类装载器)缓存Java class的二进制形式,在预知某个类将要被使用时就装载它.如果一个类装载器在预先装载时遇到问题,它应该在该类被"首次主动使用"时报告该问题(通过抛出一个java.lang.LinkageError的子类).也就是说,如果一个类装载器在预先装载时遇到缺失或错误的class文件,它必须等到程序首次被主动使用该类时才报告错误.如果这个类一直没有被程序主动使用,那么该类装载器将不会报告错误.
二. Tomcat5中的Class Loader
当Tomcat5启动的时候,它会首先创建一组class loader,如commonLoader, sharedLoader, catalinaLoader,webappLoader等.其委托模型如下图3所示:
Bootstrap
|
System
|
Common
/ \
Catalina Shared
\
Webapp1 ...
图3:Tomcat5类装载器委托模型
其中,
1) Bootstrap 该类装载器装载JAVA_HOME\jre\lib和JAVA_HOME\jre\lib\ext两目录上的JAR包.
2) System 该类装载器装载当前CLASSPATH上的JAR包.在Windows系统下, CLASSPATH环境变量会在CATALINA_HOME\bin\setclasspath.bat和CATALINA_HOME\bin\catalina.bat文件中被重新设置.
3) Common 该类装载器装载CATALINA_HOME\common\classes目录中的类, CATALINA_HOME\commons\endorsed和CATALINA_HOME\common\lib目录中的JAR包.
4) Catalina 该类装载器装载CATALINA_HOME\server\classes和CATALINA_HOME\server\lib目录中的类和JAR包.
5) Shared 该类装载器装载CATALINA_HOME\shared\classes和CATALINA_HOME\shared\lib目录中的类和JAR包.
6) WebappX 该类装载器装载WEB-INF\classes和WEB-INF\lib目录中的类和JAR包.

需要补充说明的是,WebappX类装载器独立于上文提到的Java2的委托模型.当WebappX类装载器装载一个类时,它会首先查找本身所辖目录(即WEB-INF\classes和WEB-INF\lib)下的类,而不会启动委托机制.当然对于Bootstrap和System类装载器中存在的类,是要进行委托的.另外,对于下面这些包中的类,如:
javax.*
org.xml.sax.*
org.w3c.dom.*
org.apache.xerces.*
org.apache.xalan.*
WebappX类装载器装载时也要启动委托机制.
例如,假设ojdbc14.jar处在setclasspath.bat中的CLASSPATH下,同时也处在WEB-INF\lib目录下.类装载器系统在请求装载oracle.jdbc.driver.OracleDriver类时,会得到从System类装载器返回的类,而不是WebappX类装载器.
再假设ojdbc14.jar处在CATALINA_HOME\common\lib和WEB-INF\lib目录下,而没有处在setclasspath.bat中的CLASSPATH下,那么类装载器系统就会得到从WebappX类装载器返回的类,而不是Common类装载器.
另外,如果WEB-INF\lib目录下存在包含有servlet API类的JAR包,该JAR包将会被WebappX类装载器忽略.例如,
考虑到应用程序的编译问题,你可能会把servlet-api.jar包Copy到应用程序中的WEB-INF\lib目录下.那么,在Tomcat5启动时,启动屏幕上就会出现如下输出:
2004/09/12 14:53:57 org.apache.catalina.loader.WebappClassLoader validateJarFile
情报: validateJarFile(E:\MyData\myProjects\MyBS_SQL\web\WEB-INF\lib\servlet-api.jar) - jar not loaded. See Servlet Spec 2.3, section 9.7.2. Offending class: javax/servlet/Servlet.class
上述信息并不影响你的程序运行.因为WebappX类装载器虽然会忽略掉WEB-INF\lib目录下servlet-api.jar包,但是,Common类装载器已经装载了servlet-api.jar包.如前所述,对于javax.*中的类, WebappX类装载器是要启动委托机制的,所以WebappX类装载器会得到Common类装载器返回的javax.*中的类.
如果不希望Tomcat5启动时输出上述信息,只需将servlet-api.jar包从应用程序中的WEB-INF\lib目录下移走就行了.
总结一下,Tomcat5类装载器系统在请求装载一个类时,它以下面列举的顺序进行:
· Bootstrap (JAVA_HOME\jre\lib和JAVA_HOME\jre\lib\ext)
· System (当前CLASSPATH上)
· \WEB-INF\classes 和\WEB-INF\lib\*.jar
· CATALINA_HOME\common\classes
· CATALINA_HOME\common\endorsed\*.jar
· CATALINA_HOME\common\lib\*.jar
· CATALINA_HOME\shared\classes 和CATALINA_HOME\shared\lib\*.jar
三. Tomcat5与WEB应用
我们在用Tomcat5进行应用开发时,经常需要将第三方JAR包添加到应用中.通常的作法是Copy这些JAR包到\WEB-INF\lib目录下.
但是,有些情况可能不允许我们这样Copy JAR包到Tomcat5认可的目录下(如本文上一部分提到的Tomcat5下的目录).这样我们就只有修改CATALINA_HOME\bin\setclasspath.bat和CATALINA_HOME\bin\catalina.bat文件中的CLASSPATH变量了.
假设你需要添加ojdbc14.jar到你的应用中,而又不允许你把它从\oracle\ora92\jdbc\lib目录下Copy到别的目录下,于是你可能会修改CATALINA_HOME\bin\setclasspath.bat文件,新的文件中可能存在如下代码:
set CLASSPATH=%CLASSPATH%;D:\oracle\ora92\jdbc\lib\ojdbc14.jar
这样就满足了应用本身的要求.
1. 出现java.lang.NoClassDefFoundError错误

我们继续上面的讨论,同样是在不允许Copy JAR包的情况下.
当你在Tomcat5中部署一个应用时,发现该应用需要一个coreapi.jar包(这个JAR包是你的开发人员为一个正在开发的工具开发的,你要部署的应用是该工具的产品),由于该工具正在开发过程中,你和开发人员没有进行充分地沟通,所以不知道这个JAR包中的某一个类A.class继承了javax.servlet.Servlet类,于是你只在CATALINA_HOME\bin\setclasspath.bat文件添加如下代码,就没有把servlet-api.jar添加到CLASSPATH上.
set CLASSPATH=%CLASSPATH%;D:\myTool\lib\coreapi.jar
当你测试时,发现运行某一个URL时,会出现如下错误:
root cause
java.lang.NoClassDefFoundError: javax/servlet/Servlet
...
下面我们就分析一下为什么会出现这个运行错误.
按照你的配置,coreapi.jar现处在CLASSPATH上,所以A.class被System类装载器装载. System类装载器在装载A.class时,发现它引用了javax.servlet.Servlet类,按照类装载器系统的委托模型,System类装载器会首先请求Bootstrap类装载器,Bootstrap类装载器不能返回javax.servlet.Servlet类给System类装载器,System类装载器自己也不能装载javax.servlet.Servlet类.也就是说,此时的类装载器系统在预装载时遇到一个缺失的javax.servlet.Servlet 类文件.但是,如本文第一部分中的装载 连接及初始化所述,在类装载器系统工作过程中开始阶段发生的这个问题并不会被马上报告,而是在javax.servlet.Servlet类被"首次主动使用时"抛出java.lang.NoClassDefFoundError(java.lang.LinkageError的子类之一).
解决该错误的方法当然就是把servlet-api.jar也添加到CLASSPATH上.如
set CLASSPATH=%CLASSPATH%;D:\myTool\lib\coreapi.jar;%BASEDIR%\common\lib\servlet-api.jar
2. Tomcat5中的UserDatabase
Tomcat5默认配置了一个全局JNDI资源UserDatabase, 默认配置如下:
<Resource name="UserDatabase" auth="Container"
type="org.apache.catalina.UserDatabase"
description="User database that can be updated and saved">
</Resource>
<ResourceParams name="UserDatabase">
<parameter>
<name>factory</name>
<value>org.apache.catalina.users.MemoryUserDatabaseFactory
</value>
</parameter>
<parameter>
<name>pathname</name>
<value>conf/tomcat-users.xml</value>
</parameter>
</ResourceParams>
利用它,我们可以修改pathname下的值conf/tomcat-users.xml(该值可配,如改为<value>conf/tomcat-zzz.xml</value>)文件中的数据.
为了修改tomcat-users.xml文件中的数据,我们在WEB应用中可以先取得Object,例程如下:
例程2:
1 try {
2 InitialContext initCtx = new InitialContext();
3 Object obj = initCtx.lookup("java:comp/env/userDatabase");
4 UserDatabase udb = (UserDatabase) obj;
5 } catch (Exception e) {
6 e.printStackTrace();
7 }
当例程2运行时,会产生一个错误,如:
root cause
java.lang.NoClassDefFoundError: org/apache/catalina/UserDatabase
...
上一错误是例程2中的第4行抛出的.因为UserDatabase类处在CATALINA_HOME\server\lib\catalina.jar包中,被Catalina类装载器装载.例程2所在的类处在WebappX类装载器,根据类装载器系统的委托机制, WebappX类装载器不会得到从Catalina类装载器返回的UserDatabase类.所以程序运行到例程2中的第4行时,会抛出java.lang.NoClassDefFoundError.
假设你把CATALINA_HOME\server\lib目录中的catalina.jar包及其相关JAR包Copy到\WEB-INF\lib目录下(当然这仅仅是一种测试情况),程序运行到第4行时,则会出现java.lang.ClassCastException. 因为WebappX类装载器得到的UserDatabase类是其本身装载的,与从JNDI中得到的UserDatabase类处在不同的命名空间中,是不同的类.
你还可以把catalina.jar包及其相关JAR包设置到CLASSPATH下.但还是强烈建议你不要这么做.
既然不能显式地声明UserDatabase类,我们可以通过反射(Reflection)的方式进行方法调用.

例程3:
try {
InitialContext initCtx = new InitialContext();
Object obj = initCtx.lookup("java:comp/env/userDatabase");
Class[] cl =
new Class[] { String.class, String.class, String.class };
Method cm = obj.getClass().getMethod("createUser", cl);
String[] s = new String[] { "zzz", "zzz", "zzz" };
cm.invoke(obj, s);
Method sm = obj.getClass().getMethod("save", null);
sm.invoke(obj, null);
} catch (Exception e) {
e.printStackTrace();
}
这样上述程序就在tomcat-users.xml文件中插入了这样一行数据:
<user username="zzz" password="zzz" fullName="zzz"/>
四. 参考资料
1. Bill Venners."Inside the Java Virtual Machine".McGraw-Hill Book Co,1997
2. http://jakarta.apache.org/tomcat/tomcat-5.0-doc/class-loader-howto.html

了解 JAVA classloader

什么是 ClassLoader?


在流行的商业化编程语言中,Java 语言由于在 Java 虚拟机 (JVM) 上运行而显得与众不同。这意味着已编译的程序是一种非凡的、独立于平台的格式,并非依靠于它们所运行的机器。在很大程度上,这种格式不同于传统的可执行程序格式。

与 C 或 C++ 编写的程序不同,Java 程序并不是一个可执行文件,而是由许多独立的类文件组成,每一个文件对应于一个 Java 类。
此外,这些类文件并非立即全部都装入内存,而是根据程序需要装入内存。ClassLoader 是 JVM 中将类装入内存的那部分。
而且,Java ClassLoader 就是用 Java 语言编写的。这意味着创建您自己的 ClassLoader 非常轻易,不必了解 JVM 的微小细节。


为什么编写 ClassLoader?


假如 JVM 已经有一个 ClassLoader,那么为什么还要编写另一个呢?问得好。缺省的 ClassLoader 只知道如何从本地文件系统装入类文件。不过这只适合于常规情况,即已全部编译完 Java 程序,并且计算机处于等待状态。

但 Java 语言最具新意的事就是 JVM 可以非常轻易地从那些非本地硬盘或从网络上获取类。例如,浏览者可以使用定制的 ClassLoader 从 Web 站点装入可执行内容。

有许多其它方式可以获取类文件。除了简单地从本地或网络装入文件以外,可以使用定制的 ClassLoader 完成以下任务:
在执行非置信代码之前,自动验证数字签名
使用用户提供的密码透明地解密代码
动态地创建符合用户特定需要的定制化构建类
任何您认为可以生成 Java 字节码的内容都可以集成到应用程序中。



定制 ClassLoader 示例


假如使用过 JDK 或任何基于 Java 浏览器中的 Applet 查看器,那么您差不多肯定使用过定制的 ClassLoader。

Sun 最初发布 Java 语言时,其中最令人兴奋的一件事是观看这项新技术是如何执行在运行时从远程的 Web 服务器装入的代码。(此外,还有更令人兴奋的事 -- Java 技术提供了一种便于编写代码的强大语言。)更一些令人激动的是它可以执行从远程 Web 服务器通过 HTTP 连接发送过来的字节码。

此项功能归功于 Java 语言可以安装定制 ClassLoader。Applet 查看器包含一个 ClassLoader,它不在本地文件系统中寻找类,而是访问远程服务器上的 Web 站点,经过 HTTP 装入原始的字节码文件,并把它们转换成 JVM 内的类。

浏览器和 Applet 查看器中的 ClassLoaders 还可以做其它事情:它们支持安全性以及使不同的 Applet 在不同的页面上运行而互不干扰。

Luke Gorrie 编写的 Echidna 是一个开放源码包,它可以使您在单个虚拟机上运行多个 Java 应用程序。它使用定制的 ClassLoader,通过向每个应用程序提供该类文件的自身副本,以防止应用程序互相干扰。


我们的 ClassLoader 示例


了解了 ClassLoader 如何工作以及如何编写 ClassLoader 之后,我们将创建称作 CompilingClassLoader (CCL) 的 Classloader。CCL 为我们编译 Java 代码,而无需要我们干涉这个过程。它基本上就类似于直接构建到运行时系统中的 "make" 程序。
注:进一步了解之前,应注重在 JDK 版本 1.2 中已改进了 ClassLoader 系统的某些方面(即 Java 2 平台)。本教程是按 JDK 版本 1.0 和 1.1 写的,但也可以在以后的版本中运行。

Java 2 中 ClassLoader 的变动描述了 Java 版本 1.2 中的变动,并提供了一些具体信息,以便修改 ClassLoader 来利用这些变动。

ClassLoader 的基本目标是对类的请求提供服务。当 JVM 需要使用类时,它根据名称向 ClassLoader 请求这个类,然后 ClassLoader 试图返回一个表示这个类的 Class 对象。 通过覆盖对应于这个过程不同阶段的方法,可以创建定制的 ClassLoader。

在本文的其余部分,您会学习 Java ClassLoader 的要害方法。您将了解每一个方法的作用以及它是如何适合装入类文件这个过程的。您也会知道,创建自己的 ClassLoader 时,需要编写什么代码。

在下文中,您将会利用这些知识来使用我们的 ClassLoader 示例 -- CompilingClassLoader。


方法 loadClass

ClassLoader.loadClass() 是 ClassLoader 的入口点。其特征如下:
Class loadClass( String name, boolean resolve );
name 参数指定了 JVM 需要的类的名称,该名称以包表示法表示,如 Foo 或 java.lang.Object。 resolve 参数告诉方法是否需要解析类。在预备执行类之前,应考虑类解析。并不总是需要解析。假如 JVM 只需要知道该类是否存在或找出该类的超类,那么就不需要解析。
在 Java 版本 1.1 和以前的版本中,loadClass 方法是创建定制的 ClassLoader 时唯一需要覆盖的方法。(Java 2 中 ClassLoader 的变动提供了关于 Java 1.2 中 findClass() 方法的信息。)


方法 defineClass


defineClass 方法是 ClassLoader 的主要诀窍。该方法接受由原始字节组成的数组并把它转换成 Class 对象。原始数组包含如从文件系统或网络装入的数据。

defineClass 治理 JVM 的许多复杂、神秘和倚赖于实现的方面 -- 它把字节码分析成运行时数据结构、校验有效性等等。不必担心,您无需亲自编写它。事实上,即使您想要这么做也不能覆盖它,因为它已被标记成最终的。


方法 findSystemClass


findSystemClass 方法从本地文件系统装入文件。它在本地文件系统中寻找类文件,假如存在,就使用 defineClass 将原始字节转换成 Class 对象,以将该文件转换成类。当运行 Java 应用程序时,这是 JVM 正常装入类的缺省机制。(Java 2 中 ClassLoader 的变动提供了关于 Java 版本 1.2 这个过程变动的具体信息。)

对于定制的 ClassLoader,只有在尝试其它方法装入类之后,再使用 findSystemClass。原因很简单:ClassLoader 是负责执行装入类的非凡步骤,不是负责所有类。例如,即使 ClassLoader 从远程的 Web 站点装入了某些类,仍然需要在本地机器上装入大量的基本 Java 库。而这些类不是我们所关心的,所以要 JVM 以缺省方式装入它们:从本地文件系统。这就是 findSystemClass 的用途。

其工作流程如下:
请求定制的 ClassLoader 装入类。
检查远程 Web 站点,查看是否有所需要的类。
假如有,那么好;抓取这个类,完成任务。
假如没有,假定这个类是在基本 Java 库中,那么调用 findSystemClass,使它从文件系统装入该类。


在大多数定制 ClassLoaders 中,首先调用 findSystemClass 以节省在本地就可以装入的许多 Java 库类而要在远程 Web 站点上查找所花的时间。然而,正如,在下一章节所看到的,直到确信能自动编译我们的应用程序代码时,才让 JVM 从本地文件系统装入类。


方法 resolveClass
正如前面所提到的,可以不完全地(不带解析)装入类,也可以完全地(带解析)装入类。当编写我们自己的 loadClass 时,可以调用 resolveClass,这取决于 loadClass 的 resolve 参数的值。


方法 findLoadedClass
findLoadedClass 充当一个缓存:当请求 loadClass 装入类时,它调用该方法来查看 ClassLoader 是否已装入这个类,这样可以避免重新装入已存在类所造成的麻烦。应首先调用该方法。


组装
让我们看一下如何组装所有方法。
我们的 loadClass 实现示例执行以下步骤。(这里,我们没有指定生成类文件是采用了哪种技术 -- 它可以是从 Net 上装入、或者从归档文件中提取、或者实时编译。无论是哪一种,那是种非凡的神奇方式,使我们获得了原始类文件字节。)


CCL 揭密
我们的 ClassLoader (CCL) 的任务是确保代码被编译和更新。
下面描述了它的工作方式:
当请求一个类时,先查看它是否在磁盘的当前目录或相应的子目录。
假如该类不存在,但源码中有,那么调用 Java 编译器来生成类文件。
假如该类已存在,检查它是否比源码旧。假如是,调用 Java 编译器来重新生成类文件。
假如编译失败,或者由于其它原因不能从现有的源码中生成类文件,返回 ClassNotFoundException。
假如仍然没有该类,也许它在其它库中,所以调用 findSystemClass 来寻找该类。
假如还是没有,则返回 ClassNotFoundException。
否则,返回该类。
调用 findLoadedClass 来查看是否存在已装入的类。
假如没有,那么采用那种非凡的神奇方式来获取原始字节。
假如已有原始字节,调用 defineClass 将它们转换成 Class 对象。
假如没有原始字节,然后调用 findSystemClass 查看是否从本地文件系统获取类。
假如 resolve 参数是 true,那么调用 resolveClass 解析 Class 对象。
假如还没有类,返回 ClassNotFoundException。
否则,将类返回给调用程序。


Java 编译的工作方式
在深入讨论之前,应该先退一步,讨论 Java 编译。通常,Java 编译器不只是编译您要求它编译的类。它还会编译其它类,假如这些类是您要求编译的类所需要的类。
CCL 逐个编译应用程序中的需要编译的每一个类。但一般来说,在编译器编译完第一个类后,CCL 会查找所有需要编译的类,然后编译它。为什么?Java 编译器类似于我们正在使用的规则:假如类不存在,或者与它的源码相比,它比较旧,那么它需要编译。其实,Java 编译器在 CCL 之前的一个步骤,它会做大部分的工作。

当 CCL 编译它们时,会报告它正在编译哪个应用程序上的类。在大多数的情况下,CCL 会在程序中的主类上调用编译器,它会做完所有要做的 -- 编译器的单一调用已足够了。

然而,有一种情形,在第一步时不会编译某些类。假如使用 Class.forName 方法,通过名称来装入类,Java 编译器会不知道这个类时所需要的。在这种情况下,您会看到 CCL 再次运行 Java 编译器来编译这个类。在源代码中演示了这个过程。


使用 CompilationClassLoader
要使用 CCL,必须以非凡方式调用程序。不能直接运行该程序,如: % java Foo arg1 arg2
应以下列方式运行它:
% java CCLRun Foo arg1 arg2


CCLRun 是一个非凡的存根程序,它创建 CompilingClassLoader 并用它来装入程序的主类,以确保通过 CompilingClassLoader 来装入整个程序。CCLRun 使用 Java Reflection API 来调用特定类的主方法并把参数传递给它。有关具体信息,请参阅源代码。


运行示例
源码包括了一组小类,它们演示了工作方式。主程序是 Foo 类,它创建类 Bar 的实例。类 Bar 创建另一个类 Baz 的实例,它在 baz 包内,这是为了展示 CCL 是如何处理子包里的代码。Bar 也是通过名称装入的,其名称为 Boo,这用来展示它也能与 CCL 工作。

每个类都声明已被装入并运行。现在用源代码来试一下。编译 CCLRun 和 CompilingClassLoader。确保不要编译其它类(Foo、Bar、Baz 和 Boo),否则将不会使用 CCL,因为这些类已经编译过了。

% java CCLRun Foo arg1 arg2
CCL: Compiling Foo.java...
foo! arg1 arg2
bar! arg1 arg2
baz! arg1 arg2
CCL: Compiling Boo.java...
Boo!


请注重,首先调用编译器,Foo.java 治理 Bar 和 baz.Baz。直到 Bar 通过名称来装入 Boo 时,被调用它,这时 CCL 会再次调用编译器来编译它。
CompilingClassLoader.java


以下是 CompilingClassLoader.java 的源代码

// $Id$
import java.io.*;
/*
A CompilingClassLoader compiles your Java source on-the-fly. It checks
for nonexistent .class files, or .class files that are older than their
corresponding source code.
*/
public class CompilingClassLoader extends ClassLoader
{
// Given a filename, read the entirety of that file from disk
// and return it as a byte array.
private byte[] getBytes( String filename ) throws IOException {
// Find out the length of the file
File file = new File( filename );
long len = file.length();
// Create an array that's just the right size for the file's
// contents
byte raw[] = new byte[(int)len];
// Open the file
FileInputStream fin = new FileInputStream( file );
// Read all of it into the array; if we don't get all,
// then it's an error.
int r = fin.read( raw );
if (r != len)
throw new IOException( "Can't read all, "+r+" != "+len );
// Don't forget to close the file!
fin.close();
// And finally return the file contents as an array
return raw;
}
// Spawn a process to compile the java source code file
// specified in the 'javaFile' parameter. Return a true if
// the compilation worked, false otherwise.
private boolean compile( String javaFile ) throws IOException {
// Let the user know what's going on
System.out.println( "CCL: Compiling "+javaFile+"..." );
// Start up the compiler
Process p = Runtime.getRuntime().exec( "javac "+javaFile );
// Wait for it to finish running
try {
p.waitFor();
} catch( InterruptedException ie ) { System.out.println( ie ); }
// Check the return code, in case of a compilation error
int ret = p.exitValue();
// Tell whether the compilation worked
return ret==0;
}
// The heart of the ClassLoader -- automatically compile
// source as necessary when looking for class files
public Class loadClass( String name, boolean resolve )
throws ClassNotFoundException {
// Our goal is to get a Class object
Class clas = null;
// First, see if we've already dealt with this one
clas = findLoadedClass( name );
//System.out.println( "findLoadedClass: "+clas );
// Create a pathname from the class name
// E.g. java.lang.Object => java/lang/Object
String fileStub = name.replace( '.', '/' );
// Build objects pointing to the source code (.java) and object
// code (.class)
String javaFilename = fileStub+".java";
String classFilename = fileStub+".class";
File javaFile = new File( javaFilename );
File classFile = new File( classFilename );
//System.out.println( "j "+javaFile.lastModified()+" c "+
// classFile.lastModified() );
// First, see if we want to try compiling. We do if (a) there
// is source code, and either (b0) there is no object code,
// or (b1) there is object code, but it's older than the source
if (javaFile.exists() &&
(!classFile.exists()
javaFile.lastModified() > classFile.lastModified())) {
try {
// Try to compile it. If this doesn't work, then
// we must declare failure. (It's not good enough to use
// and already-existing, but out-of-date, classfile)
if (!compile( javaFilename ) !classFile.exists()) {
throw new ClassNotFoundException( "Compile failed: "+javaFilename );
}
} catch( IOException ie ) {
// Another place where we might come to if we fail
// to compile
throw new ClassNotFoundException( ie.toString() );
}
}
// Let's try to load up the raw bytes, assuming they were
// properly compiled, or didn't need to be compiled
try {
// read the bytes
byte raw[] = getBytes( classFilename );
// try to turn them into a class
clas = defineClass( name, raw, 0, raw.length );
} catch( IOException ie ) {
// This is not a failure! If we reach here, it might
// mean that we are dealing with a class in a library,
// sUCh as java.lang.Object
}
//System.out.println( "defineClass: "+clas );
// Maybe the class is in a library -- try loading
// the normal way
if (clas==null) {
clas = findSystemClass( name );
}
//System.out.println( "findSystemClass: "+clas );
// Resolve the class, if any, but only if the "resolve"
// flag is set to true
if (resolve && clas != null)
resolveClass( clas );
// If we still don't have a class, it's an error
if (clas == null)
throw new ClassNotFoundException( name );
// Otherwise, return the class
return clas;
}
}

CCRun.java
以下是 CCRun.java 的源代码

// $Id$
import java.lang.reflect.*;
/*
CCLRun executes a Java program by loading it through a
CompilingClassLoader.
*/
public class CCLRun
{
static public void main( String args[] ) throws Exception {
// The first argument is the Java program (class) the user
// wants to run
String progClass = args[0];
// And the arguments to that program are just
// arguments 1..n, so separate those out into
// their own array
String progArgs[] = new String[args.length-1];
System.arraycopy( args, 1, progArgs, 0, progArgs.length );
// Create a CompilingClassLoader
CompilingClassLoader ccl = new CompilingClassLoader();
// Load the main class through our CCL
Class clas = ccl.loadClass( progClass );
// Use reflection to call its main() method, and to
// pass the arguments in.
// Get a class representing the type of the main method's argument
Class mainArgType[] = { (new String[0]).getClass() };
// Find the standard main method in the class
Method main = clas.getMethod( "main", mainArgType );
// Create a list containing the arguments -- in this case,
// an array of strings
Object argsArray[] = { progArgs };
// Call the method
main.invoke( null, argsArray );
}
}

Foo.java
以下是 Foo.java 的源代码

// $Id$
public class Foo
{
static public void main( String args[] ) throws Exception {
System.out.println( "foo! "+args[0]+" "+args[1] );
new Bar( args[0], args[1] );
}
}

Bar.java
以下是 Bar.java 的源代码

// $Id$
import baz.*;
public class Bar
{
public Bar( String a, String b ) {
System.out.println( "bar! "+a+" "+b );
new Baz( a, b );
try {
Class booClass = Class.forName( "Boo" );
Object boo = booClass.newInstance();
} catch( Exception e ) {
e.printStackTrace();
}
}
}

baz/Baz.java
以下是 baz/Baz.java 的源代码

// $Id$
package baz;
public class Baz
{
public Baz( String a, String b ) {
System.out.println( "baz! "+a+" "+b );
}
}

Boo.java
以下是 Boo.java 的源代码

// $Id$
public class Boo
{
public Boo() {
System.out.println( "Boo!" );
}
}

Wednesday, March 11, 2009

Log4J

Log4J采用类似C语言中的printf函数的打印格式格式化日志信息,打印参数见表1如下:
%m 输出代码中指定的消息 %p 输出优先级,即DEBUG,INFO,WARN,ERROR,FATAL %r 输出自应用启动到输出该log信息耗费的毫秒数 %c 输出所属的类目,通常就是所在类的全名 %t 输出产生该日志事件的线程名 %n 输出一个回车换行符,Windows平台为“\r\n”,Unix平台为“\n” %d 输出日志时间点的日期或时间,默认格式为ISO8601,也可以在其后指定格式,比如:%d{yyy MMM dd HH:mm:ss,SSS},输出类似:2002年10月18日 22:10:28,921 %l 输出日志事件的发生位置,包括类目名、发生的线程,以及在代码中的行数。举例:Testlog4.main(TestLog4.java:10)
  
基本应用
Log4J的配置
现在来看log4j.properties配置文件的意义。第一行指定了根Logger的级别是DEBUG,并将此指定输出到A1。A1就是第二行定义的org.apache.log4j.ConsoleAppender,此行表示将A1输出到控制台。第三行规定了输出到A1的格式为org.apache.log4j.PatternLayout。第四行规定了输出到A1格式的转换模式为org.javaresearch.log4j.TestLog4J。
很多成熟的服务器类的软件日志信息会输出到控制台,同时输出到日志文件备查。使用Log4J可以在不改变任何代码的情况下,仅通过修改配置文件就可以轻松地完成这项功能。相关配置文件如下:
#### Use two appenders, one to log to console, another to log to a filelog4j.rootCategory=debug, stdout, R
# Print only messages of priority WARN or higher for your categorylog4j.category.your.category.name=WARN
#### First appender writes to consolelog4j.appender.stdout=org.apache.log4j.ConsoleAppenderlog4j.appender.stdout.layout=org.apache.log4j.PatternLayout
# Pattern to output the caller's file name and line number.log4j.appender.stdout.layout.ConversionPattern=%5p [%t] (%F:%L) - %m%n
#### Second appender writes to a filelog4j.appender.R=org.apache.log4j.RollingFileAppenderlog4j.appender.R.File=example.log
# Control the maximum log file sizelog4j.appender.R.MaxFileSize=100KB# Archive log files (one backup file here)log4j.appender.R.MaxBackupIndex=1
log4j.appender.R.layout=org.apache.log4j.PatternLayoutlog4j.appender.R.layout.ConversionPattern=%p %t %c - %m%n

这个配置文件指定了两个输出源stdout和R。前者把日志信息输出到控制台,后者是一个轮转日志文件。最大的文件是100KB,当一个日志文件达到最大尺寸时,Log4J会自动把example.log重命名为example.log.1,然后重建一个新的example.log文件,依次轮转。
在Web应用中使用
在Web应用中,应该在哪儿对Log4J进行配置呢?首先要明确,Log4J必须在应用的其它代码执行前完成初始化。因为Servlet是在Web服务器启动时立即装入的,所以,在Web应用中一般使用一个专门的Servlet来完成Log4J的配置,并保证在web.xml的配置中,这个Servlet位于其它Servlet之前。下面是一个例子,代码如下:
package org.javaresearch.log4j;import java.io.*;import javax.servlet.*;import org.apache.log4j.*;public class Log4JInit extends HttpServlet {public void init() throws ServletException {String prefix = getServletContext().getRealPath("/");String file = getServletConfig().getInitParameter("log4j-config-file");// 从Servlet参数读取log4j的配置文件 if (file != null) {PropertyConfigurator.configure(prefix + file);}}public void doGet(HttpServletRequest request,HttpServletResponse response)throws IOException, ServletException {}public void doPost(HttpServletRequest request,HttpServletResponse response)throws IOException, ServletException {}}

log4jinit org.javaresearch. log4j.Log4JInit log4j-config-file /properties/log4j.properties 1

注意:上面的load-on-startup应设为1,以便在Web容器启动时即装入该Servlet。log4j.properties文件放在根的properties子目录中,也可以把它放在其它目录中。应该把.properties文件集中存放,这样方便管理。