Java虚拟机系列(二)深入类加载器

Author:Martin

E-mail:mwdnjupt@sina.com.cn

CSDN Blog:http://blog.csdn.net/ictcamera

Sina MicroBlog ID:ITCamera

Main Reference:

《java深度历险》 王森

《深入理解Java虚拟机-java高级特性与最佳实践》 周志明

1.        前言

类加载器是 Java 语言的一个创新,也是 Java 语言流行的重要原因之一。它使得 Java 类可以被动态加载到 Java 虚拟机中并执行。一般来说,Java 应用的开发人员不需要直接同类加载器进行交互。Java 虚拟机默认的行为就已经足够满足大多数情况的需求了。不过在一些特殊的场景中,类加载器就是非常必要的技术手段了,比如你的应用通过网络来传输 Java 类的字节代码,为了保证安全性,这些字节代码经过了加密处理。这个时候您就需要自己的类加载器来从某个网络地址上读取加密后的字节代码,接着进行解密和验证,最后定义出要在 Java 虚拟机中运行的类来。

2.        Java动态加载的两种类型

隐式(implicit)加载和显式(explicit)加载

两种加载类型 

2.1.        隐式加载—自动加载

1.         基础类库(rt.jar)的加载方式是pre-loading,而其他的用户类加载方式是load-on-demand。按需加载能够节省内存,这个在内存有限的手持终端设备上显得更加有用,但是时间上的效率比预加载要差一点。【时间复杂度和空间复杂度的权衡】

2.         用户类只有在执行构造器(使用new关键字)的时候才加载(一个类第一次调用其静态方法,虚拟机会自动执行private构造函数,也就加载了该类),仅仅声明一个引用是不被加载的。加载顺序是按照使用的先后顺序和继承关系进行的。

2.2.        显式加载­­—Class的forName方法

public static Class forName(String name);

public static Class forName(String name,Boolean initializte,ClassLoader loader);

这两个方法都调用java本地方法

private static native Class forName0(String name,Boolean initializte,ClassLoader loader)throws ClassNotFoundException;

因此,上面两个forName方法分别作如下调用:

forName0(name,true,ClassLoader.getCallerClassLoader());

forName0(name, initializte, loader);

l  参数String name:要加载的类名,包括包路径名称。

l  参数Boolean initializte:决定静态初始化代码块调用是否在该类第一次实例化对象前。如果改值为true,那么在类被加载后便执行静态初始化代码块;反之,类加载后还是不调用静态初始化代码块,需要在该类第一个实例实例化的时候(应该是实例化那一刻)才执行静态初始化代码块。这个参数在隐式加载和forName(String name)的显示加载中initializte值都为true,因此这些加载中只要类被加载,该类的静态初始化代码就被调用。

l  参数ClassLoader loader:类加载器实例,在forName(String name)中使用了ClassLoader.getCallerClassLoader()加载器对象。Caller Classloader指的是当前所在的类装载时使用的Classloader。

2.3.        显式加载—类加载器的loadClass方法

每个类在被加载后,都产生一个类型为Class实例对象,这个对象被XXX.class(XXX表示某某类名)引用,也赋给了该类对象的类型为Class的class变量。举例说Hello hell=new Hello();,生成hello对象,那么文件Hello.class被加载后产生一个类型为Class的对象,该对象被Hello.class(这里不是类文件而是引用)引用,也赋给了hello.class。Class类型的对象记录了所有该类相关的方法、字段和ClassLoadder信息。

如果拥有一个类的对象实例,那么可以调用getClass().getClassLoade()获取ClassLoader类型的对象(这个对象负责加载这个类),如果返回的是null,那么不是说明该类不是由类加载器加载,而是说它的类加载器有可能是bootstrap loader(也有人称作root loader),只不过因为这个加载器不是由Java写的,因此逻辑上没实现。

有了类加载器对象的引用,那么可以利用loadClass方法显式加载类并且生成对象实例。loadClass(String className)方法加载时候类的静态代码块不被调用,只有在第一次实例化对象的时候才被调用,这个和隐式加载和forName(String name)的显示加载中initializte值为true的效果相反。其实抽象类ClassLoader还有一个protected的方法loadClass(String name,boolean resolve),一个参数的loadClass(String name)方法调用的就是resolve=false,这个效果和initializte=false一样,也就是为什么用loadClass方法,仅仅加载类的时候静态代码块还不会加载执行的原因。当然resolve这个参数这里的含义和initializte是不一样的。

