JAVA
类动态载入的实现
1
前言
前段时间因为项目的需要,我搞了一套类似
Servlet
重新载入的一个框架,实现了类的动态载入过程。本文写了一些我的学习成果以及心得供大家分享一下。
2
类载入的原理
(下面引用网上的一篇文章):
当
JVM
(
Java
虚拟机)启动时,会形成由三个类加载器组成的初始类加载器层次结构:
bootstrap classloader
|
extension classloader
|
system classloader
bootstrap classloader
-
引导(也称为原始)类加载器,它负责加载
Java
的核心类。在
Sun
的
JVM
中,在执行
java
的命令中使用
-Xbootclasspath
选项或使用
-D
选项指定
sun.boot.class.path
系统属性值可以指定附加的类。这个加载器的是非常特殊的,它实际上不是
java.lang.ClassLoader
的子类,而是由
JVM
自身实现的。大家可以通过执行以下代码来获得
bootstrap classloader
加载了那些核心类库:
URL[] urls=sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (int i = 0; i < urls.length; i++) {
System.out.println(urls
.toExternalForm());
}
在我的计算机上的结果为:
file:/C:/j2sdk1.4.1_01/jre/lib/endorsed/dom.jar
file:/C:/j2sdk1.4.1_01/jre/lib/endorsed/sax.jar
file:/C:/j2sdk1.4.1_01/jre/lib/endorsed/xalan-2.3.1.jar
file:/C:/j2sdk1.4.1_01/jre/lib/endorsed/xercesImpl-2.0.0.jar
file:/C:/j2sdk1.4.1_01/jre/lib/endorsed/xml-apis.jar
file:/C:/j2sdk1.4.1_01/jre/lib/endorsed/xsltc.jar
file:/C:/j2sdk1.4.1_01/jre/lib/rt.jar
file:/C:/j2sdk1.4.1_01/jre/lib/i18n.jar
file:/C:/j2sdk1.4.1_01/jre/lib/sunrsasign.jar
file:/C:/j2sdk1.4.1_01/jre/lib/jsse.jar
file:/C:/j2sdk1.4.1_01/jre/lib/jce.jar
file:/C:/j2sdk1.4.1_01/jre/lib/charsets.jar
file:/C:/j2sdk1.4.1_01/jre/classes
这时大家知道了为什么我们不需要在系统属性
CLASSPATH
中指定这些类库了吧,因为
JVM
在启动的时候就自动加载它们了。
extension classloader
-
扩展类加载器,它负责加载
JRE
的扩展目录(
JAVA_HOME/jre/lib/ext
或者由
java.ext.dirs
系统属性指定的)中
JAR
的类包。这为引入除
Java
核心类以外的新功能提供了一个标准机制。因为默认的扩展目录对所有从同一个
JRE
中启动的
JVM
都是通用的,所以放入这个目录的
JAR
类包对所有的
JVM
和
system classloader
都是可见的。在这个实例上调用方法
getParent()
总是返回空值
null
,因为引导加载器
bootstrap classloader
不是一个真正的
ClassLoader
实例。所以当大家执行以下代码时:
System.out.println(System.getProperty("java.ext.dirs"));
ClassLoader extensionClassloader=ClassLoader.getSystemClassLoader().getParent();
System.out.println("the parent of extension classloader : "+extensionClassloader.getParent());
结果为:
C:\j2sdk1.4.1_01\jre\lib\ext
the parent of extension classloader : null
extension classloader
是
system classloader
的
parent
,而
bootstrap classloader
是
extension classloader
的
parent
,但它不是一个实际的
classloader
,所以为
null
。
system classloader
-
系统(也称为应用)类加载器,它负责在
JVM
被启动时,加载来自在命令
java
中的
-classpath
或者
java.class.path
系统属性或者
CLASSPATH
操作系统属性所指定的
JAR
类包和类路径。总能通过静态方法
ClassLoader.getSystemClassLoader()
找到该类加载器。如果没有特别指定,则用户自定义的任何类加载器都将该类加载器作为它的父加载器。执行以下代码即可获得:
System.out.println(System.getProperty("java.class.path"));
输出结果则为用户在系统属性里面设置的
CLASSPATH
。
classloader
加载类用的是全盘负责委托机制。所谓全盘负责,即是当一个
classloader
加载一个
Class
的时候,这个
Class
所依赖的和引用的所有
Class
也由这个
classloader
负责载入,除非是显式的使用另外一个
classloader
载入;委托机制则是先让
parent
(父)类加载器
(
而不是
super
,它与
parent classloader
类不是继承关系
)
寻找,只有在
parent
找不到的时候才从自己的类路径中去寻找。此外类加载还采用了
cache
机制,也就是如果
cache
中保存了这个
Class
就直接返回它,如果没有才从文件中读取和转换成
Class
,并存入
cache
,这就是为什么我们修改了
Class
但是必须重新启动
JVM
才能生效的原因。
每个
ClassLoader
加载
Class
的过程是:
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.
其中
5.6
步我们可以通过覆盖
ClassLoader
的
findClass
方法来实现自己的载入策略。甚至覆盖
loadClass
方法来实现自己的载入过程。
类加载器的顺序是:
先是
bootstrap classloader
,然后是
extension classloader
,最后才是
system classloader
。大家会发现加载的
Class
越是重要的越在靠前面。这样做的原因是出于安全性的考虑,试想如果
system classloader“
亲自
”
加载了一个具有破坏性的
“java.lang.System”
类的后果吧。这种委托机制保证了用户即使具有一个这样的类,也把它加入到了类路径中,但是它永远不会被载入,因为这个类总是由
bootstrap classloader
来加载的。大家可以执行一下以下的代码:
System.out.println(System.class.getClassLoader());
将会看到结果是
null
,这就表明
java.lang.System
是由
bootstrap classloader
加载的,因为
bootstrap classloader
不是一个真正的
ClassLoader
实例,而是由
JVM
实现的,正如前面已经说过的。
下面就让我们来看看
JVM
是如何来为我们来建立类加载器的结构的:
sun.misc.Launcher
,顾名思义,当你执行
java
命令的时候,
JVM
会先使用
bootstrap classloader
载入并初始化一个
Launcher
,执行下来代码:
System.out.println("the Launcher's classloader is "+sun.misc.Launcher.getLauncher().getClass().getClassLoader());
结果为:
the Launcher's classloader is null (
因为是用
bootstrap classloader
加载
,
所以
class loader
为
null)
Launcher
会根据系统和命令设定初始化好
class loader
结构,
JVM
就用它来获得
extension classloader
和
system classloader,
并载入所有的需要载入的
Class
,最后执行
java
命令指定的带有静态的
main
方法的
Class
。
extension classloader
实际上是
sun.misc.Launcher$ExtClassLoader
类的一个实例,
system classloader
实际上是
sun.misc.Launcher$AppClassLoader
类的一个实例。并且都是
java.net.URLClassLoader
的子类。
让我们来看看
Launcher
初试化的过程的部分代码。
Launcher
的部分代码:
public class Launcher {
public Launcher() {
ExtClassLoader extclassloader;
try {
//
初始化
extension classloader
extclassloader = ExtClassLoader.getExtClassLoader();
} catch(IOException ioexception) {
throw new InternalError("Could not create extension class loader");
}
try {
//
初始化
system classloader
,
parent
是
extension classloader
loader = AppClassLoader.getAppClassLoader(extclassloader);
} catch(IOException ioexception1) {
throw new InternalError("Could not create application class loader");
}
//
将
system classloader
设置成当前线程的
context classloader
(将在后面加以介绍)
Thread.currentThread().setContextClassLoader(loader);
......
}
public ClassLoader getClassLoader() {
//
返回
system classloader
return loader;
}
}
extension classloader
的部分代码:
static class Launcher$ExtClassLoader extends URLClassLoader {
public static Launcher$ExtClassLoader getExtClassLoader()
throws IOException
{
File afile[] = getExtDirs();
return (Launcher$ExtClassLoader)AccessController.doPrivileged(new Launcher$1(afile));
}
private static File[] getExtDirs() {
//
获得系统属性
“java.ext.dirs”
String s = System.getProperty("java.ext.dirs");
File afile[];
if(s != null) {
StringTokenizer stringtokenizer = new StringTokenizer(s, File.pathSeparator);
int i = stringtokenizer.countTokens();
afile = new File;
for(int j = 0; j < i; j++)
afile[j] = new File(stringtokenizer.nextToken());
} else {
afile = new File[0];
}
return afile;
}
}
system classloader
的部分代码:
static class Launcher$AppClassLoader extends URLClassLoader
{
public static ClassLoader getAppClassLoader(ClassLoader classloader)
throws IOException
{
//
获得系统属性
“java.class.path”
String s = System.getProperty("java.class.path");
File afile[] = s != null ? Launcher.access$200(s) : new File[0];
return (Launcher$AppClassLoader)AccessController.doPrivileged(new Launcher$2(s, afile, classloader));
}
}
看了源代码大家就清楚了吧,
extension classloader
是使用系统属性
“java.ext.dirs”
设置类搜索路径的,并且没有
parent
。
system classloader
是使用系统属性
“java.class.path”
设置类搜索路径的,并且有一个
parent classloader
。
Launcher
初始化
extension classloader
,
system classloader
,并将
system classloader
设置成为
context classloader
,但是仅仅返回
system classloader
给
JVM
。
这里怎么又出来一个
context classloader
呢?它有什么用呢?我们在建立一个线程
Thread
的时候,可以为这个线程通过
setContextClassLoader
方法来指定一个合适的
classloader
作为这个线程的
context classloader
,当此线程运行的时候,我们可以通过
getContextClassLoader
方法来获得此
context classloader
,就可以用它来载入我们所需要的
Class
。默认的是
system classloader
。利用这个特性,我们可以
“
打破
”classloader
委托机制了,父
classloader
可以获得当前线程的
context classloader
,而这个
context classloader
可以是它的子
classloader
或者其他的
classloader
,那么父
classloader
就可以从其获得所需的
Class
,这就打破了只能向父
classloader
请求的限制了。这个机制可以满足当我们的
classpath
是在运行时才确定
,
并由定制的
classloader
加载的时候
,
由
system classloader(
即在
jvm classpath
中
)
加载的
class
可以通过
context classloader
获得定制的
classloader
并加载入特定的
class(
通常是抽象类和接口
,
定制的
classloader
中是其实现
),
例如
web
应用中的
servlet
就是用这种机制加载的
.
好了,现在我们了解了
classloader
的结构和工作原理,那么我们如何实现在运行时的动态载入和更新呢?只要我们能够动态改变类搜索路径和清除
classloader
的
cache
中已经载入的
Class
就行了,有两个方案,一是我们继承一个
classloader
,覆盖
loadclass
方法,动态的寻找
Class
文件并使用
defineClass
方法来;另一个则非常简单实用,只要重新使用一个新的类搜索路径来
new
一个
classloader
就行了,这样即更新了类搜索路径以便来载入新的
Class
,也重新生成了一个空白的
cache(
当然
,
类搜索路径不一定必须更改
)
。
(结束)
以上所述,想必大家对
Jvm
类载入的原理有了一定的了解,大致也猜到实现的方法了吧。
3
实现类的动态载入
结合上面说到,要
JVM
重新载入一个类,一共有
3
种办法:(我加了一种)
1.
重新写一个新的路径,更换包名或类名都是可行的方法。
2.
自己写一个类的寻找机制取代
SystemClassLoader
中的,再调用
defineClass
方法。
3.
覆盖
loadClass
方法,自己实现一个类载入过程。
显而易见,第一种方法是我自己添加上去的,而且应该是用得最多得方法,因为在对
JVM
的类搜索、载入机制不是很熟悉的前提下,只要做一些简单的修改就能达到所要的效果。(至少在我的项目中,这是首选,因为不会出错)
但如果第一种方法能实现,本文就没有写的意义了。在一个
7 x 24
的大型系统中(如电信,银行等),用第一种方法,就凸现出它最大问题,就是代码变得无比庞大。因为每一个类的名字不同导致相同功能的类具有两个或以上的类文件存在,对代码的开发和维护来说,这是非常严重的问题。
好,现在又退回到开始,排除掉第一种方法,如果使用第三种方法,应该是可控性最高的方法,但本人才疏学浅,没有把握把整个类载入过程摸透,因此还是排除的第三种方法。
剩下只有第二种方法了。在
ClassLoader
中,
defineClass
方法是比较好理解的,只要传入类的字节流即可,因此,我只要重写一次类文件的载入。下面是我的实现代码:
public class testClassLoader extends ClassLoader {
private static testClassLoader cl = null;
private static boolean flag = true;
private InputStream classFile = null;
private String name = null;
/**
* @param name String
类全名
* @param url URL
类路径
* @throws FileNotFoundException
* @throws IOException
*/
public testClassLoader(String name, URL url) throws
FileNotFoundException, IOException {
super(getSystemClassLoader());
this.name = name + ".class";
//
打开
URL
指定的资源
URLConnection con = url.openConnection();
InputStream classIs = con.getInputStream();
this.classFile = classIs;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte buf[] = new byte[1024];
//
读取文件流
for (int i = 0; (i = classIs.read(buf)) != -1; ) {
baos.write(buf, 0, i);
}
classIs.close();
baos.close();
//
创建新的类对象
byte[] data = baos.toByteArray();
defineClass(name, data, 0, data.length);
}
/**
*
重载
getResourceAsStream()
是为了返回该类的文件流。
* @return an InputStream of the class bytes, or null
*/
public InputStream getResourceAsStream(String resourceName) {
try {
if (resourceName.equals(name)) {
return this.classFile;
}
} catch (Exception e) {
return null;
}
return null;
}
}
相信大家已经明白了吧。我在
ClassLoader
构造时便载入了指定的类文件,因此,就跳过了在
new
新的对象时去查找
Cache
中该类是否有载入的过程。(注明一下:我的类路径用了
URL
的方式传入,目的是为了适应多平台的机制,因为
Window
和
Linux
的路径命名不一样)
相信聪明的你一定猜到后面应该怎么做了吧,没错,就是用
Class.forName
构建类:
String name = "test.testBlank";
URL url = new URL("file:/c:/test/classes/test/testBlank.class");
ClassLoader cl = new testClassLoader(name, url);
Class c = Class.forName(name, false, cl);
//
实例化
Object obj = c.newInstance();
testInterface i = (testInterface) obj;
i.run();
你一定注意到了,为什么我用了一个
testInterface
,其实自定义
ClassLoader
中,不能直接以当前的类去直接强制转换,因为在
JVM
中,不同的
ClassLoader
载入同一个类,在
JVM
中是两个完全不同的类。因此,默认的
class
文件都是
SystemClassLoader
,所以会抛出
ClassCastException
。解决方法很简单,只要该对象实现一个接口,然后用父类去强制转换就没有问题了。
以上只是我在学习过程中的一点积累,如果有什么错误,欢迎大家评论。