【Javassist官方文档翻译】第三章类加载器
创始人
2024-02-09 12:02:44
0

系列文章目录

第一章 读写字节码
第二章 类池
第三章 类加载器


文章目录

  • 系列文章目录
  • 前言
  • 类加载器
    • 3.1 CtClass中的toClass 方法
    • 3.2 Java 中的类加载
    • 3.3 使用javassist.Loader
    • 3.4 编写类加载器
    • 3.5 修改系统类
    • 3.6 运行时重新加载类
  • 总结
  • 说明


前言

在上一章我们介绍了Javassist类池相关的一些操作,本章我们会介绍Javassist中的类加载器。


类加载器

如果事先知道想要修改的类,那么修改类的最简单的方法如下:

1.通过调用ClassPool对象的get方法来获取一个CtClass对象。
2.修改这个类
3.调用CtClass 对象的writeFile方法或toBytecode方法获取修改后的类文件。

如果想要在加载时确定类是否被修改,则用户必须使 Javassist 与类加载器协作。Javassist 可以与类加载器一起使用,以便可以在加载时修改字节码。Javassist 的用户可以自定义自己的类加载器,也可以使用 Javassist 提供的类加载器。

3.1 CtClass中的toClass 方法

CtClass类提供了一个方便的toClass方法,请求当前线程的上下文类加载器加载该CtClass 对象所代表的类。要调用此方法,调用者必须具有适当的权限;否则,程序可能会抛出 SecurityException异常。

以下程序展示如何使用toClass方法

public class Hello {public void say() {System.out.println("Hello");}
}public class Test {public static void main(String[] args) throws Exception {ClassPool cp = ClassPool.getDefault();CtClass cc = cp.get("Hello");CtMethod m = cc.getDeclaredMethod("say");m.insertBefore("{ System.out.println(\"Hello.say():\"); }");Class c = cc.toClass();Hello h = (Hello)c.newInstance();h.say();}
}

Test.main方法在Hello的say方法的方法体中插入一个对println方法的调用。然后构造出修改后的Hello类的实例,最后调用该实例的say方法。

请注意,上面的程序取决于这样一个事实,即在调用toClass方法之前从未加载过Hello类。否则,JVM将在toClass方法请求加载修改后的Hello类之前加载原始的Hello类。因此,加载修改后的Hello类就会失败(引发LinkageError)。例如,如果Test中的main()如下所示:

public static void main(String[] args) throws Exception {Hello orig = new Hello();ClassPool cp = ClassPool.getDefault();CtClass cc = cp.get("Hello");:
}

以上实例在main的第一行加载原始Hello类(调用了Hello类的构造方法去使类加载器加载Hello类),之后再调用toClass方法将会引发异常,因为类加载器不能同时加载Hello类的两个不同版本。

如果程序在JBoss和Tomcat等应用服务器上运行,则toClass方法使用的上下文类加载器可能不合适。在这种情况,应用程序会抛出ClassCastException异常。为避免这种异常抛出,你必须对toClass方法明确地指定一个合适的类加载器。例如,如果bean是你的会话中的bean对象,代码如下:

CtClass cc = ...;
Class c = cc.toClass(bean.getClass().getClassLoader());

想要程序正常运行的话,你应该给toClass方法一个类加载器去加载你的程序(在上面的例子中,为bean对象类的类加载器)

toClass方法是简便的,如果你需要更多复杂度功能,你应该编写你自己的类加载器。

3.2 Java 中的类加载

在Java中,多个类装入器可以共存,每个类加载器都会创建自己的命名空间。不同的类加载器可以加载具有相同类名的不同类文件。被加载的两个类被视为不同的类。这个特性使我们能够在一个JVM上运行多个应用程序,即使这些程序包含具有相同类名的不同的类。

