Java编程极限考验:ClassLoader类装载策略

个人认为,Java编程中极限考验是Classloader机制的掌握和灵活运用,特别是在复杂的系统,如存在动态类装载,Reflect,EJB,AOP等环境。

CLass.forName()
和Thread.currentThread().getContextClassLoader())
是否一样?

在很多文章中,都认为两者是一致的,如Java研究组织中一篇文章,被我从google搜索到的:
http://www.javaresearch.org/article/showarticle.jsp?column=31&thread=10178
文中说"这个方法可以用Class.forName()代替",在一般简单情况是可以替代,但实际上有时候是不能替代的。

Classloader存在下面问题:
在一个JVM中可能存在多个ClassLoader,每个ClassLoader拥有自己的NameSpace。一个ClassLoader只能拥有一个class对象类型的实例,但是不同的ClassLoader可能拥有相同的class对象实例,这时可能产生致命的问题。如ClassLoaderA,装载了类A的类型实例A1,而ClassLoaderB,也装载了类A的对象实例A2。逻辑上讲A1=A2,但是由于A1和A2来自于不同的ClassLoader,它们实际上是完全不同的,如果A中定义了一个静态变量c,则c在不同的ClassLoader中的值是不同的。

因此,研究JBoss的ClassLoader策略,对于更好地实现EJB组件拼装是用好处的,因为,一个项目中可能要用其他项目的EJB组件,如何实现运行时EJB组件共享,如何实现EJB组件打包是很重要的。


为了说明ClassLoader对于复杂架构是至重关键,列举开源Portal产品Exo中ServivesManager类内容。

该类是Exo利用PicoCOntainer实现功能性Service JavaBeans初始化,在将那些Service性质的JavaBeans加载到pico中时,需要使用到Classloader,
Exo专门设立一个ServiceContext类:



public class ServiceContext {

private ClassLoader cl; //包含Classloader信息
private Services services;

public ServiceContext(ClassLoader cl, Services services) {
this.cl = cl;
this.services = services;
}

public ClassLoader getCl() {
return cl;
}

public Services getServices() {
return services;
}

}

在ServicesManager中,有:
private ClassLoader updatedClassLoader;

它的初始值是:
Thread.currentThread().getContextClassLoader();

如果,这里写Class.forName 那么简单,那么你头疼去吧。

但是这样不够:
在addService方法中,根据加入的不同ServiceContext实现类装载:



public void addService(ServiceContext context) {
Services servicesToAdd = context.getServices();
String name = servicesToAdd.getName();
URLClassLoader cl = null;
if(context.getCl() instanceof URLClassLoader) {
cl = (URLClassLoader) context.getCl();
} else {
cl = URLClassLoader.newInstance(new URL[]{}, context.getCl());
}

updatedClassLoader = new URLClassLoader(cl.getURLs(), updatedClassLoader);
synchronized (servicesContext) {
servicesContext.put(name, context);
reloadContainer();
}
}

其实向Picocontainer中加入一个服务很简单,上述方法的主要代码是处理Classloader,考虑到Classloader有嵌套关系,上述代码小心使用这个Service服务的父Classloader,使用父Classloader装载服务Service。


希望有兴趣者一起讨论。

下面一段代码 是在Web容器中,实现Servlet类或Jsp类的动态装载,一般Servlet类部署到Web容器,是由Web容器监测Web.xml是否变化,<br>以决定是否装载,这是非常被动的,使用下面代码通过ClassLoader可以在自己程序中主动实现装载:<br>


