• 【Javassist官方文档翻译】第三章类加载器


    系列文章目录

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



    前言

    在上一章我们介绍了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();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    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");
            :
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

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

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

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

    想要程序正常运行的话,你应该给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.
    
    • 1
    • 2
    • 3
    • 4

    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 PL
        private int x, y;
        public int getX() { return x; }
            :
    }
    
    public class Box {      // the initiator is L but the real loader is PL
        private Point upperLeft, size;
        public int getBaseX() { return upperLeft.x; }
            :
    }
    
    public class Window {    // loaded by a class loader L
        private Box box;
        public int getBaseX() { return box.getBaseX(); }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    假设一个类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 PL
        private Point upperLeft, size;
        public Point getSize() { return size; }
            :
    }
    
    public class Window {    // loaded by a class loader L
        private Box box;
        public boolean widthIs(int w) {
            Point p = box.getSize();
            return w == p.getX();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    现在,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();
    
    • 1

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

    public class Point {
        public int x, y;    // not private
        public int getX() { return x; }
            :
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    有关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();
             :
      }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    此程序修改类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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    当通过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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    请注意,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);
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

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

    % java Main2 arg1 arg2...
    
    • 1

    类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 here
                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();
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

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

    % java SampleLoader
    
    • 1

    类加载器加载类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(".");
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

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

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

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

    假设MyApp的定义如下:

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

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

    说明

  • 相关阅读:
    vue3 回顾
    抓包day1
    38_final关键字
    基于单片机设计的水平仪(STC589C52+MPU6050)
    Python语言学习:Python语言学习之python包/库package的简介、管理外部package的Python工具案例应用之详细攻略
    Qt怎么把背景转换为黑色?
    【新星计划·2023】TCP协议与UDP协议讲解
    【5G PHY】物理层逻辑和物理天线的映射
    CSS - 定位布局(position)
    DirectX12学习笔记-创建窗口
  • 原文地址:https://blog.csdn.net/qq_39072607/article/details/124970743