深入了解类加载器Word格式文档下载.docx
- 文档编号:17875559
- 上传时间:2022-12-11
- 格式:DOCX
- 页数:11
- 大小:61.85KB
深入了解类加载器Word格式文档下载.docx
《深入了解类加载器Word格式文档下载.docx》由会员分享,可在线阅读,更多相关《深入了解类加载器Word格式文档下载.docx(11页珍藏版)》请在冰豆网上搜索。
表1.ClassLoader中与加载类相关的方法
方法
说明
getParent()
返回该类加载器的父类加载器。
loadClass(Stringname)
加载名称为
name的类,返回的结果是
java.lang.Class类的实例。
findClass(Stringname)
查找名称为
findLoadedClass(Stringname)
name的已经被加载过的类,返回的结果是
defineClass(Stringname,byte[]b,intoff,intlen)
把字节数组
b中的内容转换成Java类,返回的结果是
这个方法被声明为
final的。
resolveClass(Class<
?
>
c)
链接指定的Java类。
对于
表1中给出的方法,表示类名称的
name参数的值是类的二进制名称。
需要注意的是内部类的表示,如
com.example.Sample$1和com.example.Sample$Inner等表示方式。
这些方法会在下面介绍类加载器的工作机制时,做进一步的说明。
下面介绍类加载器的树状组织结构。
类加载器的树状组织结构
Java中的类加载器大致可以分成两类,一类是系统提供的,另外一类则是由Java应用开发人员编写的。
系统提供的类加载器主要有下面三个:
∙引导类加载器(bootstrapclassloader):
它用来加载Java的核心库,是用原生代码来实现的,并不继承自
java.lang.ClassLoader。
∙扩展类加载器(extensionsclassloader):
它用来加载Java的扩展库。
Java虚拟机的实现会提供一个扩展库目录。
该类加载器在此目录里面查找并加载Java类。
∙系统类加载器(systemclassloader):
它根据Java应用的类路径(CLASSPATH)来加载Java类。
一般来说,Java应用的类都是由它来完成加载的。
可以通过
ClassLoader.getSystemClassLoader()来获取它。
除了系统提供的类加载器以外,开发人员可以通过继承
java.lang.ClassLoader类的方式实现自己的类加载器,以满足一些特殊的需求。
除了引导类加载器之外,所有的类加载器都有一个父类加载器。
通过
表1中给出的
getParent()方法可以得到。
对于系统提供的类加载器来说,系统类加载器的父类加载器是扩展类加载器,而扩展类加载器的父类加载器是引导类加载器;
对于开发人员编写的类加载器来说,其父类加载器是加载此类加载器Java类的类加载器。
因为类加载器Java类如同其它的Java类一样,也是要由类加载器来加载的。
一般来说,开发人员编写的类加载器的父类加载器是系统类加载器。
类加载器通过这种方式组织起来,形成树状结构。
树的根节点就是引导类加载器。
图1中给出了一个典型的类加载器树状组织结构示意图,其中的箭头指向的是父类加载器。
图1.类加载器树状组织结构示意图
代码清单1演示了类加载器的树状组织结构。
清单1.演示类加载器的树状组织结构
publicclassClassLoaderTree{
publicstaticvoidmain(String[]args){
ClassLoaderloader=ClassLoaderTree.class.getClassLoader();
while(loader!
=null){
System.out.println(loader.toString());
loader=loader.getParent();
}
}
每个Java类都维护着一个指向定义它的类加载器的引用,通过
getClassLoader()方法就可以获取到此引用。
代码清单1中通过递归调用getParent()方法来输出全部的父类加载器。
代码清单1的运行结果如
代码清单2所示。
清单2.演示类加载器的树状组织结构的运行结果
sun.misc.Launcher$AppClassLoader@9304b1
sun.misc.Launcher$ExtClassLoader@190d11
如
代码清单2所示,第一个输出的是
ClassLoaderTree类的类加载器,即系统类加载器。
它是
sun.misc.Launcher$AppClassLoader类的实例;
第二个输出的是扩展类加载器,是
sun.misc.Launcher$ExtClassLoader类的实例。
需要注意的是这里并没有输出引导类加载器,这是由于有些JDK的实现对于父类加载器是引导类加载器的情况,getParent()方法返回
null。
在了解了类加载器的树状组织结构之后,下面介绍类加载器的代理模式。
类加载器的代理模式
类加载器在尝试自己去查找某个类的字节代码并定义它时,会先代理给其父类加载器,由父类加载器先去尝试加载这个类,依次类推。
在介绍代理模式背后的动机之前,首先需要说明一下Java虚拟机是如何判定两个Java类是相同的。
Java虚拟机不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。
只有两者都相同的情况,才认为两个类是相同的。
即便是同样的字节代码,被不同的类加载器加载之后所得到的类,也是不同的。
比如一个Java类
com.example.Sample,编译之后生成了字节代码文件
Sample.class。
两个不同的类加载器ClassLoaderA和
ClassLoaderB分别读取了这个
Sample.class文件,并定义出两个
java.lang.Class类的实例来表示这个类。
这两个实例是不相同的。
对于Java虚拟机来说,它们是不同的类。
试图对这两个类的对象进行相互赋值,会抛出运行时异常ClassCastException。
下面通过示例来具体说明。
代码清单3中给出了Java类
com.example.Sample。
清单3.com.example.Sample类
packagecom.example;
publicclassSample{
privateSampleinstance;
publicvoidsetSample(Objectinstance){
this.instance=(Sample)instance;
代码清单3所示,com.example.Sample类的方法
setSample接受一个
java.lang.Object类型的参数,并且会把该参数强制转换成com.example.Sample类型。
测试Java类是否相同的代码如
代码清单4所示。
清单4.测试Java类是否相同
publicvoidtestClassIdentity(){
StringclassDataRootPath="
C:
\\workspace\\Classloader\\classData"
;
FileSystemClassLoaderfscl1=newFileSystemClassLoader(classDataRootPath);
FileSystemClassLoaderfscl2=newFileSystemClassLoader(classDataRootPath);
StringclassName="
com.example.Sample"
try{
Class<
class1=fscl1.loadClass(className);
Objectobj1=class1.newInstance();
class2=fscl2.loadClass(className);
Objectobj2=class2.newInstance();
MethodsetSampleMethod=class1.getMethod("
setSample"
java.lang.Object.class);
setSampleMethod.invoke(obj1,obj2);
}catch(Exceptione){
e.printStackTrace();
代码清单4中使用了类
FileSystemClassLoader的两个不同实例来分别加载类
com.example.Sample,得到了两个不同的java.lang.Class的实例,接着通过
newInstance()方法分别生成了两个类的对象
obj1和
obj2,最后通过Java的反射API在对象obj1上调用方法
setSample,试图把对象
obj2赋值给
obj1内部的
instance对象。
代码清单4的运行结果如
代码清单5所示。
清单5.测试Java类是否相同的运行结果
java.lang.reflect.InvocationTargetException
atsun.reflect.NativeMethodAccessorImpl.invoke0(NativeMethod)
atsun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:
39)
atsun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:
25)
atjava.lang.reflect.Method.invoke(Method.java:
597)
atclassloader.ClassIdentity.testClassIdentity(ClassIdentity.java:
26)
atclassloader.ClassIdentity.main(ClassIdentity.java:
9)
Causedby:
java.lang.ClassCastException:
com.example.Sample
cannotbecasttocom.example.Sample
atcom.example.Sample.setSample(Sample.java:
7)
...6more
从
代码清单5给出的运行结果可以看到,运行时抛出了
java.lang.ClassCastException异常。
虽然两个对象
obj2的类的名字相同,但是这两个类是由不同的类加载器实例来加载的,因此不被Java虚拟机认为是相同的。
了解了这一点之后,就可以理解代理模式的设计动机了。
代理模式是为了保证Java核心库的类型安全。
所有Java应用都至少需要引用java.lang.Object类,也就是说在运行的时候,java.lang.Object这个类需要被加载到Java虚拟机中。
如果这个加载过程由Java应用自己的类加载器来完成的话,很可能就存在多个版本的
java.lang.Object类,而且这些类之间是不兼容的。
通过代理模式,对于Java核心库的类的加载工作由引导类加载器来统一完成,保证了Java应用所使用的都是同一个版本的Java核心库的类,是互相兼容的。
不同的类加载器为相同名称的类创建了额外的名称空间。
相同名称的类可以并存在Java虚拟机中,只需要用不同的类加载器来加载它们即可。
不同类加载器加载的类之间是不兼容的,这就相当于在Java虚拟机内部创建了一个个相互隔离的Java类空间。
这种技术在许多框架中都被用到,后面会详细介绍。
下面具体介绍类加载器加载类的详细过程。
加载类的过程
在前面介绍类加载器的代理模式的时候,提到过类加载器会首先代理给其它类加载器来尝试加载某个类。
这就意味着真正完成类的加载工作的类加载器和启动这个加载过程的类加载器,有可能不是同一个。
真正完成类的加载工作是通过调用
defineClass来实现的;
而启动类的加载过程是通过调用
loadClass来实现的。
前者称为一个类的定义加载器(definingloader),后者称为初始加载器(initiatingloader)。
在Java虚拟机判断两个类是否相同的时候,使用的是类的定义加载器。
也就是说,哪个类加载器启动类的加载过程并不重要,重要的是最终定义这个类的加载器。
两种类加载器的关联之处在于:
一个类的定义加载器是它引用的其它类的初始加载器。
如类
com.example.Outer引用了类
com.example.Inner,则由类
com.example.Outer的定义加载器负责启动类
com.example.Inner的加载过程。
方法
loadClass()抛出的是
java.lang.ClassNotFoundException异常;
defineClass()抛出的是java.lang.NoClassDefFoundError异常。
类加载器在成功加载某个类之后,会把得到的
java.lang.Class类的实例缓存起来。
下次再请求加载该类的时候,类加载器会直接使用缓存的类的实例,而不会尝试再次加载。
也就是说,对于一个类加载器实例来说,相同全名的类只加载一次,即
loadClass方法不会被重复调用。
下面讨论另外一种类加载器:
线程上下文类加载器。
线程上下文类加载器
线程上下文类加载器(contextclassloader)是从JDK1.2开始引入的。
类
java.lang.Thread中的方法
getContextClassLoader()和setContextClassLoader(ClassLoadercl)用来获取和设置线程的上下文类加载器。
如果没有通过setContextClassLoader(ClassLoadercl)方法进行设置的话,线程将继承其父线程的上下文类加载器。
Java应用运行的初始线程的上下文类加载器是系统类加载器。
在线程中运行的代码可以通过此类加载器来加载类和资源。
前面提到的类加载器的代理模式并不能解决Java应用开发中会遇到的类加载器的全部问题。
Java提供了很多服务提供者接口(ServiceProviderInterface,SPI),允许第三方为这些接口提供实现。
常见的SPI有JDBC、JCE、JNDI、JAXP和JBI等。
这些SPI的接口由Java核心库来提供,如JAXP的SPI接口定义包含在
javax.xml.parsers包中。
这些SPI的实现代码很可能是作为Java应用所依赖的jar包被包含进来,可以通过类路径(CLASSPATH)来找到,如实现了JAXPSPI的
ApacheXerces所包含的jar包。
SPI接口中的代码经常需要加载具体的实现类。
如JAXP中的
javax.xml.parsers.DocumentBuilderFactory类中的
newInstance()方法用来生成一个新的DocumentBuilderFactory的实例。
这里的实例的真正的类是继承自
javax.xml.parsers.DocumentBuilderFactory,由SPI的实现所提供的。
如在ApacheXerces中,实现的类是
org.apache.xerces.jaxp.DocumentBuilderFactoryImpl。
而问题在于,SPI的接口是Java核心库的一部分,是由引导类加载器来加载的;
SPI实现的Java类一般是由系统类加载器来加载的。
引导类加载器是无法找到SPI的实现类的,因为它只加载Java的核心库。
它也不能代理给系统类加载器,因为它是系统类加载器的祖先类加载器。
也就是说,类加载器的代理模式无法解决这个问题。
线程上下文类加载器正好解决了这个问题。
如果不做任何的设置,Java应用的线程的上下文类加载器默认就是系统上下文类加载器。
在SPI接口的代码中使用线程上下文类加载器,就可以成功的加载到SPI实现的类。
线程上下文类加载器在很多SPI的实现中都会用到。
下面介绍另外一种加载类的方法:
Class.forName。
Class.forName
Class.forName是一个静态方法,同样可以用来加载类。
该方法有两种形式:
Class.forName(Stringname,booleaninitialize,ClassLoaderloader)和
Class.forName(StringclassName)。
第一种形式的参数
name表示的是类的全名;
initialize表示是否初始化类;
loader表示加载时使用的类加载器。
第二种形式则相当于设置了参数
initialize的值为
true,loader的值为当前类的类加载器。
Class.forName的一个很常见的用法是在加载数据库驱动的时候。
如Class.forName("
org.apache.derby.jdbc.EmbeddedDriver"
).newInstance()用来加载ApacheDerby数据库的驱动。
在介绍完类加载器相关的基本概念之后,下面介绍如何开发自己的类加载器。
回页首
开发自己的类加载器
虽然在绝大多数情况下,系统默认提供的类加载器实现已经可以满足需求。
但是在某些情况下,您还是需要为应用开发出自己的类加载器。
比如您的应用通过网络来传输Java类的字节代码,为了保证安全性,这些字节代码经过了加密处理。
这个时候您就需要自己的类加载器来从某个网络地址上读取加密后的字节代码,接着进行解密和验证,最后定义出要在Java虚拟机中运行的类来。
下面将通过两个具体的实例来说明类加载器的开发。
文件系统类加载器
第一个类加载器用来加载存储在文件系统上的Java字节代码。
完整的实现如
代码清单6所示。
清单6.文件系统类加载器
publicclassFileSystemClassLoaderextendsClassLoader{
privateStringrootDir;
publicFileSystemClassLoader(StringrootDir){
this.rootDir=rootDir;
protectedClass<
findClass(Stringname)throwsClassNotFoundException{
byte[]classData=getClassData(name);
if(classData==null){
thrownewClassNotFoundException();
else{
returndefineClass(name,classData,0,classData.length);
privatebyte[]getClassData(StringclassName){
Stringpath=classNameToPath(className);
InputStreamins=newFileInputStream(path);
ByteArrayOutputStreambaos=newByteArrayOutputStream();
intbufferSize=4096;
byte[]buffer=newbyte[bufferSize];
intbytesNumRead=0;
while((bytesNumRead=ins.read(buffer))!
=-1){
baos.write(buffer,0,bytesNumRead);
returnbaos.toByteArray();
}catch(IOExceptione){
returnnull;
privateStringclassNameToPath(StringclassName){
returnrootDir+File.separatorChar
+className.replace('
.'
File.separatorChar)+"
.class"
代码清单6所示,类
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 深入 了解 加载