public class WrapperServlet extends Servlet {
public void init(ServletConfig config) {
m_config = config; // stored as you don't want this to change
}

public void doGet(...) throws ServletException {
getServlet().doGet(...);
}

private Servlet getServlet() throws ServletException {
if ((m_servlet == null) || haveClassesChanged()) {
创造一个独立的classloader 确保它的父是我们的自己的ClassLoader<br>
path_to_separate_classes是将被装载的路

URLClassLoader loader = new URLClassLoader(new URL[] {
new URL(
"path_to_separate_classes");
}, WrapperServlet.class.getClassLoader());

从被装载的路径使用自己装载器来装载Servlet类
Class servletClass =
loader.loadClass(m_config.getInitParameter(
"servletClass");
m_servlet = (Servlet)servletClass.newInstance();
m_servlet.init(m_config);
}
return m_servlet;
}

private boolean haveClassesChanged() {
// determine whether classes have changed somehow
}

private ServletConfig m_config;
private Servlet m_servlet = null;
}

以前写的一篇文章:http://www.blogbus.com/blogbus/blog/diary.php?diaryid=153255,可供参考。

嗯,难道需要编写通用的ClassLoader查询类?我写了一个...


// 摘自javax.xml.parsers.FactoryFinder
public final class ClassLoaderFinder {
private ClassLoaderFinder() {
}

/**
* Figure out which ClassLoader to use. For JDK 1.2 and later use the
* context ClassLoader if possible. Note: we defer linking the class
* that calls an API only in JDK 1.2 until runtime so that we can catch
* LinkageError so that this code will run in older non-Sun JVMs such
* as the Microsoft JVM in IE.
*/

public static ClassLoader findClassLoader() throws ConfigurationError {
ClassLoader classLoader;
try {
// Construct the name of the concrete class to instantiate
Class clazz = Class.forName(ClassLoaderFinder.class.getName()
+
"$ClassLoaderFinderConcrete");
ContextClassLoaderFinder clf = (ContextClassLoaderFinder) clazz.
newInstance();
classLoader = clf.getContextClassLoader();
}
catch (LinkageError le) {
// Assume that we are running JDK 1.1, use the current ClassLoader
classLoader = ClassLoaderFinder.class.getClassLoader();
}
catch (ClassNotFoundException x) {
// This case should not normally happen. MS IE can throw this
// instead of a LinkageError the second time Class.forName() is
// called so assume that we are running JDK 1.1 and use the
// current ClassLoader
classLoader = ClassLoaderFinder.class.getClassLoader();
}
catch (Exception x) {
// Something abnormal happened so throw an error
throw new ConfigurationError(x.toString(), x);
}
return classLoader;
}

static class ConfigurationError
extends Error {
private Exception exception;

/**
* Construct a new instance with the specified detail string and
* exception.
*/

ConfigurationError(String msg, Exception x) {
super(msg);
this.exception = x;
}

Exception getException() {
return exception;
}
}

/*
* The following nested classes allow getContextClassLoader() to be
* called only on JDK 1.2 and yet run in older JDK 1.1 JVMs
*/


private static abstract class ContextClassLoaderFinder {
abstract ClassLoader getContextClassLoader();
}

static class ClassLoaderFinderConcrete
extends ContextClassLoaderFinder {
ClassLoader getContextClassLoader() {
return Thread.currentThread().getContextClassLoader();
}
}

//Sample
public static void main(String args[]) {
try {

final String CLASS_NAME =
"oracle.jdbc.driver.OracleDriver";
Class spiClass;
ClassLoader classLoader = ClassLoaderFinder.findClassLoader();
if (classLoader == null) {
System.out.println(
"From Class.forName");
spiClass = Class.forName(CLASS_NAME);
}
else {
System.out.println(
"From ClassLoaderFinder");
spiClass = classLoader.loadClass(CLASS_NAME);
spiClass.newInstance();
}

java.sql.Connection conn = java.sql.DriverManager.getConnection(
"jdbc:oracle:thin:@localhost:1521:ORCL",
"scott",
"tiger");
java.sql.Statement stmt = conn.createStatement();
java.sql.ResultSet rs = stmt.executeQuery(
"select * from cat");
while (rs.next()) {
System.out.println(rs.getString(2));
}
}
catch (Exception e) {
e.printStackTrace();
}
}

}

大家还是看深入java虚拟机那本书把

说的很详细了

我不想再多说了!

深入java虚拟机只是从原理机制谈了ClassLoader,关键是应用,面对复杂环境的应用,比如别人的框架已经有ClassLoader,你在别人的框架下还要ClassLoader,你就得注意父子关系,但是你也可以建立并行关系。

在你决定如何建立你的CLassLoader策略时,你必须对你基于的框架内部有了解,因为你的应用和JVM之间有很多层,有EJB容器层 有你的框架层,你也必须了解这中间层的ClassLoader策略,只是掌握JVM还不够的。

简单地说,如果你基于AOP框架或EJB容器实现ClassLoader,那么你必须对你的AOP框架或EJB容器的ClassLoader策略掌握,Weblogic和JBoss 3.2机制就不一样,各个服务器都不同,不同版本不同,如果你不注意这些变化,那么你的应用系统迁移就有问题,甚至无法实现迁移。

to cats_tiger
你做了findClassLoader,但是没有构造自己的ClassLoader,还是有漏洞,你的Classloader是使用Class.forName,这个问题在复杂环境下会有问题,特别是动态代理Proxy下不正常,有时会报ClassCastException错误。这也是很多人对动态代理感到无法掌握的原因之一。

to banq
难道要这样?好像太麻烦了。


package cloader;

public final class ClassLoaderFinder {
private ClassLoaderFinder() {
}

/**
* The following nested classes extends the <tt>ClassLoader</tt> so that we
* can create our ClassLoader freely.
*/

private static class PersonalClassLoader extends ClassLoader {
public PersonalClassLoader(ClassLoader parentLoader) {
super(parentLoader);
}

public PersonalClassLoader() {
//Use the external class's <tt>ClassLoader</tt>
super(ClassLoaderFinder.class.getClassLoader());
}
}
/**
* Figure out which ClassLoader to use. For JDK 1.2 and later use the
* context ClassLoader if possible. Note: we defer linking the class
* that calls an API only in JDK 1.2 until runtime so that we can catch
* LinkageError so that this code will run in older non-Sun JVMs such
* as the Microsoft JVM in IE.
*/

public static ClassLoader findClassLoader() throws ConfigurationError {
ClassLoader classLoader;
try {
// Construct the name of the concrete class to instantiate
PersonalClassLoader pLoader = new PersonalClassLoader();
Class clazz = pLoader.loadClass(ClassLoaderFinder.class.getName()
+
"$ClassLoaderFinderConcrete");
//Class clazz = Class.forName(ClassLoaderFinder.class.getName()
// + "$ClassLoaderFinderConcrete");
ContextClassLoaderFinder clf = (ContextClassLoaderFinder) clazz.
newInstance();
classLoader = clf.getContextClassLoader();
}
catch (LinkageError le) {
// Assume that we are running JDK 1.1, use the current ClassLoader
classLoader = ClassLoaderFinder.class.getClassLoader();
}
catch (ClassNotFoundException x) {
// This case should not normally happen. MS IE can throw this
// instead of a LinkageError the second time Class.forName() is
// called so assume that we are running JDK 1.1 and use the
// current ClassLoader
classLoader = ClassLoaderFinder.class.getClassLoader();
}
catch (Exception x) {
// Something abnormal happened so throw an error
throw new ConfigurationError(x.toString(), x);
}
return classLoader;
}

static class ConfigurationError
extends Error {
private Exception exception;

/**
* Construct a new instance with the specified detail string and
* exception.
*/

ConfigurationError(String msg, Exception x) {
super(msg);
this.exception = x;
}

Exception getException() {
return exception;
}
}

/*
* The following nested classes allow getContextClassLoader() to be
* called only on JDK 1.2 and yet run in older JDK 1.1 JVMs
*/


private static abstract class ContextClassLoaderFinder {
abstract ClassLoader getContextClassLoader();
}

static class ClassLoaderFinderConcrete
extends ContextClassLoaderFinder {
ClassLoader getContextClassLoader() {
return Thread.currentThread().getContextClassLoader();
}
}

//Sample

}

实在是麻烦,绕来绕去的。
看来怎样处理ClassLoader,要根据具体的应用具体分析,从运行环境(Container)、用途等方面综合考虑。
绝大多数应用系统Class.forName就足够了。

>绝大多数应用系统Class.forName就足够了
这个定义如果存在的话,Exo的ServiceManager就不必这么复杂,Exo作为一个设计概念非常前卫的开源系统,在ServiceManager将ClassLoader搞得很复杂是有道理的,是经验的总结,使得自己的系统伸缩性强,健壮性更强,这些都是严格要求的设计目标所致。

这里我想讨论的是,不怕复杂,解剖其本质。

另外,自己定义ClassLoader象上面的代码是可以,但是在具体情况时还要注意,可能需要将原来的ClassLoader机制暂存,用自己定义的ClassLoader,然后再恢复原来的ClassLoader。

ClassLoader其实是很简单的,以树型来组织。我看是楼上的各位把把简单的问题搞复杂了。

> ClassLoader其实是很简单的,以树型来组织。我看是楼上的?> 位把把简单的问题搞复杂了。

这个帖子的目的主要是研究一下ClassLoader的应用,而不是它的原理,所以复杂一些(很多原理简单的东西,用起来却复杂,例如围棋)。
但是,如果你的应用可能会在不同的JVM(例如MS JVM)下或不同的Container中运行,就要考虑ClassLoader的问题了。比如基于xml的静态工厂类,通常用Class.forName就行,但是在有些情况下却出现异常。

> 这个帖子的目的主要是研究一下ClassLoader的应用,而不是?> 的原理,所以复杂一些(很多原理简单的东西,用起来却复杂
> 缥澹?> 但是,如果你的应用可能会在不同的JVM(例如MS
> JVM)下或不同的Container中运行,就要考虑ClassLoader的?> 题了。比如基于xml的静态工厂类,通常用Class.forName就行
> 窃谟行┣榭鱿氯闯鱿忠斐!?

不知道你说的基于xml的静态工厂类是怎么回事。
装入类和是否通过Class.forName没有必然的联系。
任何一个类装入都有一个运行环境和边界,搞清楚这个就行,没那么复杂。类要在该装入的地方装入。每个ClassLoader指的就是一个运行环境,每个ClassLoader也都有一个边界,类必须在这个边界内运行。
如果在不同的JVM下运行(不管是Sun的JVM还是MS的JVM,于此无关),应该和ClassLoader没有关系。比如一个分布式的Web应用,类的装入都在本地,和其他的JVM无关。如果考虑集群的问题,就不是ClassLoader的问题了。这一块没有做过,请各位指正。

>不知道你说的基于xml的静态工厂类是怎么回事。
>装入类和是否通过Class.forName没有必然的联系。
就是读取xml中定义的类名,然后实例化,返回父类型的那种工厂。
>任何一个类装入都有一个运行环境和边界,搞清楚这个就行,没那么复杂。
问题是有时候没有那么容易搞清楚。建议参考javax.xml.parsers.DocumentBuilderFactory的源代码

那就先搞清楚再装入,呵呵。没有必要纠缠在ClassLoader上,把ClassLoader搞复杂了,并没有任何的好处。
另外,parser并没有你说得那么复杂。哈哈