最近一个项目需求,实现基于http接口的外部jar包动态类加载。我平台提供标准化的接口,接口的具体实现由业务方实现。业务方根据开发规范,实现接口后,打包成jar文件,上传至平台上,用户调用接口的时候,动态载入jar文件,运行结果返回。整个过程,业务方开发人员通过平台的管理页面配置,并上传实现的jar包,即可把能力添加到我平台上。
整个项目的一个关键点事如何动态加载类文件,还必须实现动态更新。
基于这一目标,对java类加载做了具体学习。
类的装载方式
首先我们来认识一下ClassLoader,程序在启动的时候,并不会一次性加载程序所要用的所有class文件,而是根据程序的需要,通过Java的类加载机制(ClassLoader)来动态加载某个 class 文件到内存当中的,从而只有 class 文件被载入到了内存之后,才能被其它 class 所引用。所以 ClassLoader 就是用来动态加载 class 文件到内存当中用的。
ClassLoader主要对类的请求提供服务,当JVM需要某类时,它根据名称向ClassLoader要求这个类,然后由ClassLoader返回这个类的class对象。
Classloader就是负责将class加载到JVM中,基于某个加载模型确定到底由谁加载,最后将 Class 字节码重新解析成 JVM 统一要求的对象格式。
JAVA类装载方式,有两种:
1.隐式装载,程序在运行过程中当碰到通过new 等方式生成对象时,隐式调用类装载器加载对应的类到jvm中。
2.显式装载,通过class.forname(),this.getClassLoader().loadClass()等方法,显式加载需要的类。
类的装载过程
看一下ClassLoader中loadClass方法的实现:
1.调用findLoaderClass查看是否已存在装入的类。存在就直接返回class对象(java中任何类型都有对应的class对象,哪怕是void也有)。
2.findSystemClass查找本地文件系统,根据不同的类装载器,文件系统位置不一样。没有找到则返回ClassNotFoundException.
3.difineClass 接收以字节数组表示的类字节码,并把它转换成 Class 实例
4.resolveClass 链接一个指定的类。(jvm装载类的三个过程为load ,link,initialize)
loadClass方法的默认实现按照以下顺序查找类: 调用findLoadedClass(String) 方法检查这个类是否被加载过 使用父加载器调用 loadClass(String) 方法,如果父加载器为 Null,类加载器装载虚拟机内置的加载器调用 findClass(String) 方法装载类, 如果,按照以上的步骤成功的找到对应的类,并且该方法接收的 resolve 参数的值为 true,那么就调用resolveClass(Class) 方法来处理类。
ClassLoader 的子类最好覆盖 findClass(String) 而不是这个方法。 除非被重写,这个方法默认在整个装载过程中都是同步的(线程安全的)。
类装载器
ClassLoader 在加载类时有一定的层次关系和规则,树状组织结构。在 Java 中,有四种类型的类加载器,分别为:BootStrapClassLoader、ExtClassLoader、AppClassLoader 以及用户自定义的 ClassLoader
1.BootStrapClassLoader(引导类加载器) 处于类加载器层次结构的最高层,负责 sun.boot.class.path 路径下类的加载,默认为 jre/lib 目录下的核心 API 或 -Xbootclasspath 选项指定的 jar 包。
2.ExtClassLoader (扩展类加载器)的加载路径为 java.ext.dirs,默认为 jre/lib/ext 目录或者 -Djava.ext.dirs 指定目录下的 jar 包加载。
3.AppClassLoader(系统类加载器) 的加载路径为 java.class.path,默认为环境变量 CLASSPATH 中设定的值。也可以通过 -classpath 选型进行指定。
4.用户自定义 ClassLoader 可以根据用户的需要定制自己的类加载过程,在运行期进行指定类的动态实时加载。
一般来说,这四种类加载器会形成一种父子关系,高层为低层的父加载器。在进行类加载时,首先会自底向上挨个检查是否已经加载了指定类,如果已经加载则直接返回该类的引用。如果到最高层也没有加载过指定类,那么会自顶向下挨个尝试加载,直到用户自定义类加载器,如果还不能成功,就会抛出异常。
对于同一个类加载器实例来说,名字相同的类只能存在一个,并且仅加载一次。不管该类有没有变化,下次再需要加载时,它只是从自己的缓存中直接返回已经加载过的类引用。
类加载原理——双亲委托模型
每个ClassLoader实例都有一个父类加载器的引用(不是继承的关系,是一个包含的关系),虚拟机内置的类加载器(Bootstrap ClassLoader)本身没有父类加载器,但可以用作其它ClassLoader实例的的父类加载器。
当一个ClassLoader实例需要加载某个类时,它会试图亲自搜索某个类之前,先把这个任务委托给它的父类加载器,这个过程是由上至下依次检查的,首先由最顶层的类加载器Bootstrap ClassLoader试图加载,如果没加载到,则把任务转交给Extension ClassLoader试图加载,如果也没加载到,则转交给App ClassLoader 进行加载,如果它也没有加载得到的话,则返回给委托的发起者,由它到指定的文件系统或网络等URL中加载该类。
如果它们都没有加载到这个类时,则抛出ClassNotFoundException异常。否则将这个找到的类生成一个类的定义,并将它加载到内存当中,最后返回这个类在内存中的Class实例对象。
为什么要使用双亲委托这种模型呢?
因为这样可以避免重复加载,当父亲已经加载了该类的时候,就没有必要 ClassLoader再加载一次。考虑到安全因素,我们试想一下,如果不使用这种委托模式,那我们就可以随时使用自定义的String来动态替代java核心api中定义的类型,这样会存在非常大的安全隐患,而双亲委托的方式,就可以避免这种情况,因为String已经在启动时就被引导类加载器(Bootstrcp ClassLoader)加载,所以用户自定义的ClassLoader永远也无法加载一个自己写的String,除非你改变JDK中ClassLoader搜索类的默认算法。
但是JVM在搜索类的时候,又是如何判定两个class是相同的呢?
JVM在判定两个class是否相同时,不仅要判断两个类名是否相同,而且要判断是否由同一个类加载器实例加载的。只有两者同时满足的情况下,JVM才认为这两个class是相同的。就算两个class是同一份class字节码,如果被两个不同的ClassLoader实例所加载,JVM也会认为它们是两个不同class。
Java装载类使用“全盘负责委托机制”。“全盘负责”是指当一个ClassLoder装载一个类时,该类所依赖及引用的类也由这个ClassLoder载入(除非显示的使用另外一个ClassLoder);“委托机制”是指先委托父类装载器寻找目标类,只有在找不到的情况下才从自己的类路径中查找并装载目标类。
线程上下文类加载器
每个运行中的线程都有一个成员ContextClassLoader,用来在运行时动态地载入其它类,可以使用方法Thread.currentThread().setContextClassLoader(...);更改当前线程的contextClassLoader,来改变其载入类的行为;也可以通过方法Thread.currentThread().getContextClassLoader()来获得当前线程的ClassLoader。
Java 应用运行的初始线程的上下文类加载器是系统类加载器。在线程中运行的代码可以通过此类加载器来加载类和资源。
实际上,在Java应用中所有程序都运行在线程里,如果在程序中没有手工设置过ClassLoader,对于一般的java类如下两种方法获得的ClassLoader通常都是同一个。
this.getClass.getClassLoader();
Thread.currentThread().getContextClassLoader();
方法一得到的Classloader是静态的,表明类的载入者是谁;
方法二得到的Classloader是动态的,谁执行(某个线程),就是那个执行者的Classloader。
对于单例模式的类,静态类等,载入一次后,这个实例会被很多程序(线程)调用,对于这些类,载入的Classloader和执行线程的Classloader通常都不同。
WEB容器类加载器
对于运行在 Java EE容器中的 Web 应用来说,类加载器的实现方式与一般的 Java 应用有所不同。不同的 Web 容器的实现方式也会有所不同。
以 Apache Tomcat 来说,每个 Web 应用都有一个对应的类加载器实例。该类加载器也使用代理模式,所不同的是它是首先尝试去加载某个类,如果找不到再代理给父类加载器。这与一般类加载器的顺序是相反的。这是 Java Servlet 规范中的推荐做法,其目的是使得 Web 应用自己的类的优先级高于 Web 容器提供的类。
这种代理模式的一个例外是:Java 核心库的类是不在查找范围之内的。这也是为了保证 Java 核心库的类型安全。
对于WEB APP线程,它的contextClassLoader是WebAppClassLoader
对于Tomcat Server线程,它的contextClassLoader是CatalinaClassLoader
自定义类加载器
默认的classloader只能加载指定目录下的jar和class,如果想加载其它位置的类或jar时,需要定义自己的classloader
定义自已的类加载器分为两步:
1、继承java.lang.ClassLoader
2、重写父类的findClass方法
文件类加载器
加载存储在文件系统上的 Java 字节代码。
类 FileSystemClassLoader继承自类java.lang.ClassLoader。为了保证类加载器都正确实现代理模式,在开发自己的类加载器时,最好不要覆写 loadClass()方法,而是覆写findClass()方法。
类 FileSystemClassLoader的 findClass()方法首先根据类的全名在硬盘上查找类的字节代码文件(.class 文件),然后读取该文件内容,最后通过 defineClass()方法来把这些字节代码转换成 java.lang.Class类的实例。
public class FileSystemClassLoader extends ClassLoader
{
private String rootDir;
public FileSystemClassLoader(String rootDir){
this.rootDir = rootDir;
}
protected Class> findClass(String name) throws ClassNotFoundException {
byte[] classData = getClassData(name);
if (classData == null){
throw new ClassNotFoundException();
}
else {
return defineClass(name, classData, 0, classData.length);
}
}
private byte[] getClassData(String className) {
String path = classNameToPath(className);
try {
InputStream ins = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int bytesNumRead = 0;
while ((bytesNumRead = ins.read(buffer)) != -1){
baos.write(buffer, 0, bytesNumRead);
}
return baos.toByteArray();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
private String classNameToPath(String className) {
return rootDir + File.separatorChar + className.replace('.', File.separatorChar) + ".class";
}
}
网络类加载器
一个网络类加载器来说明如何通过类加载器来实现组件的动态更新。即基本的场景是:Java 字节代码(.class)文件存放在服务器上,客户端通过网络的方式获取字节代码并执行。当有版本更新的时候,只需要替换掉服务器上保存的文件即可。通过类加载器可以比较简单的实现这种需求。
类 NetworkClassLoader 负责通过网络下载 Java 类字节代码并定义出 Java 类。它的实现与FileSystemClassLoader 类似。在通过 NetworkClassLoader 加载了某个版本的类之后,一般有两种做法来使用它。第一种做法是使用 Java 反射 API。另外一种做法是使用接口。
OSGI动态模块系统
osgi是java动态化模块系统,动态模块部署。面向服务和基于组件的运行环境。组件尽可能解耦,能让组件动态的发现其他组件。
这个有兴趣也可以去研究关注一下。
关于《java类的热替换》参考:
https://www.ibm.com/developerworks/cn/java/j-lo-hotswapcls/