上面说明的是某个类已经被加载或者拥有该类的对象实例情况下获取ClassLoader对象,但是我们也可以自己构造一个ClassLoader—URLClassLOader,URLClassLOader就是ClassLoader的子类。通过这个类就可以加载指定类。例如URL url=new URL(“file:/d;/my/lib/”);其中URL路径可以来自网络,也可以是本地,本地路径可以是绝对路径也可以是相对路径。URLClassLOader loader=new URLClassLOader(new URL[]{url });

有了自己构造的URLClassLOader类加载器就可以同样加载类了,同一个类可以被不同的URLClassLoader对象或者相同的URLClassLoader对象分别加载一次,但是静态初始化代码块只会被执行一次。下面有个例子是使用URLClassLOader加载指定类,其中Word实现了Assembly接口

  URL url1 = new URL("file:e:\\eclipse_begin\\Test\\bin\\org\\martin");

            URLClassLoader loader1=new URLClassLoader(new URL[]{url1});

            Class cl1=loader1.loadClass("org.martin.Word");

            Assembly asb1=(Assembly)cl1.newInstance();

            URL url2=new URL("file:e:\\eclipse_begin\\Test\\bin\\org\\martin");

            URLClassLoader loader2=new URLClassLoader(new URL[]{url2});

            Class cl2=loader2.loadClass("org.martin.Word");

            Assembly asb2=(Assembly)cl2.newInstance();

            System.out.println(Office.class.getClassLoader());

            System.out.println(url1.getClass().getClassLoader());

            System.out.println(loader1.getClass().getClassLoader());

            System.out.println(cl1.getClassLoader());

            System.out.println(asb1.getClass().getClassLoader());

            System.out.println(url2.getClass().getClassLoader());

            System.out.println(loader2.getClass().getClassLoader());

            System.out.println(cl2.getClassLoader());

            System.out.println(asb2.getClass().getClassLoader());

输入结果

word static initialization

sun.misc.Launcher$AppClassLoader@19821f

null

null

sun.misc.Launcher$AppClassLoader@19821f

sun.misc.Launcher$AppClassLoader@19821f

null

null

sun.misc.Launcher$AppClassLoader@19821f

sun.misc.Launcher$AppClassLoader@19821f

文章的后面部分将仔细分析为什么会得到这个结果。

2.4.        Java类加载分层体系

Java类分层体系中,上下级的关系不是继承关系,而是分层的关系,也是对象之间的依赖关系,和继承关系有很大的差别。当我们的程序从执行main主方法main(public static void main(String[]args))时候Java虚拟机才开始加载所需的class文件,逻辑上成为一个Java应用程序,下面讨论这个流程。

当我们执行命令java xxx.class的时候,java.exe根据一定的原则找到JRE中的jvm.dll(真正的Java虚拟机),加载这个动态链接库,启动Java虚拟机。虚拟机启动后,会做一些初始化动作,比如获取系统参数等。

一旦初始化完后就产生第一个类加载器即bootstrap Loader,即由C++写的(这个不是Java中的类实例,因此打印的时候会是null)。bootstrap Loader开始做一些初始化动作,其中一个动作是加载sun.misc包路劲下的Launcher.java的内部类Launcher$ExtClassLoader.class,并将其Parent为null,表示其父加载器是Bootstrap Loader。然后Bootstrap Loade再载入un.misc包路劲下的Launcher.java的内部类Launcher$AppClassLoader.class(AppClassloader又叫System ClassLoader),并将其Parent为刚才载入的Launcher$ExtClassLoader.class类实例。由此可知Parent和由哪个类加载器载入没关系(一定要注意),Launcher$ExtClassLoader.class和Launcher$AppClassLoader.class都是由Bootstrap Loader载入,但是Launcher$AppClassLoader.class类实例的Parent不是Bootstrap Loader而是Launcher$ExtClassLoader.class类实例。bootstrap Loader->ExtClassLoader-> AppClassLoader就是所谓的父子关系、所谓的类加载器分层体系(再强调下Parent和由哪个类加载器载入没关系)。一个ClassLoader对象的getParent()方法就能获取其父加载器。bootstrap Loader的产生和其他两个类的载入过程和关系如下图所示:

加载器生成过程

三个加载器的引用关系

