转载于深入理解Java类加载器(1):Java类加载原理解析 、 深入理解Java类加载器(2):线程上下文类加载器
启动(Bootstrap)类加载器、扩展(Extension)类加载器、系统(System)类加载器。
public class LoaderTest { public static void main(String[] args) { try { System.out.println(ClassLoader.getSystemClassLoader()); System.out.println(ClassLoader.getSystemClassLoader().getParent()); System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent()); } catch (Exception e) { e.printStackTrace(); } } }
说明:通过java.lang.ClassLoader.getSystemClassLoader()可以直接获取到系统类加载器。
代码输出如下:
sun.misc.Launcher$AppClassLoader@6d06d69c sun.misc.Launcher$ExtClassLoader@70dea4e null
在Java中,一个类用其完全匹配类名(fully qualified class name)作为标识,这里指的完全匹配类名包括包名和类名。但在JVM中一个类用其全名和一个加载类ClassLoader的实例作为唯一标识,不同类加载器加载的类将被置于不同的命名空间 我们可以用两个自定义类加载器去加载某自定义类型(注意不要将自定义类型的字节码放置到系统路径或者扩展路径中,否则会被系统类加载器或扩展类加载器抢先加载),然后用获取到的两个Class实例进行java.lang.Object.equals(…)判断,将会得到不相等的结果。这个大家可以写两个自定义的类加载器去加载相同的自定义类型,然后做个判断;同时,可以测试加载java.*类型,然后再对比测试一下测试结果。
Class.forName(String name)默认会使用调用类的类加载器来进行类加载。我们直接来分析一下对应的jdk的代码:
//java.lang.Class.java publicstatic Class<?> forName(String className) throws ClassNotFoundException { return forName0(className, true, ClassLoader.getCallerClassLoader()); } //java.lang.ClassLoader.java // Returns the invoker's class loader, or null if none. static ClassLoader getCallerClassLoader() { // 获取调用类(caller)的类型 Class caller = Reflection.getCallerClass(3); // This can be null if the VM is requesting it if (caller == null) { return null; } // 调用java.lang.Class中本地方法获取加载该调用类(caller)的ClassLoader return caller.getClassLoader0(); } //java.lang.Class.java //虚拟机本地实现,获取当前类的类加载器,前面介绍的Class的getClassLoader()也使用此方法 native ClassLoader getClassLoader0();
前面讲过,在不指定父类加载器的情况下,默认采用系统(System)类加载器。可能有人觉得不明白,现在我们来看一下JDK对应的代码实现。众所周知,我们编写自定义的类加载器直接或者间接继承自java.lang.ClassLoader抽象类,对应的无参默认构造函数实现如下:
//摘自java.lang.ClassLoader.java protected ClassLoader() { SecurityManager security = System.getSecurityManager(); if (security != null) { security.checkCreateClassLoader(); } this.parent = getSystemClassLoader(); initialized = true; }
我们再来看一下对应的getSystemClassLoader()方法的实现:
private static synchronized void initSystemClassLoader() { //... sun.misc.Launcher l = sun.misc.Launcher.getLauncher(); scl = l.getClassLoader(); //... }
我们可以写简单的测试代码来测试一下:
System.out.println(sun.misc.Launcher.getLauncher().getClassLoader());
本机对应输出如下:
sun.misc.Launcher$AppClassLoader@73d16e93
所以,我们现在可以相信当自定义类加载器没有指定父类加载器的情况下,默认的父类加载器即为系统类加载器。同时,我们可以得出如下结论: 即使用户自定义类加载器不指定父类加载器,那么,同样可以加载如下三个地方的类:
JVM规范中规定如果用户自定义的类加载器将父类加载器强制设置为null,那么会自动将启动类加载器设置为当前用户自定义类加载器的父类加载器(这个问题前面已经分析过了)。同时,我们可以得出如下结论:
即使用户自定义类加载器不指定父类加载器,那么,同样可以加载到<Java_Runtime_Home>/lib下的类,但此时就不能够加载<Java_Runtime_Home>/lib/ext目录下的类了。
说明:问题3和问题4的推断结论是基于用户自定义的类加载器本身延续了java.lang.ClassLoader.loadClass(…)默认委派逻辑,如果用户对这一默认委派逻辑进行了改变,以上推断结论就不一定成立了,详见问题5。
(1)一般尽量不要覆写已有的loadClass(…)方法中的委派逻辑
一般在JDK 1.2之前的版本才这样做,而且事实证明,这样做极有可能引起系统默认的类加载器不能正常工作。在JVM规范和JDK文档中(1.2或者以后版本中),都没有建议用户覆写loadClass(…)方法,相比而言,明确提示开发者在开发自定义的类加载器时覆写findClass(…)逻辑。举一个例子来验证该问题:
//用户自定义类加载器WrongClassLoader.Java(覆写loadClass逻辑) public class WrongClassLoader extends ClassLoader { public Class<?> loadClass(String name) throws ClassNotFoundException { return this.findClass(name); } protected Class<?> findClass(String name) throws ClassNotFoundException { // 假设此处只是到工程以外的特定目录D:\library下去加载类 // 具体实现代码省略 } }
通过前面的分析我们已经知道,这个自定义类加载器WrongClassLoader的默认类加载器是系统类加载器,但是现在问题4种的结论就不成立了。大家可以简单测试一下,现在<Java_Runtime_Home>/lib、< Java_Runtime_Home >/lib/ext和工程类路径上的类都加载不上了。
//问题5测试代码一 public class WrongClassLoaderTest { publicstaticvoid main(String[] args) { try { WrongClassLoader loader = new WrongClassLoader(); Class classLoaded = loader.loadClass("beans.Account"); System.out.println(classLoaded.getName()); System.out.println(classLoaded.getClassLoader()); } catch (Exception e) { e.printStackTrace(); } } }
这里D:"classes"beans"Account.class是物理存在的。输出结果:
java.io.FileNotFoundException: D:"classes"java"lang"Object.class (系统找不到指定的路径。) at java.io.FileInputStream.open(Native Method) at java.io.FileInputStream.<init>(FileInputStream.java:106) at WrongClassLoader.findClass(WrongClassLoader.java:40) at WrongClassLoader.loadClass(WrongClassLoader.java:29) at java.lang.ClassLoader.loadClassInternal(ClassLoader.java:319) at java.lang.ClassLoader.defineClass1(Native Method) at java.lang.ClassLoader.defineClass(ClassLoader.java:620) at java.lang.ClassLoader.defineClass(ClassLoader.java:400) at WrongClassLoader.findClass(WrongClassLoader.java:43) at WrongClassLoader.loadClass(WrongClassLoader.java:29) at WrongClassLoaderTest.main(WrongClassLoaderTest.java:27) Exception in thread "main" java.lang.NoClassDefFoundError: java/lang/Object at java.lang.ClassLoader.defineClass1(Native Method) at java.lang.ClassLoader.defineClass(ClassLoader.java:620) at java.lang.ClassLoader.defineClass(ClassLoader.java:400) at WrongClassLoader.findClass(WrongClassLoader.java:43) at WrongClassLoader.loadClass(WrongClassLoader.java:29) at WrongClassLoaderTest.main(WrongClassLoaderTest.java:27)
这说明,连要加载的类型的超类型java.lang.Object都加载不到了。这里列举的由于覆写loadClass()引起的逻辑错误明显是比较简单的,实际引起的逻辑错误可能复杂的多。
//问题5测试二 //用户自定义类加载器WrongClassLoader.Java(不覆写loadClass逻辑) public class WrongClassLoader extends ClassLoader { protected Class<?> findClass(String name) throws ClassNotFoundException { //假设此处只是到工程以外的特定目录D:\library下去加载类 //具体实现代码省略 } }
将自定义类加载器代码WrongClassLoader.Java做以上修改后,再运行测试代码,输出结果如下:
beans.Account WrongClassLoader@1c78e57
(2) 正确设置父类加载器
通过上面问题4和问题5的分析我们应该已经理解,个人觉得这是自定义用户类加载器时最重要的一点,但常常被忽略或者轻易带过。有了前面JDK代码的分析作为基础,我想现在大家都可以随便举出例子了。
(3)保证findClass(String name)方法的逻辑正确性
事先尽量准确理解待定义的类加载器要完成的加载任务,确保最大程度上能够获取到对应的字节码内容。
一是可以直接调用ClassLoader.getSystemClassLoader()或者其他方式获取到系统类加载器(系统类加载器和扩展类加载器本身都派生自URLClassLoader),调用URLClassLoader中的getURLs()方法可以获取到。
二是可以直接通过获取系统属性java.class.path来查看当前类路径上的条目信息 :System.getProperty(“java.class.path”)。
线程上下文类加载器(context class loader)是从 JDK 1.2 开始引入的。类 java.lang.Thread中的方法 getContextClassLoader()和 setContextClassLoader(ClassLoader cl)用来获取和设置线程的上下文类加载器。如果没有通过 setContextClassLoader(ClassLoader cl)方法进行设置的话,线程将继承其父线程的上下文类加载器。Java 应用运行的初始线程的上下文类加载器是系统类加载器。在线程中运行的代码可以通过此类加载器来加载类和资源。
hot swap即热插拔的意思,这里表示一个类已经被一个加载器加载了以后,在不卸载它的情况下重新再加载它一次。我们知道Java缺省的加载器对相同全名的类只会加载一次,以后直接从缓存中取这个Class object。因此要实现hot swap,必须在加载的那一刻进行拦截,先判断是否已经加载,若是则重新加载一次,否则直接首次加载它。 我们从URLClassLoader继承,加载类的过程都代理给系统类加载器URLClassLoader中的相应方法来完成。
package classloader; import java.net.URL; import java.net.URLClassLoader; /** * 可以重新载入同名类的类加载器实现 * 放弃了双亲委派的加载链模式,需要外部维护重载后的类的成员变量状态 */ public class HotSwapClassLoader extends URLClassLoader { public HotSwapClassLoader(URL[] urls) { super(urls); } public HotSwapClassLoader(URL[] urls, ClassLoader parent) { super(urls, parent); } // 下面的两个重载load方法实现类的加载,仿照ClassLoader中的两个loadClass() // 具体的加载过程代理给父类中的相应方法来完成 public Class<?> load(String name) throws ClassNotFoundException { return load(name, false); } public Class<?> load(String name, boolean resolve) throws ClassNotFoundException { // 若类已经被加载,则重新再加载一次 if (null != super.findLoadedClass(name)) { return reload(name, resolve); } // 否则用findClass()首次加载它 Class<?> clazz = super.findClass(name); if (resolve) { super.resolveClass(clazz); } return clazz; } public Class<?> reload(String name, boolean resolve) throws ClassNotFoundException { return new HotSwapClassLoader(super.getURLs(), super.getParent()).load( name, resolve); } }
两个重载的load方法参数与ClassLoader类中的两个loadClass()相似。在load的实现中,用findLoadedClass()查找指定的类是否已经被祖先加载器加载了,若已加载则重新再加载一次,从而放弃了双亲委派的方式(这种方式只会加载一次)。若没有加载则用自身的findClass()来首次加载它。
下面是使用示例:
package classloader; public class A { private B b; public void setB(B b) { this.b = b; } public B getB() { return b; } }
package classloader; public class B { }
package classloader; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.MalformedURLException; import java.net.URL; public class TestHotSwap { public static void main(String args[]) throws MalformedURLException { A a = new A(); // 加载类A B b = new B(); // 加载类B a.setB(b); // A引用了B,把b对象拷贝到A.b System.out.printf("A classLoader is %s\n", a.getClass().getClassLoader()); System.out.printf("B classLoader is %s\n", b.getClass().getClassLoader()); System.out.printf("A.b classLoader is %s\n", a.getB().getClass().getClassLoader()); try { URL[] urls = new URL[]{ new URL("file:///C:/Users/JackZhou/Documents/NetBeansProjects/classloader/build/classes/") }; HotSwapClassLoader c1 = new HotSwapClassLoader(urls, a.getClass().getClassLoader()); Class clazz = c1.load("classloader.A"); // 用hot swap重新加载类A Object aInstance = clazz.newInstance(); // 创建A类对象 Method method1 = clazz.getMethod("setB", B.class); // 获取setB(B b)方法 method1.invoke(aInstance, b); // 调用setB(b)方法,重新把b对象拷贝到A.b Method method2 = clazz.getMethod("getB"); // 获取getB()方法 Object bInstance = method2.invoke(aInstance); // 调用getB()方法 System.out.printf("Reloaded A.b classLoader is %s\n", bInstance.getClass().getClassLoader()); } catch (MalformedURLException | ClassNotFoundException | InstantiationException | IllegalAccessException | NoSuchMethodException | SecurityException | IllegalArgumentException | InvocationTargetException e) { e.printStackTrace(); } } }
运行输出:
A classLoader is sun.misc.Launcher$AppClassLoader@73d16e93 B classLoader is sun.misc.Launcher$AppClassLoader@73d16e93 A.b classLoader is sun.misc.Launcher$AppClassLoader@73d16e93 Reloaded A.b classLoader is sun.misc.Launcher$AppClassLoader@73d16e93
HotSwapClassLoader加载器的作用是重新加载同名的类。为了实现hot swap,一个类在加载过后,若重新再加载一次,则新的Class object的状态会改变,老的状态数据需要通过其他方式拷贝到重新加载过的类生成的全新Class object实例中来。上面A类引用了B类,加载A时也会加载B(如果B已经加载,则直接从缓存中取出)。在重新加载A后,其Class object中的成员b会重置,因此要重新调用setB(b)拷贝一次。你可以注释掉这行代码,再运行会抛出java.lang.NullPointerException,指示A.b为null。
注意新的A Class object实例所依赖的B类Class object,如果它与老的B Class object实例不是同一个类加载器加载的, 将会抛出类型转换异常(ClassCastException),表示两种不同的类。因此在重新加载A后,要特别注意给它的B类成员b传入外部值时,它们是否由同一个类加载器加载。为了解决这种问题, HotSwapClassLoader自定义的l/oad方法中,当前类(类A)是由自身classLoader加载的, 而内部依赖的类(类B)还是老对象的classLoader加载的。
这是一个很常见的问题,但答案却很难回答。这个问题通常在需要动态加载类和资源的系统编程时会遇到。总的说来动态加载资源时,往往需要从三种类加载器里选择:系统或程序的类加载器、当前类加载器、以及当前线程的上下文类加载器。在程序中应该使用何种类加载器呢?
系统类加载器通常不会使用。此类加载器处理启动应用程序时classpath指定的类,可以通过ClassLoader.getSystemClassLoader()来获得。所有的ClassLoader.getSystemXXX()接口也是通过这个类加载器加载的。一般不要显式调用这些方法,应该让其他类加载器代理到系统类加载器上。 由于系统类加载器是JVM最后创建的类加载器,这样代码只会适应于简单命令行启动的程序。一旦代码移植到EJB、Web应用或者Java Web Start应用程序中,程序肯定不能正确执行。
因此一般只有两种选择,当前类加载器和线程上下文类加载器。当前类加载器是指当前方法所在类的加载器。这个类加载器是运行时类解析使用的加载器,Class.forName(String)和Class.getResource(String)也使用该类加载器。代码中X.class的写法使用的类加载器也是这个类加载器。
线程上下文类加载器在Java 2(J2SE)时引入。每个线程都有一个关联的上下文类加载器。如果你使用new Thread()方式生成新的线程,新线程将继承其父线程的上下文类加载器。如果程序对线程上下文类加载器没有任何改动的话,程序中所有的线程将都使用系统类加载器作为上下文类加载器。Web应用和Java企业级应用中,应用服务器经常要使用复杂的类加载器结构来实现JNDI(Java命名和目录接口)、线程池、组件热部署等功能,因此理解这一点尤其重要。
为什么要引入线程的上下文类加载器? 将它引入J2SE并不是纯粹的噱头,由于Sun没有提供充分的文档解释说明这一点,这使许多开发者很糊涂。实际上,上下文类加载器为同样在J2SE中引入的类加载代理机制提供了后门。<.font> 通常JVM中的类加载器是按照层次结构组织的,目的是每个类加载器(除了启动整个JVM的原初类加载器)都有一个父类加载器。当类加载请求到来时,类加载器通常首先将请求代理给父类加载器。只有当父类加载器失败后,它才试图按照自己的算法查找并定义当前类。
有时这种模式并不能总是奏效。这通常发生在JVM核心代码必须动态加载由应用程序动态提供的资源时。拿JNDI为例,它的核心是由JRE核心类(rt.jar)实现的。但这些核心JNDI类必须能加载由第三方厂商提供的JNDI实现。这种情况下调用父类加载器(原初类加载器)来加载只有其子类加载器可见的类,这种代理机制就会失效。解决办法就是让核心JNDI类使用线程上下文类加载器,从而有效的打通类加载器层次结构,逆着代理机制的方向使用类加载器。
顺便提一下,XML解析API(JAXP)也是使用此种机制。当JAXP还是J2SE扩展时,XML解析器使用当前类加载器方法来加载解析器实现。但当JAXP成为J2SE核心代码后,类加载机制就换成了使用线程上下文加载器,这和JNDI的原因相似。
好了,现在我们明白了问题的关键:这两种选择不可能适应所有情况。一些人认为线程上下文类加载器应成为新的标准。但这在不同JVM线程共享数据来沟通时,就会使类加载器的结构乱七八糟。除非所有线程都使用同一个上下文类加载器。而且,使用当前类加载器已成为缺省规则,它们广泛应用在类声明、Class.forName等情景中。即使你想尽可能只使用上下文类加载器,总是有这样那样的代码不是你所能控制的。这些代码都使用代理到当前类加载器的模式。混杂使用代理模式是很危险的。
更为糟糕的是,某些应用服务器将当前类加载器和上下文类加器分别设置成不同的ClassLoader实例。虽然它们拥有相同的类路径,但是它们之间并不存在父子代理关系。想想这为什么可怕:记住加载并定义某个类的类加载器是虚拟机内部标识该类的组成部分,如果当前类加载器加载类X并接着执行它,如JNDI查找类型为Y的数据,上下文类加载器能够加载并定义Y,这个Y的定义和当前类加载器加载的相同名称的类就不是同一个,使用隐式类型转换就会造成异常。
这种混乱的状况还将在Java中存在很长时间。在J2SE中还包括以下的功能使用不同的类加载器:
(1)JNDI使用线程上下文类加载器。
(2)Class.getResource()和Class.forName()使用当前类加载器。
(3)JAXP使用上下文类加载器。
(4)java.util.ResourceBundle使用调用者的当前类加载器。
(5)URL协议处理器使用java.protocol.handler.pkgs系统属性并只使用系统类加载器。
(6)Java序列化API缺省使用调用者当前的类加载器。
这些类加载器非常混乱,没有在J2SE文档中给以清晰明确的说明。
该如何选择类加载器?
如若代码是限于某些特定框架,这些框架有着特定加载规则,则不要做任何改动,让框架开发者来保证其工作(比如应用服务器提供商,尽管他们并不能总是做对)。如在Web应用和EJB中,要使用Class.gerResource来加载资源。
在其他情况下,我们可以自己来选择最合适的类加载器。可以使用策略模式来设计选择机制。其思想是将“总是使用上下文类加载器”或者“总是使用当前类加载器”的决策同具体实现逻辑分离开。往往设计之初是很难预测何种类加载策略是合适的,该设计能够让你可以后来修改类加载策略。
考虑使用下面的代码,这是作者本人在工作中发现的经验。这儿有一个缺省实现,应该可以适应大部分工作场景:
package classloader.context; /** * 类加载上下文,持有要加载的类 */ public class ClassLoadContext { private final Class m_caller; public final Class getCallerClass() { return m_caller; } ClassLoadContext(final Class caller) { m_caller = caller; } }
package classloader.context; /** * 类加载策略接口 */ public interface IClassLoadStrategy { ClassLoader getClassLoader(ClassLoadContext ctx); }
/** * 缺省的类加载策略,可以适应大部分工作场景 */ public class DefaultClassLoadStrategy implements IClassLoadStrategy { /** * 为ctx返回最合适的类加载器,从系统类加载器、当前类加载器 * 和当前线程上下文类加载中选择一个最底层的加载器 * @param ctx * @return */ @Override public ClassLoader getClassLoader(final ClassLoadContext ctx) { final ClassLoader callerLoader = ctx.getCallerClass().getClassLoader(); final ClassLoader contextLoader = Thread.currentThread().getContextClassLoader(); ClassLoader result; // If 'callerLoader' and 'contextLoader' are in a parent-child // relationship, always choose the child: if (isChild(contextLoader, callerLoader)) { result = callerLoader; } else if (isChild(callerLoader, contextLoader)) { result = contextLoader; } else { // This else branch could be merged into the previous one, // but I show it here to emphasize the ambiguous case: result = contextLoader; } final ClassLoader systemLoader = ClassLoader.getSystemClassLoader(); // Precaution for when deployed as a bootstrap or extension class: if (isChild(result, systemLoader)) { result = systemLoader; } return result; } // 判断anotherLoader是否是oneLoader的child private boolean isChild(ClassLoader oneLoader, ClassLoader anotherLoader){ //... } // ... more methods }
决定应该使用何种类加载器的接口是IClassLoaderStrategy,为了帮助IClassLoadStrategy做决定,给它传递了个ClassLoadContext对象作为参数。ClassLoadContext持有要加载的类。
上面代码的逻辑很简单:如调用类的当前类加载器和上下文类加载器是父子关系,则总是选择子类加载器。对子类加载器可见的资源通常是对父类可见资源的超集,因此如果每个开发者都遵循J2SE的代理规则,这样做大多数情况下是合适的。
当前类加载器和上下文类加载器是兄弟关系时,决定使用哪一个是比较困难的。理想情况下,Java运行时不应产生这种模糊。但一旦发生,上面代码选择上下文类加载器。这是作者本人的实际经验,绝大多数情况下应该能正常工作。你可以修改这部分代码来适应具体需要。一般来说,上下文类加载器要比当前类加载器更适合于框架编程,而当前类加载器则更适合于业务逻辑编程。
最后需要检查一下,以便保证所选类加载器不是系统类加载器的父亲,在开发标准扩展类库时这通常是个好习惯。
注意作者故意没有检查要加载资源或类的名称。Java XML API成为J2SE核心的历程应该能让我们清楚过滤类名并不是好想法。作者也没有试图检查哪个类加载器加载首先成功,而是检查类加载器的父子关系,这是更好更有保证的方法。
下面是类加载器的选择器:
package classloader.context; /** * 类加载解析器,获取最合适的类加载器 */ public abstract class ClassLoaderResolver { private static IClassLoadStrategy s_strategy; // initialized in <clinit> private static final int CALL_CONTEXT_OFFSET = 3; // may need to change if this class is redesigned private static final CallerResolver CALLER_RESOLVER; // set in <clinit> static { try { // This can fail if the current SecurityManager does not allow // RuntimePermission ("createSecurityManager"): CALLER_RESOLVER = new CallerResolver(); } catch (SecurityException se) { throw new RuntimeException("ClassLoaderResolver: could not create CallerResolver: " + se); } s_strategy = new DefaultClassLoadStrategy(); //默认使用缺省加载策略 } /** * This method selects the best classloader instance to be used for * class/resource loading by whoever calls this method. The decision * typically involves choosing between the caller's current, thread context, * system, and other classloaders in the JVM and is made by the {@link IClassLoadStrategy} * instance established by the last call to {@link #setStrategy}. * * @return classloader to be used by the caller ['null' indicates the * primordial loader] */ public static synchronized ClassLoader getClassLoader() { final Class caller = getCallerClass(0); // 获取执行当前方法的类 final ClassLoadContext ctx = new ClassLoadContext(caller); // 创建类加载上下文 return s_strategy.getClassLoader(ctx); // 获取最合适的类加载器 } public static synchronized IClassLoadStrategy getStrategy() { return s_strategy; } public static synchronized IClassLoadStrategy setStrategy(final IClassLoadStrategy strategy) { final IClassLoadStrategy old = s_strategy; // 设置类加载策略 s_strategy = strategy; return old; } /** * A helper class to get the call context. It subclasses SecurityManager * to make getClassContext() accessible. An instance of CallerResolver * only needs to be created, not installed as an actual security manager. */ private static final class CallerResolver extends SecurityManager { @Override protected Class[] getClassContext() { return super.getClassContext(); // 获取当执行栈的所有类,native方法 } } /* * Indexes into the current method call context with a given * offset. */ private static Class getCallerClass(final int callerOffset) { return CALLER_RESOLVER.getClassContext()[CALL_CONTEXT_OFFSET + callerOffset]; // 获取执行栈上某个方法所属的类 } }
可通过调用 ClassLoaderResolver.getClassLoader() 方法来获取类加载器对象,并使用其 ClassLoader 的接口如loadClass()等来加载类和资源。此外还可使用下面的 ResourceLoader 接口来取代 ClassLoader 接口:
package classloader.context; import java.net.URL; public class ResourceLoader { /** * 加载一个类 * * @param name * @return * @throws java.lang.ClassNotFoundException * @see java.lang.ClassLoader#loadClass(java.lang.String) */ public static Class<?> loadClass(final String name) throws ClassNotFoundException { //获取最合适的类加载器 final ClassLoader loader = ClassLoaderResolver.getClassLoader(); //用指定加载器加载类 return Class.forName(name, false, loader); } /** * 加载一个资源 * * @param name * @return * @see java.lang.ClassLoader#getResource(java.lang.String) */ public static URL getResource(final String name) { //获取最合适的类加载器 final ClassLoader loader = ClassLoaderResolver.getClassLoader(); //查找指定的资源 if (loader != null) { return loader.getResource(name); } else { return ClassLoader.getSystemResource(name); } } // ... more methods ... }
ClassLoadContext.getCallerClass()返回的类在ClassLoaderResolver或ResourceLoader使用,这样做的目的是让其能找到调用类的类加载器(上下文加载器总是能通过Thread.currentThread().getContextClassLoader()来获得)。注意调用类是静态获得的,因此这个接口不需现有业务方法增加额外的Class参数,而且也适合于静态方法和类初始化代码。具体使用时,可以往这个上下文对象中添加具体部署环境中所需的其他属性。
对于运行在 Java EE容器中的 Web 应用来说,类加载器的实现方式与一般的 Java 应用有所不同。不同的 Web 容器的实现方式也会有所不同。以 Apache Tomcat 来说,每个 Web 应用都有一个对应的类加载器实例。该类加载器也使用代理模式,所不同的是它是首先尝试去加载某个类,如果找不到再代理给父类加载器。这与一般类加载器的顺序是相反的。这是 Java Servlet 规范中的推荐做法,其目的是使得 Web 应用自己的类的优先级高于 Web 容器提供的类。 这种代理模式的一个例外是:Java 核心库的类是不在查找范围之内的。这也是为了保证 Java 核心库的类型安全。
绝大多数情况下,Web 应用的开发人员不需要考虑与类加载器相关的细节。下面给出几条简单的原则:
(1)每个 Web 应用自己的 Java 类文件和使用的库的 jar 包,分别放在 WEB-INF/classes和 WEB-INF/lib目录下面。
(2)多个应用共享的 Java 类文件和 jar 包,分别放在 Web 容器指定的由所有 Web 应用共享的目录下面。
(3)当出现找不到类的错误时,检查当前类的类加载器和当前线程的上下文类加载器是否正确。
OSGi是 Java 上的动态模块系统。它为开发人员提供了面向服务和基于组件的运行环境,并提供标准的方式用来管理软件的生命周期。OSGi 已经被实现和部署在很多产品上,在开源社区也得到了广泛的支持。Eclipse就是基于OSGi 技术来构建的。
OSGi 中的每个模块(bundle)都包含 Java 包和类。模块可以声明它所依赖的需要导入(import)的其它模块的 Java 包和类(通过 Import-Package),也可以声明导出(export)自己的包和类,供其它模块使用(通过 Export-Package)。也就是说需要能够隐藏和共享一个模块中的某些 Java 包和类。这是通过 OSGi 特有的类加载器机制来实现的。OSGi 中的每个模块都有对应的一个类加载器。它负责加载模块自己包含的 Java 包和类。当它需要加载 Java 核心库的类时(以 java开头的包和类),它会代理给父类加载器(通常是启动类加载器)来完成。当它需要加载所导入的 Java 类时,它会代理给导出此 Java 类的模块来完成加载。 模块也可以显式的声明某些 Java 包和类,必须由父类加载器来加载。只需要设置系统属性 org.osgi.framework.bootdelegation的值即可。
假设有两个模块 bundleA 和 bundleB,它们都有自己对应的类加载器 classLoaderA 和 classLoaderB。在 bundleA 中包含类 com.bundleA.Sample,并且该类被声明为导出的,也就是说可以被其它模块所使用的。bundleB 声明了导入 bundleA 提供的类 com.bundleA.Sample,并包含一个类 com.bundleB.NewSample继承自 com.bundleA.Sample。在 bundleB 启动的时候,其类加载器 classLoaderB 需要加载类 com.bundleB.NewSample,进而需要加载类 com.bundleA.Sample。由于 bundleB 声明了类 com.bundleA.Sample是导入的,classLoaderB 把加载类 com.bundleA.Sample的工作代理给导出该类的 bundleA 的类加载器 classLoaderA。classLoaderA 在其模块内部查找类 com.bundleA.Sample并定义它,所得到的类 com.bundleA.Sample实例就可以被所有声明导入了此类的模块使用。对于以 java开头的类,都是由父类加载器来加载的。如果声明了系统属性 org.osgi.framework.bootdelegation=com.example.core.*,那么对于包 com.example.core中的类,都是由父类加载器来完成的。
OSGi 模块的这种类加载器结构,使得一个类的不同版本可以共存在 Java 虚拟机中,带来了很大的灵活性。 不过它的这种不同,也会给开发人员带来一些麻烦,尤其当模块需要使用第三方提供的库的时候。下面提供几条比较好的建议:
(1)如果一个类库只有一个模块使用,把该类库的 jar 包放在模块中,在 Bundle-ClassPath中指明即可。
(2)如果一个类库被多个模块共用,可以为这个类库单独的创建一个模块,把其它模块需要用到的 Java 包声明为导出的。其它模块声明导入这些类。
(3)如果类库提供了 SPI 接口,并且利用线程上下文类加载器来加载 SPI 实现的 Java 类,有可能会找不到 Java 类。如果出现了 NoClassDefFoundError异常,首先检查当前线程的上下文类加载器是否正确。通过 Thread.currentThread().getContextClassLoader()就可以得到该类加载器。该类加载器应该是该模块对应的类加载器。如果不是的话,可以首先通过 class.getClassLoader()来得到模块对应的类加载器,再通过 Thread.currentThread().setContextClassLoader()来设置当前线程的上下文类加载器。
类加载器是 Java 语言的一个创新。它使得动态安装和更新软件组件成为可能。本文详细介绍了类加载器的相关话题,包括基本概念、代理模式、线程上下文类加载器、与 Web 容器和 OSGi 的关系等。开发人员在遇到 ClassNotFoundException和 NoClassDefFoundError等异常的时候,应该检查抛出异常的类的类加载器和当前线程的上下文类加载器,从中可以发现问题的所在。在开发自己的类加载器的时候,需要注意与已有的类加载器组织结构的协调。