注意:JVM不允许动态地重加载类。一旦类加载器加载了一个类,它就不能在运行时重新加载该类的修改后版本。因此,您不能在JVM加载类后更改它的定义。然而,JPDA(Java平台调试器架构)提供了有限的重新加载类的能力。参见第3.6节。

如果同一个类文件由两个不同的类加载器加载,JVM将生成两个具有相同名称和定义的不同类。这两个类被视为不同的类。由于这两个类不相同,因此一个类的实例不能作为变量分配给另一个类。两个类之间的强制转换将会失败,并会抛出ClassCastException异常。

例如,以下代码段将引发异常:

MyClassLoader myLoader = new MyClassLoader();
Class clazz = myLoader.loadClass("Box");
Object obj = clazz.newInstance();
Box b = (Box)obj;    // this always throws ClassCastException.

Box类由两个类加载器加载。假设类加载器CL加载包含此代码段的类。由于此代码段引用了MyClassLoader、Class、Object和Box,所以CL也会加载这些类(除非它委托给另一个类加载器)。因此,变量b的类型是CL加载的Box类。另一方面,myLoader也加载Box类。对象obj是由myLoader加载的Box类的实例。因此,最后一条语句总是抛出ClassCastException,因为对象obj的类是Box类的不同版本,而不是用作变量b的类型。

多个类加载器形成一个树结构。除启动类加载器外,每个类加载器都有一个父类加载器。它通常加载子类加载器加载的类。由于加载类的请求可以沿着类加载器的这个树形结构结构进行委托,(参考双亲委派模型)因此类可能会由你没有请求的类加载器加载。因此,请求加载类C的类加载器可能与实际加载类C的类加载器不同。为了区别,我们将前一个加载器称为C的发起加载器,将后一个类加载器称为C的真正的类加载器。

此外,如果被请求加载类C的类加载器CL(C的发起加载器)委托给父类加载器PL加载类C,则类加载器CL永远不会被请求加载在类C中引用的任何类。CL不是这些类的发起加载器。相反,父类加载器PL成为它们的发起加载器,并被请求加载它们。类C的定义所引用的类由C的真正加载器加载。

为了理解这种行为,让我们考虑以下示例。

public class Point {    // loaded by PLprivate int x, y;public int getX() { return x; }:
}public class Box {      // the initiator is L but the real loader is PLprivate Point upperLeft, size;public int getBaseX() { return upperLeft.x; }:
}public class Window {    // loaded by a class loader Lprivate Box box;public int getBaseX() { return box.getBaseX(); }
}

假设一个类Window由类加载器L加载。Window的发起类加载器和真正的加载器都是L。由于Window引用了Box,JVM将请求L加载Box。这里,假设L将此任务委托给父类加载器PL。Box的发起类加载器是L,但真正的类加载器是PL。在这种情况下,Point的发起类加载器不是L,而是PL,因为它与Box的真正类加载器相同。因此,L从未被请求加载Point。

接下来,让我们考虑一个稍作修改的示例。

public class Point {private int x, y;public int getX() { return x; }:
}public class Box {      // the initiator is L but the real loader is PLprivate Point upperLeft, size;public Point getSize() { return size; }:
}public class Window {    // loaded by a class loader Lprivate Box box;public boolean widthIs(int w) {Point p = box.getSize();return w == p.getX();}
}

现在,Window的定义也指Point。在这种情况下,如果请求装入Point,类装入器L也必须委托给PL。必须避免让两个类加载器双重加载同一个类。两个加载器中的一个必须委派给另一个。

如果加载Point时L没有委托给PL,widthIs()将抛出ClassCastException。由于Box的实际加载程序是PL,因此Box中引用的Point也由PL加载。因此,getSize()的结果值是PL加载的Point的实例,而widthIs()中变量p的类型是L加载的Point。JVM将它们视为不同的类型,因此由于类型不匹配而引发异常。

这种行为有些不便,但很有必要。如果以下声明:

Point p = box.getSize();