这三个类加载器产生后由AppClassloader开始“负责加载”应用程序的类,开始Java应用程序的生命周期。注意这里所说的负责加载意思加载由它开始,但是具体由哪个加载器还取决于几个环境变量,即由类加载器的委托机制决定的。类加载委托机制将在后面详细说明。

因此上面的整个过程如下图所示:

Java应用程序加载整体流程

2.5.        Java类加载的委托机制

无论是隐射加载,或是显式的forName()加载,还是显式的ClassLoader.loadClass()加载,都有对应的ClassLoader,并且都遵循委托加载机制。隐射加载的加载器CallerClassLoader(main函数所在类由AppClassloader开始“负责加载”),forName()加载器是CallerClassLoader或指定的ClassLoader,ClassLoader.load()指定了加载器。当一个类装载请求被提交到某个Classloader时,其默认的类装载过程如下:

  • 检查这个类有没有被装载过(加载过的Class类型的类实例会得到缓存,下次再请求加载该类的时候,类加载器会直接使用缓存的类的实例,而不会尝试再次加载。也就是说,对于一个类加载器实例来说,相同全名的类只加载一次,即 loadClass 方法不会被重复调用),如果已经装载过,则直接返回;
  • 调用父Classloader去装载类,如果装载成功直接返回;
  • 调用自身的装载类的方法,如果装载成功直接返回;
  • 上述所有步骤都没有成功装载到类,抛出ClassNotFoundException;

每一层次的Classloader都重复上述动作。这个过程也可以细化成如下算法:

1.         检测此Class是否载入过(即在cache中是否有此Class),如果有到8,如果没有到2 。

2.         如果parent classloader不存在(没有parent,那parent一定是bootstrap classloader了),到4。

3.         请求parent classloader载入,如果成功到8,不成功到5 。

4.         请求jvm从bootstrap classloader中载入,如果成功到8 。

5.         寻找Class文件(从与此classloader相关的类路径中寻找)。如果找不到则到7。

6.         从文件中载入Class,到8。

7.         抛出ClassNotFoundException。

8.         返回Class。

简单说,当Classloader链上的某一Classloader收到类装载请求时,会按顺序向上询问其所有父节点,直至最顶端(BootstrapClassLoader),任何一个节点成功受理了此请求,则返回,如果所有父节点都不能受理,这时候才由被请求的Classloader自身来装载这个类,如果仍然不能装载,则抛出异常。由此也可知,类加载器可以通过向子类加载器请求从而看到父类加载器所能看到的类,但是反过来就不行。

根据这个委托机制,1.3的自己构造类加载器的例子,即执行一个main()函数所在的类,就可以根据bootstrap Loader->ExtClassLoader-> AppClassLoader的父子关系可以大概分析其加载过程。AppClassLoader要加载main()函数所在的类时,先请父ClassLoader即 ExtClassLoader加载,如果ExtClassLoader加载不到,那么ExtClassLoader再请其父ClassLoader即bootstrap Loader加载,依次按照上述原则加载(bootstrap Loader为最顶层),最后找不到才抛异常。整个过程根据类加载请求的先后顺序依次加载,除了main()函数所在的类,其他的类都是从他被使用的类的ClallerClassLoader开始请求加载(Caller Classloader指的是当前所在的类装载时使用的ClassloaderA.class.getClassLoader()AClassLoader的实例,它就是A.classCaller Classloader)。例如,main()所在类为Class A,这个Class由AppClassLoader加载到,如果A中的第一条语句用了Class B,那么对Class B而言请求就是发给了AppClassLoader,也就是A的CallerClassLoader。

那么bootstrap Loader->ExtClassLoader-> AppClassLoader这三个ClassLoader通过什么机制找到对应的jar包?AppClassLoader和ExtClassLoader都是URLClassLoader的子类,因此有URLClassLoader的jar包搜索机制。

l  AppClassLoader由系统变量java.class.path即环境变量CLASSPATH决定(默认的是当前目录),如果在java命令中增加-classpath参数,那么以-classpath(-cp)参数指定为主

l  类似的ExtClassLoader由系统变量java.ext.dirs,指向虚拟机所在jre目录下的ext目录。java命令中增加-Djava.ext.dirs=xxx参数来指定这个变量。

l  Bootstrap Loader由系统变量sun.boot.class.path决定,指向虚拟机所在jre目录下的lib下的rt.jar等jar包,以及jre下的classes目录。java命令中增加-Dsun.boot.class.path=xxx参数来指定这个变量。

