Javassist之Classloader(二)

Javassist之Classloader(一)中我们讲述了Javassist的toClass()以及Java的类加载器,本次我们将介绍Javassist的加载器,以及自定义加载器。

1. 使用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()会被调用。方法onLoad()在javassist.Loader加载类之前调用。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按不同的顺序扫描类。ClassLoader首先委托加载行为给父加载器,它只会加载父加载器无法加载的类。另一方面,javassist.Loader试图在委托给父加载器之前加载类。它只有在下面的情况才会进行委托:

  1. 调用ClassPool对象的get()并不能发现的类
  2. 使用delegateLoadingOf()被指定由父加载器加载的类

这个扫描顺序允许通过Javassist加载修改类。然而,它因为某些原因无法加载修改类时,它会委托给父类加载。一旦一个类被父类加载,其它的被该类引用的类也会被父加载器加载,因此它们从不被修改。所有在类C中所引用的类都被类C的实际加载器加载。如果你的程序加载修改类失败,你应该确认是否所有类被修改类引用的类都已经被javassist.Loader加载。

2. 写一个类加载

一个简单的使用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"); // <em>MyApp.class must be there.</em> } /* 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); // <em>modify the CtClass object here</em> byte[] 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.class会被默认系统类加载器加载,它是SampleLoader的父类。目录名./class是在构造方法里的insertClassPath()指定。如果需要,你可以选择一个和./class不同的目录。然后做如下操作:

% java SampleLoader

类加载器会加载MyApp(./class/MyApp.class)同时使用命令行参数调用MyApp.main()。

这是使用Javassist最简单的方式。然而,如果你写一个更复杂的类加载器,你可能需要更详细的关于Java类加载器机制的知识。例如,上面的程序将把MyApp类放在与SampleLoader不同的命名空间中,因为两个类被不同的类加载器加载。因此,MyApp类无法直接访问类SampleLoader。

3. 修改系统类

如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 Runtime Environment二进制代码许可证。

4. 运行时重新加载类

如果JVM使用JPDA(Java Platform Debugger Architecture)开启的模式运行,一个类可以被动态重新加载。在JVM加载类之后,旧版本的类定义可以被卸载,新的可以被再次加载。这意味着,类的定义可以在运行器被动态加载。然而,新的类定义必须与旧的兼容。JVM不允许两个版本之间的不兼容。它们需要拥有相同的方法和属性。

Javassist提供了一个方便的类用于在运行期间重新加载类。更多的信息,可以查看javassist.tools.HotSwapper文档的API。

坚持原创技术分享,您的支持将鼓励我继续创作!
  • 本文作者:bdqfork
  • 本文链接:/articles/21
  • 版权声明:本博客所有文章除特别声明外,均采用BY-NC-SA 许可协议。转载请注明出处!
表情 |预览
快来做第一个评论的人吧~