如果没有抛出异常,那么Window的程序员可能会破坏Point对象的封装。例如,字段x在PL加载的Point中是私有的。但是,如果L使用以下定义加载Point,则Window类可以直接访问x的值:

public class Point {public int x, y;    // not privatepublic int getX() { return x; }:
}

有关Java中类加载器的更多详细信息,请参阅以下文章:

Sheng Liang and Gilad Bracha, “Dynamic Class Loading in the Java Virtual Machine”,
ACM OOPSLA’98, pp.36-44, 1998.

3.3 使用javassist.Loader

Javassist提供了一个类加载器Javassist.loader。这个类加载器使用javassist.ClassPool对象读取类文件。

例如,javassist.Loader可以用于加载被javassist修改的特定类。

import javassist.*;
import test.Rectangle;public class Main {public static void main(String[] args) throws Throwable {ClassPool pool = ClassPool.getDefault();Loader cl = new Loader(pool);CtClass ct = pool.get("test.Rectangle");ct.setSuperclass(pool.get("test.Point"));Class c = cl.loadClass("test.Rectangle");Object rect = c.newInstance();:}
}

此程序修改类test.Rectangle的父类test.Rectangle为test.Point类。然后,程序加载修改后的类,并创建test.Rectangle类的新实例。

如果用户希望在加载类时按需修改这个类,则可以将事件监听器添加到javassist.Loader上。当类加载器加载这个类时,会通知添加的事件监听器。事件监听器类必须实现以下接口:

public interface Translator {public void start(ClassPool pool)throws NotFoundException, CannotCompileException;public void onLoad(ClassPool pool, String classname)throws NotFoundException, CannotCompileException;
}

当通过javassist.Loader中的addTranslator方法将事件监听器添加到javassist.Loader对象时,start方法被调用。在javassist.Loader加载类之前onLoad方法被调用。onLoad方法可以修改已加载类的定义。

例如,下面的事件监听器在加载所有类之前将类修改为公有的。

public class MyTranslator implements Translator {void start(ClassPool pool)throws NotFoundException, CannotCompileException {}void onLoad(ClassPool pool, String classname)throws NotFoundException, CannotCompileException{CtClass cc = pool.get(classname);cc.setModifiers(Modifier.PUBLIC);}
}

请注意,onLoad方法不必调用toBytecode方法或writeFile方法。因为javassist.Loader调用这些方法来获取类文件。

要使用MyTranslator对象运行应用程序类MyApp,请编写一个主类,如下所示:

import javassist.*;public class Main2 {public static void main(String[] args) throws Throwable {Translator t = new MyTranslator();ClassPool pool = ClassPool.getDefault();Loader cl = new Loader();cl.addTranslator(pool, t);cl.run("MyApp", args);}
}

要运行此程序,请执行以下操作:

% java Main2 arg1 arg2...

类MyApp和其他应用程序类被MyTranslator转换。

请注意,像MyApp这样的应用程序类无法访问诸如Main2、MyTranslator和ClassPool之类的被加载的类,因为它们是由不同的加载器加载的。应用程序类由javassist.Loader加载。而像Main2这样的类是由Java默认的类加载器加载的。

javassist.Loader以与java.lang.ClassLoader搜索类的顺序不同。java.lang.ClassLoader首先将加载操作委托给父类加载器,然后仅当父类加载器找不到类时才尝试加载类。另一方面,javassist.Loader尝试在类加载请求被委托给父类加载器之前加载类。仅在以下情况下才会把类加载请求委托给父类:

  • 在ClassPool对象上调用get方法找不到类。
  • 或者类被指定使用delegateLoadingOf方法由父类加载器加载。

这个搜索顺序允许Javassist加载修改后的类。但是,如果由于某种原因Javassist找不到修改过的类,它将委托给父类加载器去加载类。一旦一个类被父类加载器加载,该类中引用的其他类也将被父类加载器加载,因此它们永远不会被修改。回想一下,类C中引用的所有类都是由C的真正加载器加载的。如果您的程序未能加载修改后的类,您应该确保使用该类的所有类是否都已由javassist.Loader加载。