AppClassLoader和Bootstrap Loader会搜索指定位置的class文件和jar文档,如果找不到,不会再去搜索指定位置下的其他路径,或者没有指定的jar文档。相反ExtClassLoader就会搜获指定位置下的所有jar文档和classes目录,这也就是增加-Djava.ext.dirs=xxx参数后执行可能变慢的原因。

    用-classpath和-Djava.ext.dirs=xxx参数来改变AppClassLoader、ExtClassLoader的指定位置是有效的,但是用Dsun.boot.class.path=xxx是无效的,因为前者命令执行的时候会参考这两个参数,后者执行命令的时候这个系统参数已经确定(已经确定了jre及其下面的rt.jar),再修改参数已经不起作用.

    AppClassLoader、ExtClassLoader的环境变量在程序运行过程中是修改不了的(比如运行过程中作修改SetProperty("java.class.path",”C:\”))即修改时无效的(要么在开始就指定)。因此,如果有特殊需需要,要加载一开始不能指定位置的类,那么需要新建类加载器来辅助完成。

    现在回过头来可以根据Java类分层体系和Java类加载的委托机制详细分析1.3的自己构造类加载器的例子。代码和输出如下所示,现在详细说明其中的加载流程。

  URL url1 = new URL("file:e:\\eclipse_begin\\Test\\bin\\org\\martin");

            URLClassLoader loader1=new URLClassLoader(new URL[]{url1});

            Class cl1=loader1.loadClass("org.martin.Word");

            Assembly asb1=(Assembly)cl1.newInstance();

            URL url2=new URL("file:e:\\eclipse_begin\\Test\\bin\\org\\martin");

            URLClassLoader loader2=new URLClassLoader(new URL[]{url2});

            Class cl2=loader2.loadClass("org.martin.Word");

            Assembly asb2=(Assembly)cl2.newInstance();

            System.out.println(Office.class.getClassLoader());

            System.out.println(url1.getClass().getClassLoader());

            System.out.println(loader1.getClass().getClassLoader());

            System.out.println(cl1.getClassLoader());

            System.out.println(asb1.getClass().getClassLoader());

            System.out.println(url2.getClass().getClassLoader());

            System.out.println(loader2.getClass().getClassLoader());

            System.out.println(cl2.getClassLoader());

            System.out.println(asb2.getClass().getClassLoader());

输入结果

word static initialization

sun.misc.Launcher$AppClassLoader@19821f

null

null

sun.misc.Launcher$AppClassLoader@19821f

sun.misc.Launcher$AppClassLoader@19821f

null

null

sun.misc.Launcher$AppClassLoader@19821f

sun.misc.Launcher$AppClassLoader@19821f

程序一开始执行Main()函数所在的类,即Office类,按照上面描述的流程生成Bootstrap Loader->ExtClassLoader-> AppClassLoader三个类加载器,由AppClassLoader开始负责加载Office. Class(注意负责的含义)。

AppClassLoader父加载器ExtClassLoader和ExtClassLoader的父加载器Bootstrap Loader都无法搜索到Office. class,所以由AppClassLoader载入Office.class。

在Office.class需要建立URL.class和URLClassLoader.class的类实例,以加载指定的类。这时候发起加载URL.class的请求,使用当前所在类的类加载器,也就是说使用Office.class的CallerClassLOader即AppClassLoader(这个ClallerClassLoader是当前所在的类通过调用ClassLoader.getClallerClassLoader()方法获取,这个方法是private方法,我们无法自行调用,而是new运算本身隐含的机制中自动调用)来加载,AppClassLoader请求ExtClassLoader,ExtClassLoader再请求Bootstrap Loader,Bootstrap Loader加载到了URL.class。

此时再请求加载URLClassLoader.class,同样这时来使用当前所在类的类加载器,也就是说Office.class的CallerClassLOader即AppClassLoader (Caller Classloader指的是当前所在的类装载时使用的Classloader),来加载URLClassLoader.class。类似URL.class的加载方式,最后 Bootstrap Loader加载到了URLClassLoader.class,生成了URLClassLoader对象,并且把praent设置为AppClassLoader。注意:这里再强调下Parent和由哪个类加载器载入没关系,其实URLClassLoader是由Bootstrap Loader加载的,在Java类继承上看,AppClassLoaderURLClassLoader的子类,这里也说明了这里的父子关系和Java的继承关系没有任何关系,任何非系统生成的ClassLoader实例的parentClassLoader默认都设置为AppClassLoader

