美文网首页程序员
Javassist之Classloader(二)

Javassist之Classloader(二)

作者: bdqfork | 来源:发表于2018-06-19 20:44 被阅读0次

    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。

    相关文章

      网友评论

        本文标题:Javassist之Classloader(二)

        本文链接:https://www.haomeiwen.com/subject/cryzeftx.html