3.4 编写类加载器

使用Javassist的简单类加载器如下:

import javassist.*;public class SampleLoader extends ClassLoader {/* Call MyApp.main().*/public static void main(String[] args) throws Throwable {SampleLoader s = new SampleLoader();Class c = s.loadClass("MyApp");c.getDeclaredMethod("main", new Class[] { String[].class }).invoke(null, new Object[] { args });}private ClassPool pool;public SampleLoader() throws NotFoundException {pool = new ClassPool();pool.insertClassPath("./class"); // MyApp.class must be there.}/* Finds a specified class.* The bytecode for that class can be modified.*/protected Class findClass(String name) throws ClassNotFoundException {try {CtClass cc = pool.get(name);// modify the CtClass object herebyte[] b = cc.toBytecode();return defineClass(name, b, 0, b.length);} catch (NotFoundException e) {throw new ClassNotFoundException();} catch (IOException e) {throw new ClassNotFoundException();} catch (CannotCompileException e) {throw new ClassNotFoundException();}}
}

类MyApp是一个应用程序。要执行此程序,首先将类文件放在./class目录下,该目录不得包含在类的搜索路径中。否则,MyApp类将由系统默认的类加载器(即SampleLoader的父类加载器)加载。目录名./class由构造函数中的insertClassPath方法指定。如果需要,可以选择其他名称而不是./class。然后执行以下操作:

% java SampleLoader

类加载器加载类MyApp(./class/MyApp.class),并使用命令行参数调用MyApp.main方法。

这是使用Javassist的最简单方法。然而,如果您编写一个更复杂的类加载器,您可能需要详细了解Java的类加载机制。例如,上面的程序将MyApp类放在一个与类SampleLoader所属的命名空间分开的命名空间中,因为这两个类由不同的类加载器加载。因此,MyApp类不能直接访问SampleLoader类。

3.5 修改系统类

像java.lang.String这样的系统类不能由系统类加载器以外的类加载器加载。因此,上面展示的SampleLoader或javassist.Loader加载器无法在加载时修改系统类。

如果应用程序需要这样做,则必须静态地修改系统类。例如,以下程序将一个新字段hiddenValue添加到java.lang.String类中:

ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("java.lang.String");
CtField f = new CtField(CtClass.intType, "hiddenValue", cc);
f.setModifiers(Modifier.PUBLIC);
cc.addField(f);
cc.writeFile(".");

该程序生成一个文件“./java/lang/String.class”。

要使用这个修改过的String类运行程序MyApp,请执行以下操作:

% java -Xbootclasspath/p:. MyApp arg1 arg2...

假设MyApp的定义如下:

public class MyApp {public static void main(String[] args) throws Exception {System.out.println(String.class.getField("hiddenValue").getName());}
}

如果正确加载了修改后的String类,MyApp将打印hiddenValue。

注意:不应部署使用此技术覆盖rt.jar中的系统类的应用程序,因为这样做会违反Java 2 运行时环境二进制代码许可证(Java 2 Runtime Environment binary code license)。

3.6 运行时重新加载类

如果JVM是在启用JPDA(Java平台调试器架构)的情况下启动的,则类是可以动态重加载的。JVM加载类后,可以卸载旧版本的类定义,并重新加载新版本的类。也就是说,该类的定义可以在运行时动态修改。然而,新的类定义必须与旧的类定义兼容。JVM不允许在两个版本之间有较大的改动。它们具有相同的方法和字段集合。

Javassist为在运行时重新加载类提供了一个方便的类。有关更多信息,请参阅javassist.tools.HotSwapper的API文档。

总结

本篇文章介绍了Javassist的CttoClass方法 、Java类加载、如何编写类加载器、修改系统类以及运行时重新加载类。

说明

相关内容

热门资讯

离开旧爱,想坐慢车,看透彻了心... 离开旧爱,想坐慢车,看透彻了心就会是晴朗的 是哪首歌?分手快乐,梁静茹的一首老歌分手快乐 梁静茹 我...
火影忍者决斗场什么忍者能卡进墙... 火影忍者决斗场什么忍者能卡进墙里波风水门。1、利用瞬身术进竖核入墙内:首先选信尘择一个合适的墙壁,然...
只有再爱一次,才能忘记前男友吗... 只有再爱一次,才能忘记前男友吗?你可以多和你的好朋友谈心啊,多结交一些朋友一样可以让你淡忘他,不一定...
女的穿越时空回到古代朝鲜的言情... 女的穿越时空回到古代朝鲜的言情小说古灵的《替身》
求一部动漫,很久以前看的,忘记... 求一部动漫,很久以前看的,忘记了名字。是关于一个少女变身用扑克牌行窃的故事。圣少女(提醒:变身后穿黑...
好听的歌 劲爆的 伤感的 忧伤... 好听的歌 劲爆的 伤感的 忧伤的 都行求要听的 欢快 忧伤的都行 只要好听 分享下呗Gee-少...
汤圆创作里面容易签约嘛?难度怎... 汤圆创作里面容易签约嘛?难度怎么样?是特别好看的文文才会成功嘛?什么地方,没听说过。
异界小说 男主被冰封了 然后有... 异界小说 男主被冰封了 然后有一帮来拍电影的女的在洗澡的时候发现了他! 好像是这样的就记得这些还有点...
选择词语填在横线上(快来帮帮我... 选择词语填在横线上(快来帮帮我吧)调整 整顿 矗立 伫立1、调整 整顿2、耸立 伫立调整 整顿矗立...
朴灿烈香水百瑞德,灿烈用的是百... 朴灿烈香水百瑞德,灿烈用的是百瑞德哪种香味的香水?朴灿烈喜欢用的百瑞德香水是银色山泉这款,这款香水是...
精神分析的学习与自我成长的联系... 精神分析的学习与自我成长的联系?心理学专业人士来答卡伦 霍尼《神经症与人的成长》,如果你是专业的,你...
怎么就没一首我喜欢听的歌 怎么就没一首我喜欢听的歌你最喜欢听的歌,听久了也会腻。心情不要那么浮躁,就会找到你喜欢的那首歌
如果把地球直线挖通的话,人跳进... 如果把地球直线挖通的话,人跳进去会摔死还是会掉到另一端呢?应该会直接摔死,因为地球中心才是非常复杂的...
和男朋友分手,他居然说谢谢我给... 和男朋友分手,他居然说谢谢我给他上了一堂人生课,什么意思?虽然你们分手了,但是你们一起度过了一段人生...
带口哨的纯音乐 带口哨的纯音乐曲名:The Voyage艺人:The Mountaineering Club Orc...
禾葡兰的禾善基金的理念是什么? 禾葡兰的禾善基金的理念是什么?禾善基金是有什么理念呢?禾善基金是禾葡兰创办的互助基金会,基金会的救助...
名侦探柯南给工藤新一的挑战书每... 名侦探柯南给工藤新一的挑战书每集开头那段音乐,有完整的吗,不是剧场和动画那个!那首歌是仓木麻衣的《T...
路漫漫其修远兮的下一句 路漫漫其修远兮的下一句路漫漫其修远兮,吾将上下而求索”这句出自屈原的名作《离骚》。“路漫漫其修远兮,...
海伦凯勒的背景 海伦凯勒的背景 海伦·凯勒(Helen Keller)(1880年6月27日-1968年6月1日...
激情燃烧的岁月实际上就是中国人... 激情燃烧的岁月实际上就是中国人焕发出巨大的劳动热情2.咱们工人有力量的歌曲,焕发的是现代人的激情。以...