这时请求加载Word.class,但是根据Java的类继承关系加载Word.class前需要先加载Assembly.calss,此时URLClassLoader请求加载Assembly.calss,先向其父加载器AppClassLoader请求,AppClassLoader向ExtClassLoader请求,ExtClassLoader向Bootstrap Loader请求,最后还是由AppClassLoader加载到(AppClassLoader能看到这个类)。

然后URLClassLoader继续请求加载Word.class,同样URLClassLoader请求父加载器AppClassLoader,AppClassLoader向ExtClassLoader请求,ExtClassLoader向Bootstrap Loader请求,最后还是由AppClassLoader加载到(AppClassLoader能看到这个类)。

所以最终的加载顺序为:Office. Class由AppClassLoader.class加载,URL.class和URLClassLoader.class分别由Bootstrap Loader加载,Assembly.calss和Word.class分别由AppClassLoader.class加载。结合Java类加载委托机制的一个原则“检查这个类有没有被装载过,如果已经装载过,则直接返回”可知,输出结果符合分析结果。

2.6.        Java类加载机制和虚拟机安全

Java的类加载分层体系和委托机制提供了加载机制,这是一个非常复杂的系统,这个就实现了Java的动态性,其实设计如此复杂的机制,主要还是出于Java的安全性,如下图所示:

加载器分层结构和虚拟机安全

这张图说明两点:第一,如果我们利用URLClassLoader从网络上下载了其他类,但是此URLClassLoader不可能下载AppClassLoader、ExtClassLoader或者BootstrapLoader可以找到的同一个类(类名和包名都相同),即如果下载了这些问题类,AppClassLoader、ExtClassLoader或者BootstrapLoader也会加载自己路径下的类,会忽略下载到的类。因此,蓄意破坏者根本没有几乎将有问题的代码植入到我们的电脑中。第二,类加载器无法加载到其他同阶层的类加载器所能看到的类。这也就是说不通的类加载器完全可以加载完全相同的类,这也排除了误用或而已使用别人代码的机会。注意:Java虚拟机中只有当两个Class的名称以及加载器都完全相同时,才认为是同一个类。

2.7.        Java类加器Contex ClassLoader

由类加载分层体系和委托加载机制可知,类加载器可以通过向子类加载器请求从而看到父类加载器所能看到的类,但是反过来就不行。那么像JDBC API作为核心类库都放在jre下,即由Bootstrap Loader或者ExtClassLoader加载,而JDBC driver是JDBC API的实现,由各个厂商实现,由ExtClassLoader或者AppClassLoader来载入,即出现这了这样的流程:加载JDBC driver类的时候会事先加载JDBC API类,这个类由Bootstrap Loader加载,而JDBC API类中使用了JDBC drive类,即JDBC  API实现类,根据加载链关系,Bootstrap Loader作为JDBC API的CallerClassLoader类再去加载JDBC drive类,而它本身是看不到这些类,因此会出现找不到类的情况。Java领域还有很多这种API和SPI关系的例子(JNDI等等)都会遇到这种情况。

另外一个很典型的例子是JAXP,当使用xerces的SAX实现时,我们首先需要通过rt.jar中的javax.xml.parsers.SAXParserFactory.getInstance()得到xercesImpl.jar中的org.apache.xerces.jaxp.SAXParserFactoryImpl的实例。由于JAXP的框架接口的class位于JAVA_HOME/lib/rt.jar中,由Bootstrap Loader装载,处于Classloader层次结构中的最顶层,而xercesImpl.jar由低层的Classloader装载(AppClassLoader或者ExtClassLoader),也就是说SAXParserFactoryImpl是在SAXParserFactory中实例化的,如前所述,使用SAXParserFactory的Caller Classloader(这里是Bootstrap Loader)是完成不了这个任务的,即Bootstrap Loader是找不到实现类SAXParserFactoryImpl的。

