【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类加载、如何编写类加载器、修改系统类以及运行时重新加载类。

说明

相关内容

热门资讯

求经典台词和经典旁白 求经典台词和经典旁白谁有霹雳布袋戏里的经典对白和经典旁白啊?朋友,你尝过失去的滋味吗? 很多人在即将...
小王子第二章主要内容概括 小王子第二章主要内容概括小王子第二章主要内容概括小王子第二章主要内容概括
爱情睡醒了第15集里刘小贝和项... 爱情睡醒了第15集里刘小贝和项天骐跳舞时唱的那首歌是什么谢谢开始找舞伴的时候是林俊杰的《背对背拥抱》...
世界是什么?世界是什么概念?可... 世界是什么?世界是什么概念?可以干什么?物质的和意识的 除了我们生活的地方 比方说山 河 公路 ...
全职猎人中小杰和奇牙拿一集被抓 全职猎人中小杰和奇牙拿一集被抓动画片是第五十九集,五十八集被发现,五十九被带回基地,六十逃走
“不周山”意思是什么 “不周山”意思是什么快快快快......一座山,神话里被共工撞倒了。
《揭秘》一元一分15张跑得快群... 一元一分麻将群加群主微【ab120590】【tj525555】 【mj120590】等风也等你。喜欢...
玩家必看手机正规红中麻将群@2... 好运连连,全网推荐:(ab120590)(mj120590)【tj525555】-Q号:(QQ443...
始作俑者15张跑的快群@24小... 微信一元麻将群群主微【ab120590】 【tj525555】【mj120590】一元一分群内结算,...
《重大通知》24小时一元红中麻... 加V【ab120590】【tj525555】【mj120590】红中癞子、跑得快,等等,加不上微信就...
盘点一下正规一块红中麻将群@2... 一元一分麻将群加群主微:微【ab120590】 【mj120590】【tj525555】喜欢手机上打...
(免押金)上下分一元一分麻将群... 微【ab120590】 【mj120590】【tj525555】专业麻将群三年房费全网最低,APP苹...
[解读]正规红中麻将跑的快@群... 微信一元麻将群群主微【ab120590】 【tj525555】【mj120590】一元一分群内结算,...
《普及一下》全天24小时红中... 微【ab120590】 【mj120590】【tj525555】专业麻将群三年房费全网最低,APP苹...
优酷视频一元一分正规红中麻将... 好运连连,全网推荐:(ab120590)(mj120590)【tj525555】-Q号:(QQ443...
《火爆》加入附近红中麻将群@(... 群主微【ab120590】 【mj120590】【tj525555】免带押进群,群内跑包包赔支持验证...
《字节跳动》哪里有一元一分红中... 1.进群方式-[ab120590]或者《mj120590》【tj525555】--QQ(QQ4434...
全网普及红中癞子麻将群@202... 好运连连,全网推荐:(ab120590)(mj120590)【tj525555】-Q号:(QQ443...
「独家解读」一元一分麻将群哪里... 1.进群方式《ab120590》或者《mj120590》《tj525555》--QQ(4434063...
通知24小时不熄火跑的快群@2... 1.进群方式《ab120590》或者《mj120590》《tj525555》--QQ(4434063...