程序员

Javassist之Classloader(二)

2018-06-19  本文已影响0人  bdqfork

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。

上一篇下一篇

猜你喜欢

热点阅读