那么怎么解决问题?解决问题的办法就是使用ContexClassLoader(上下文ClassLoader),其中一个就是线程的ContextClassLoader。 每个线程都有一个关联的ContextClassLoader。如果使用new Thread()方式生成新的线程,新线程将继承其父线程的ContextClassLoader。如果程序对线程ContextClassLoader没有任何改动的话(缺省的),程序中所有的线程将都使用System Classloader(也即是前面提到的AppClassLoader,可以用ClassLoader.getSystemClassLoader()来获取该Classloader对象)作为线程的ContextClassLoader。当使用Thread.currentThread().setContextClassLoader(classloader)时,线程ContextClassLoader就变成了指定的Classloader了。此时,在本线程的任意一处地方,调用Thread.currentThread().getContextClassLoader(),都可以得到前面设置的Classloader。

下面以JAXP为例说明如果解决这个问题,假设xercesImpl.jar只有XClassLoader能装载,现在A.class内部要使用JAXP,那么在A.class中,应该这样写才能正确得到xercesImpl的实现(当然最好在获取之前保存线程当前的ContexClassLoader,获取之后将线程的ContexClassLoader恢复):

XClassLoader aClassLoader = new XClassLoader(parent);

Thread.currentThread().setContextClassLoader(xClassLoader);

SAXParserFactory factory = SAXParserFactory.getInstance();

...

因为JAXP接口由Bootstrap Loader加载, CallerClassLoader是Bootstrap Loader,如果不做上述处理,是找不到xercesImpl即JAXP实现类。而上面处理能找到实现类,是因为在使用SAXParserFactory.getInstance();获取实现类对象的处理中,是通过getContextClassLoader()方法来获取ClassLoader,然后再使用loadClass方法加载类,而这个ClassLoader就是替换过的ContextClassLoader,这样就能加载到了,这个细节可看参考SAXParserFactory类相关的源代码。

3.        Java类加载器和Web容器

对于运行在 Java EE™ 容器中的 Web 应用来说,类加载器的实现方式与一般的 Java 应用有所不同。不同的 Web 容器的实现方式也会有所不同。以 Apache Tomcat 来说,每个 Web 应用都有一个对应的类加载器实例。该类加载器也使用代理模式,所不同的是它是首先尝试去加载某个类,如果找不到再代理给父类加载器。这与一般类加载器的顺序是相反的。这是 Java Servlet 规范中的推荐做法,其目的是使得 Web 应用自己的类的优先级高于 Web 容器提供的类。这种代理模式的一个例外是:Java 核心库的类是不在查找范围之内的。这也是为了保证 Java 核心库的类型安全。绝大多数情况下,Web 应用的开发人员不需要考虑与类加载器相关的细节。下面给出几条简单的原则:

l  每个 Web 应用自己的 Java 类文件和使用的库的 jar 包,分别放在 WEB-INF/classes 和 WEB-INF/lib 目录下面。

l  多个应用共享的 Java 类文件和 jar 包,分别放在 Web 容器指定的由所有 Web 应用共享的目录下面。

l  当出现找不到类的错误时,检查当前类的类加载器和当前线程的上下文类加载器是否正确。

4.        Java类加载器和OSGI

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 虚拟机中,带来了很大的灵活性。不过它的这种不同,也会给开发人员带来一些麻烦,尤其当模块需要使用第三方提供的库的时候。下面提供几条比较好的建议:

l  如果一个类库只有一个模块使用,把该类库的 jar 包放在模块中,在 Bundle-ClassPath 中指明即可。

l  如果一个类库被多个模块共用,可以为这个类库单独的创建一个模块,把其它模块需要用到的 Java 包声明为导出的。其它模块声明导入这些类。

l  如果类库提供了 SPI 接口,并且利用线程上下文类加载器来加载 SPI 实现的 Java 类,有可能会找不到 Java 类。如果出现了 NoClassDefFoundError 异常,首先检查当前线程的上下文类加载器是否正确。通过 Thread.currentThread().getContextClassLoader() 就可以得到该类加载器。该类加载器应该是该模块对应的类加载器。如果不是的话,可以首先通过 class.getClassLoader() 来得到模块对应的类加载器,再通过 Thread.currentThread().setContextClassLoader() 来设置当前线程的上下文类加载器。

5.        实际中的二次开发和常见问题

5.1.        替代CLASSPATH

一般运行Java程序时只要在CLASSPATH参数中指定需要Jar文件即可。但是CLASSPATH的局限性比较大,例如必须指定到具体的文件。如果是在庞大的系统,是不可能把所有Jar文件都写到CLASSPATH里。

       下面给出了针对问题的一种实现方式:

publicclass BootLoaderextends URLClassLoader {

    publicstaticvoid main(String[] args) {

        BootLoader loader = new BootLoader(ClassLoader.getSystemClassLoader());

      Thread.currentThread().setContextClassLoader(loader);

       try {

            Class mainClass = loader.loadClass(args[0]);

            Method method = mainClass.getMethod("main",new Class[] { String.class });

            String[] newArgs = new String[args.length - 1];

            System.arraycopy(args, 1, newArgs, 0, newArgs.length);

            method.invoke(null, newArgs);

        } catch (Exception e) {

            e.printStackTrace();

        }

    }

    public BootLoader(ClassLoaderloader) {

        super(new URL[0],loader);

        String userLib = System.getProperty("user.lib");

        loadJarFile(userLib);

    }

}

l  自行实现了一个类加载器。在这个类加载器中可以根据user.lib参数指定的路径,搜索Jar文件,并且加载。

l  在Java程序的Main入口中构建这个类加载器的实例,并将其作为线程上下文类加载器。触发该实例加载Jar文件,并通过这个实例找到真正的Main入口。

l  在执行时,首先通过Java执行BootLoader,再把真正的业务入口Main作为参数。

Java  -Duser.lib =xxx BootLoader xxx.xxx.xxx

分析一下,在上面这个例子里,自定义了一个BootLoader,并将其作为线程上下文类加载器。并且所有业务相关的Jar以及业务Main入口都是通过这个实例加载的,因此程序可以正常运行。

5.2.        替代AppClassLoader

但是这种实现存在一个缺陷。因为所有业务Jar实际上是由一个线程上下文类加载器加载的,AppClassLoader(System ClassLoader)是看不到这些类的。如果代码中又定义了其他的类加载器,例如形成了下图的父子关系:

开发人员编写的不同类加载器

A是BootLoader,B是其他的。这时所有Jar都是A加载的,B里是看不到的,就会出现找不到类的错误。

publicclassSystemClassLoaderextends URLClassLoader {

    publicstaticvoid main(String[] args) {

        SystemClassLoader loader = (SystemClassLoader)

ClassLoader.getSystemClassLoader();

        loader.loadJarFile();           //根据user.lib加载Jar

        try {

            Class mainClass = loader.loadClass(args[0]);

            Method method = mainClass.getMethod("main",new Class[] { String.class });

            String[] newArgs = new String[args.length - 1];

            System.arraycopy(args, 1, newArgs, 0, newArgs.length);

            method.invoke(null, newArgs);

        } catch (Exception e) {

            e.printStackTrace();

        }

    }

}

l  这段代码和之前的比较类似,但是SystemClassLoader是作为系统类加载器。同样的将SystemClassLoader作为Main入口,随后根据user.lib加载Jar文件,并且执行真正的业务Main入口。

l  这种实现的关键是要替换Java缺省的SystemClassLoader,java命令的参数如下:

Java  -Duser.lib =xxx  -Djava.system.class.loader=SystemClassLoader

SystemClassLoader xxx.xxx.xxx

在这种实现里,所有业务Jar都是通过System Class Loader加载的。因此即使二次开发的Class Loader也能够访问到所有业务Jar。

5.3.        常见问题

类加载器最常见的两个异常是ClassNotFoundException和NoClassDefFoundError。这两个异常体现了类加载的两个步骤。

加载类的第一步是defineClass,负责根据字节生成Java类,第二步是loadClass,负责类的初始化。这两步可能是由两个不同的类加载器实例来完成的,前者被称为类的定义加载器,后者称为类的初始化加载器。

NoClassDefFoundError

NoClassDefFoundError是由defineClass抛出的,它表示类加载器没有找到对应的类。但是有时候明明Jar包已经部署了,为什么还会有这个错误呢?

最有可能的原因就是上文中提到的,Jar是被一个用户自定义类加载器加载的,System Class Loader并不知道。那么在其他的上下文环境里,就不知道这个类了。

这个问题通常是框架层面引起的,开发人员需要理解类加载器的工作原理,再针对框架的部署方式定位问题。

ClassNotFoundException

ClassNotFoundException是由loadClass抛出的。它的名字比较有诱惑性,实际上它表达的是类已经找到了,但是类初始化有错误,例如初始化static属性时抛出异常。

所以当看到这个错误时,应当首先看看代码是否有BUG。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值