1.类加载器的概念及其作用
顾名思义,类加载器(class loader)用来加载 Java 类到 Java 虚拟机中。一般来说,Java 虚拟机使用 Java 类的方式如下:Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件)。类加载器负责读取 Java 字节代码,并转换成java.lang.Class
类的一个实例。每个这样的实例用来表示一个 Java 类。通过此实例的 newInstance()
方法就可以创建出该类的一个对象。实际的情况可能更加复杂,比如 Java 字节代码可能是通过工具动态生成的,也可能是通过网络下载的。
2.java.lang.ClassLoader类介绍
java.lang.ClassLoader
类的基本职责就是根据一个指定的类的名称,找到或者生成其对应的字节代码,然后从这些字节代码中定义出一个 Java 类,即 java.lang.Class
类的一个实例。除此之外,ClassLoader
还负责加载 Java 应用所需的资源,如图像文件和配置文件等。不过本文只讨论其加载类的功能。为了完成加载类的这个职责,ClassLoader
提供了一系列的方法,比较重要的方法如图所示。
对于上图中给出的方法,表示类名称的 name
参数的值是类的二进制名称。需要注意的是内部类的表示,如 com.example.Sample$1
和com.example.Sample$Inner
等表示方式。
3.类加载器的树状组织结构
Java 中的类加载器大致可以分成两类,一类是系统提供的,另外一类则是由 Java 应用开发人员编写的。系统提供的类加载器主要有下面三个:
- 引导类加载器(bootstrap class loader):它用来加载 Java 的核心库,是用原生代码来实现的,并不继承自
java.lang.ClassLoader
。 - 扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
- 系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过
ClassLoader.getSystemClassLoader()
来获取它。
除了系统提供的类加载器以外,开发人员可以通过继承 java.lang.ClassLoader
类的方式实现自己的类加载器,以满足一些特殊的需求。
除了引导类加载器之外,所有的类加载器都有一个父类加载器。通过 表 1中给出的 getParent()
方法可以得到。对于系统提供的类加载器来说,系统类加载器的父类加载器是扩展类加载器,而扩展类加载器的父类加载器是引导类加载器;对于开发人员编写的类加载器来说,其父类加载器是加载此类加载器 Java 类的类加载器。因为类加载器 Java 类如同其它的 Java 类一样,也是要由类加载器来加载的。一般来说,开发人员编写的类加载器的父类加载器是系统类加载器。类加载器通过这种方式组织起来,形成树状结构。树的根节点就是引导类加载器。如图中给出了一个典型的类加载器树状组织结构示意图,其中的箭头指向的是父类加载器。
1.演示类加载器的树状组织结构
1.演示类加载器的树状组织结构
package com.itcast.classloader;
public class ClassLoaderTree {
/**
* @param args
*/
public static void main(String[] args) {
ClassLoader loader = ClassLoaderTree.class.getClassLoader();
while (loader != null) {
System.out.println(loader.toString());
loader = loader.getParent();
}
}
}
package com.itcast.classloader;
public class ClassLoaderTree {
/**
* @param args
*/
public static void main(String[] args) {
ClassLoader loader = ClassLoaderTree.class.getClassLoader();
while (loader != null) {
System.out.println(loader.toString());
loader = loader.getParent();
}
}
}
测试结果:
测试结果:
第一个输出的是 ClassLoaderTree类的类加载器,即系统类加载器。它是 sun.misc.Launcher$AppClassLoader类的实例;
第二个输出的是扩展类加载器,是 sun.misc.Launcher$ExtClassLoader类的实例。
需要注意的是这里并没有输出引导类加载器,
这是由于有些 JDK 的实现对于父类加载器是引导类加载器的情况,getParent()方法返回 null。
第一个输出的是 ClassLoaderTree类的类加载器,即系统类加载器。它是 sun.misc.Launcher$AppClassLoader类的实例;
第二个输出的是扩展类加载器,是 sun.misc.Launcher$ExtClassLoader类的实例。
需要注意的是这里并没有输出引导类加载器,
这是由于有些 JDK 的实现对于父类加载器是引导类加载器的情况,getParent()方法返回 null。
4.类加载器的全盘负责和委托机制
- classloader 加载类用的是全盘负责委托机制。
- 全盘负责:即是当一个classloader加载一个Class的时候,这个Class所依赖的和引用的其它Class通常也由这个classloader负责载入。
- 委托机制:先让parent(父)类加载器 寻找,只有在parent找不到的时候才从自己的类路径中去寻找。
- 类加载还采用了cache机制:如果 cache中保存了这个Class就直接返回它,如果没有才从文件中读取和转换成Class,并存入cache,这就是为什么修改了Class但是必须重新启动JVM才能生效,并且类只加载一次的原因。
代理模式是为了保证 Java 核心库的类型安全。所有 Java 应用都至少需要引用java.lang.Object类,也就是说在运行的时候,java.lang.Object这个类需要被加载到 Java 虚拟机中。如果这个加载过程由 Java 应用自己的类加载器来完成的话,很可能就存在多个版本的 java.lang.Object类,而且这些类之间是不兼容的。通过代理模式,对于 Java 核心库的类的加载工作由引导类加载器来统一完成,保证了 Java 应用所使用的都是同一个版本的 Java 核心库的类,是互相兼容的。
不同的类加载器为相同名称的类创建了额外的名称空间。相同名称的类可以并存在 Java 虚拟机中,只需要用不同的类加载器来加载它们即可。不同类加载器加载的类之间是不兼容的,这就相当于在 Java 虚拟机内部创建了一个个相互隔离的 Java类空间。这种技术在许多框架中都被用到。
代理模式是为了保证 Java 核心库的类型安全。所有 Java 应用都至少需要引用java.lang.Object类,也就是说在运行的时候,java.lang.Object这个类需要被加载到 Java 虚拟机中。如果这个加载过程由 Java 应用自己的类加载器来完成的话,很可能就存在多个版本的 java.lang.Object类,而且这些类之间是不兼容的。通过代理模式,对于 Java 核心库的类的加载工作由引导类加载器来统一完成,保证了 Java 应用所使用的都是同一个版本的 Java 核心库的类,是互相兼容的。
不同的类加载器为相同名称的类创建了额外的名称空间。相同名称的类可以并存在 Java 虚拟机中,只需要用不同的类加载器来加载它们即可。不同类加载器加载的类之间是不兼容的,这就相当于在 Java 虚拟机内部创建了一个个相互隔离的 Java类空间。这种技术在许多框架中都被用到。
真正完成类的加载工作是通过调用 defineClass
来实现的;而启动类的加载过程是通过调用 loadClass
来实现的。前者称为一个类的定义加载器(defining loader),后者称为初始加载器(initiating loader)。在 Java 虚拟机判断两个类是否相同的时候,使用的是类的定义加载器。也就是说,哪个类加载器启动类的加载过程并不重要,重要的是最终定义这个类的加载器。两种类加载器的关联之处在于:一个类的定义加载器是它引用的其它类的初始加载器。如类 com.example.Outer
引用了类com.example.Inner
,则由类 com.example.Outer
的定义加载器负责启动类 com.example.Inner
的加载过程。
5.开发自己的类加载器
虽然在绝大多数情况下,系统默认提供的类加载器实现已经可以满足需求。但是在某些情况下,您还是需要为应用开发出自己的类加载器。比如您的应用通过网络来传输 Java 类的字节代码,为了保证安全性,这些字节代码经过了加密处理。这个时候您就需要自己的类加载器来从某个网络地址上读取加密后的字节代码,接着进行解密和验证,最后定义出要在 Java 虚拟机中运行的类来。
1.获取字节码文件,加密---->保存在工程res目录下
1.获取字节码文件,加密---->保存在工程res目录下
2.将工程下bin目录下的.class文件删除.
2.将工程下bin目录下的.class文件删除.
3.测试,用自己的类加载器加载.
3.测试,用自己的类加载器加载.
package com.itcast.classloader;
import java.util.Date;
public class MyClass extends Date{
/**
*
*/
private static final long serialVersionUID = -6002593698605131409L;
@Override
public String toString() {
return "Hello,World.This is my class";
}
}
package com.itcast.classloader;
import java.util.Date;
public class MyClass extends Date{
/**
*
*/
private static final long serialVersionUID = -6002593698605131409L;
@Override
public String toString() {
return "Hello,World.This is my class";
}
}
package com.itcast.classloader;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class MyClassLoader extends ClassLoader{
private String classDir;
public MyClassLoader(String classDir) {
this.classDir = classDir;
}
public MyClassLoader(){
}
public static void main(String[] args) {
String srcPath=args[0];
String destDir=args[1];
try {
/*将.class文件加密,并且输出到res目录下*/
InputStream fis=new FileInputStream(srcPath);
String destFileName=srcPath.substring(srcPath.lastIndexOf("\\")+1);
String destFilePath=destDir+"\\"+destFileName;
OutputStream fos=new FileOutputStream(destFilePath);
cypher(fis, fos);
fis.close();
fos.close();
System.out.println("OK");
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 加密字节码文件
* @param in
* @param out
*/
private static void cypher(InputStream in,OutputStream out){
int b=-1;
try {
while((b=in.read())!=-1){
out.write(b^0xff);
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 加载字节码文件
*/
@SuppressWarnings("deprecation")
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String classFileName=classDir+"\\"+name+".class";
try {
FileInputStream fis=new FileInputStream(classFileName);
ByteArrayOutputStream bos=new ByteArrayOutputStream();
cypher(fis, bos);
fis.close();
System.out.println("MyClassLoader加载的");
byte[] bytes=bos.toByteArray();
/*将字节数组转化为Class实例*/
return defineClass(bytes, 0, bytes.length);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return super.findClass(name);
}
}
package com.itcast.classloader;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class MyClassLoader extends ClassLoader{
private String classDir;
public MyClassLoader(String classDir) {
this.classDir = classDir;
}
public MyClassLoader(){
}
public static void main(String[] args) {
String srcPath=args[0];
String destDir=args[1];
try {
/*将.class文件加密,并且输出到res目录下*/
InputStream fis=new FileInputStream(srcPath);
String destFileName=srcPath.substring(srcPath.lastIndexOf("\\")+1);
String destFilePath=destDir+"\\"+destFileName;
OutputStream fos=new FileOutputStream(destFilePath);
cypher(fis, fos);
fis.close();
fos.close();
System.out.println("OK");
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 加密字节码文件
* @param in
* @param out
*/
private static void cypher(InputStream in,OutputStream out){
int b=-1;
try {
while((b=in.read())!=-1){
out.write(b^0xff);
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 加载字节码文件
*/
@SuppressWarnings("deprecation")
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String classFileName=classDir+"\\"+name+".class";
try {
FileInputStream fis=new FileInputStream(classFileName);
ByteArrayOutputStream bos=new ByteArrayOutputStream();
cypher(fis, bos);
fis.close();
System.out.println("MyClassLoader加载的");
byte[] bytes=bos.toByteArray();
/*将字节数组转化为Class实例*/
return defineClass(bytes, 0, bytes.length);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return super.findClass(name);
}
}
测试条件:
测试条件:
测试结果:
测试结果:
子类加载:
子类加载:
package com.itcast.classloader;
import com.itcast.annotation.AnnotationTest;
public class ClassLoaderTest {
@SuppressWarnings("rawtypes")
public static void main(String[] args) {
System.out.println(
//sun.misc.Launcher$AppClassLoader
//%CLASSPATH%
ClassLoaderTest.class.getClassLoader().getClass().getName()
);
System.out.println(
//bootStrap------>%HOME_JAVA%\jre\lib\rt.jar
System.class.getClassLoader()
);
//sun.misc.Launcher$ExtClassLoader
//%HOME_JAVA%\jre\lib\ext\itcast.jar
System.out.println(
AnnotationTest.class.getClassLoader().getClass().getName()
);
System.out.println("******************************************");
ClassLoader classLoader=ClassLoaderTest.class.getClassLoader();
while(classLoader!=null){
System.out.println(classLoader.getClass().getName());
classLoader=classLoader.getParent();
}
System.out.println("*******************************************");
try {
Class clazz=new MyClassLoader("res").loadClass("MyClass");
Object obj=clazz.newInstance().toString();
System.out.println(obj);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
package com.itcast.classloader;
import com.itcast.annotation.AnnotationTest;
public class ClassLoaderTest {
@SuppressWarnings("rawtypes")
public static void main(String[] args) {
System.out.println(
//sun.misc.Launcher$AppClassLoader
//%CLASSPATH%
ClassLoaderTest.class.getClassLoader().getClass().getName()
);
System.out.println(
//bootStrap------>%HOME_JAVA%\jre\lib\rt.jar
System.class.getClassLoader()
);
//sun.misc.Launcher$ExtClassLoader
//%HOME_JAVA%\jre\lib\ext\itcast.jar
System.out.println(
AnnotationTest.class.getClassLoader().getClass().getName()
);
System.out.println("******************************************");
ClassLoader classLoader=ClassLoaderTest.class.getClassLoader();
while(classLoader!=null){
System.out.println(classLoader.getClass().getName());
classLoader=classLoader.getParent();
}
System.out.println("*******************************************");
try {
Class clazz=new MyClassLoader("res").loadClass("MyClass");
Object obj=clazz.newInstance().toString();
System.out.println(obj);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
父类加载:Class clazz=new MyClassLoader("res").loadClass("com.itcast.classloader.MyClass");
父类加载:Class clazz=new MyClassLoader("res").loadClass("com.itcast.classloader.MyClass");
同时也可以将:MyClassLoader.java中name----->name.substring(name.lastIndexOf('.')+1)
同时也可以将:MyClassLoader.java中name----->name.substring(name.lastIndexOf('.')+1)
测试结果:
测试结果:
6.类加载器与 Web 容器
对于运行在 Java EE容器中的 Web 应用来说,类加载器的实现方式与一般的 Java 应用有所不同。不同的 Web 容器的实现方式也会有所不同。以 Apache Tomcat 来说,每个 Web 应用都有一个对应的类加载器实例。该类加载器也使用代理模式,所不同的是它是首先尝试去加载某个类,如果找不到再代理给父类加载器。这与一般类加载器的顺序是相反的。这是 Java Servlet 规范中的推荐做法,其目的是使得 Web 应用自己的类的优先级高于 Web 容器提供的类。这种代理模式的一个例外是:Java 核心库的类是不在查找范围之内的。这也是为了保证 Java 核心库的类型安全。
绝大多数情况下,Web 应用的开发人员不需要考虑与类加载器相关的细节。下面给出几条简单的原则:
- 每个 Web 应用自己的 Java 类文件和使用的库的 jar 包,分别放在
WEB-INF/classes
和WEB-INF/lib
目录下面。 - 多个应用共享的 Java 类文件和 jar 包,分别放在 Web 容器指定的由所有 Web 应用共享的目录下面。
- 当出现找不到类的错误时,检查当前类的类加载器和当前线程的上下文类加载器是否正确。
7.总结
类加载器是 Java 语言的一个创新。它使得动态安装和更新软件组件成为可能。本文详细介绍了类加载器的相关话题,包括基本概念、代理模式、线程上下文类加载器、与 Web 容器和 OSGi 的关系等。开发人员在遇到 ClassNotFoundException
和 NoClassDefFoundError
等异常的时候,应该检查抛出异常的类的类加载器和当前线程的上下文类加载器,从中可以发现问题的所在。在开发自己的类加载器的时候,需要注意与已有的类加载器组织结构的协调。
参考文档: 深入探讨 Java 类加载器