`
Weich_JavaDeveloper
  • 浏览: 100299 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

Java 编程的动态性,第 6 部分: 利用 Javassist 进行面向方面的更改

    博客分类:
  • JAVA
阅读更多
Java 顾问 Dennis Sosnoski 在他的关于 Javassist 框架的三期文章中将精华部分留在了最后。这次他展现了 Javassist 对搜索-替换的支持是如何使对 Java 字节码的编辑变得像文本编辑器的“替换所有(Replace All )”命令一样容易的。想报告所有写入特定字段的内容或者对方法调用中参数的更改中的补丁吗?Javassist 使这变得很容易,Dennis 向您展示了其做法。

本系列的 第 4 部分第 5 部分讨论了如何用 Javassist 对二进制类进行局部更改。这次您将学习以一种更强大的方式使用该框架,从而充分利用 Javassist 对在字节码中查找所有特定方法或者字段的支持。对于 Javassist 功能而言,这个功能至少与它以类似源代码的方式指定字节码的能力同样重要。对选择替换操作的支持也有助于使 Javasssist 成为一个在标准 Java 代码中增加面向方面的编程功能的绝好工具。

第 5 部分介绍了 Javassist 是如何让您拦截类加载过程的 ―― 甚至在二进制类表示正在被加载的时候对它们进行更改。这篇文章中讨论的系统字节码转换可以用于静态类文件转换,也可以用于运行时拦截,但是在运行时使用尤其有用。

处理字节码修改

Javassist 提供了两种不同的系统字节码修改的处理方法。第一种技术是使用 javassist.CodeConverter 类,使用起来要稍微简单一些,但是可以完成的任务有很多限制。第二种技术使用 javassist.ExprEditor 类的自定义子类,它稍微复杂一些,但是所增加的灵活性足以抵销所付出的努力。在本文中我将分析这两种方法的例子。

代码转换

系统字节码修改的第一种 Javassist 技术使用 javassist.CodeConverter 类。要利用这种技术,只需要创建 CodeConverter 类的一个实例并用一个或者多个转换操作配置它。每一个转换都是用识别转换类型的方法调用来配置的。转换类型可分为三类:方法调用转换、字段访问转换和新对象转换。

清单 1 给出了使用方法调用转换的一个例子。在这个例子中,转换只是增加了一个方法正在被调用的通知。在代码中,首先得到将要使用的 javassist.ClassPool 实例,将它配置为与一个翻译器一同工作 (正如在前面 第 5 部分 所看到的)。然后,通过 ClassPool 访问两个方法定义。第一个方法定义针对的是要监视的“set”类型的方法(类和方法名来自命令行参数),第二个方法定义针对的是 reportSet() 方法 ,它位于 TranslateConvert 类中,并会报告对第一个方法的调用。

有了方法信息后,就可以用 CodeConverter insertBeforeMethod() 配置一个转换,以在每次调用这个 set 方法之前增加一个对报告方法的调用。然后所要做的就是将这个转换器应用到一个或者多个类上。在清单 1 的代码中,我是通过调用类对象的 instrument() 方法,在 ConverterTranslator 内部类的 onWrite() 方法中完成这项工作的。这将自动对从 ClassPool 实例中加载的每一个类应用这个转换。


清单 1. 使用 CodeConverter

public class TranslateConvert
{
    public static void main(String[] args) {
        if (args.length >= 3) {
            try {
                
                // set up class loader with translator
                ConverterTranslator xlat =
                    new ConverterTranslator();
                ClassPool pool = ClassPool.getDefault(xlat);
                CodeConverter convert = new CodeConverter();
                CtMethod smeth = pool.get(args[0]).
                    getDeclaredMethod(args[1]);
                CtMethod pmeth = pool.get("TranslateConvert").
                    getDeclaredMethod("reportSet");
                convert.insertBeforeMethod(smeth, pmeth);
                xlat.setConverter(convert);
                Loader loader = new Loader(pool);
                
                // invoke "main" method of application class
                String[] pargs = new String[args.length-3];
                System.arraycopy(args, 3, pargs, 0, pargs.length);
                loader.run(args[2], pargs);
                
            } catch ...
            }
            
        } else {
            System.out.println("Usage: TranslateConvert " +
                "clas-name set-name main-class args...");
        }
    }
    
    public static void reportSet(Bean target, String value) {
        System.out.println("Call to set value " + value);
    }
    
    public static class ConverterTranslator implements Translator
    {
        private CodeConverter m_converter;
        
        private void setConverter(CodeConverter convert) {
            m_converter = convert;
        }
        
        public void start(ClassPool pool) {}
        
        public void onWrite(ClassPool pool, String cname)
            throws NotFoundException, CannotCompileException {
            CtClass clas = pool.get(cname);
            clas.instrument(m_converter);
        }
    }
}

配置转换是一个相当复杂的操作,但是设置好以后,在它工作时就不用费什么心了。清单 2 给出了代码示例,可以作为测试案例。这里 Bean 提供了具有类似 bean 的 get 和 set 方法的测试对象, BeanTest 程序用这些方法来访问值。


清单 2. 一个 bean 测试程序

public class Bean
{
    private String m_a;
    private String m_b;
    
    public Bean() {}
    
    public Bean(String a, String b) {
        m_a = a;
        m_b = b;
    }
    
    public String getA() {
        return m_a;
    }
    public String getB() {
        return m_b;
    }
    public void setA(String string) {
        m_a = string;
    }
    public void setB(String string) {
        m_b = string;
    }
}
public class BeanTest
{
    private Bean m_bean;
    
    private BeanTest() {
        m_bean = new Bean("originalA", "originalB");
    }
    
    private void print() {
        System.out.println("Bean values are " +
            m_bean.getA() + " and " + m_bean.getB());
    }
    
    private void changeValues(String lead) {
        m_bean.setA(lead + "A");
        m_bean.setB(lead + "B");
    }
    
    public static void main(String[] args) {
        BeanTest inst = new BeanTest();
        inst.print();
        inst.changeValues("new");
        inst.print();
    }
}

如果直接运行清单 2 中的 中的 BeanTest 程序,则输出如下:

 

[dennis]$ java -cp . BeanTest
Bean values are originalA and originalB
Bean values are newA and newB

如果用 清单 1 中的 TranslateConvert 程序运行它并指定监视其中的一个 set 方法,那么输出将如下所示:

 

[dennis]$ java -cp .:javassist.jar TranslateConvert Bean setA BeanTest
Bean values are originalA and originalB
Call to set value newA
Bean values are newA and newB

每项工作都与以前一样,但是现在在执行这个程序时,所选的方法被调用时会有一个通知。

在这个例子中,可以用其他的方法容易地实现同样的效果,例如通过使用 第 4 部分 中的技术在实际的 set 方法体中增加代码。这里的区别是,在使用位置增加代码让我有了灵活性。例如,可以容易地修改 TranslateConvert.ConverterTranslator onWrite() 方法来检查正在加载的类名,并只转换在我想要监视的类的清单中列出的类。直接在 set 方法体中添加代码无法进行这种有选择的监视。

系统字节码转换由于提供了灵活性而使其成为为标准 Java 代码实现面向方面的扩展的强大工具。在本文后面您会看到更多这方面的内容。

转换限制

CodeConverter 处理的转换很有用,但是有局限性。例如,如果希望在调用目标方法之前或者之后调用一个监视方法,那么这个监视方法必须定义为 static void 并且必须先接受一个目标方法的类的参数,然后是与目标方法所要求的同样数量和类型的参数。

这种严格的结构意味着监视方法需要与目标类和方法完全匹配。举一个例子,假设我改变了 清单 1reportSet() 方法的定义,让它接受一个一般性的 java.lang.Object 参数,想使它可以用于不同的目标类:

 

    public static void reportSet(Object target, String value) {
        System.out.println("Call to set value " + value);
    }

编译没有问题,但是当我运行它时它就会中断:

 

[dennis]$ java -cp .:javassist.jar TranslateConvert Bean setA BeanTest
Bean values are A and B
java.lang.NoSuchMethodError: TranslateConvert.reportSet(LBean;Ljava/lang/String;)V
        at BeanTest.changeValues(BeanTest.java:17)
        at BeanTest.main(BeanTest.java:23)
        at ...

有办法绕过这种限制。一种解决方案是在运行时实际生成与目标方法相匹配的自定义监视方法。不过这要做很多工作,在本文中我不打算试验这种方法。幸运的是,Javassist 还提供了另一种处理系统字节码转换的方法。这种方法使用 javassist.ExprEditor ,与 CodeConverter 相比,它更灵活、也更强大。

容易的类剖析

CodeConverter 进行字节码转换与用 javassist.ExprEditor 的原理一样。不过, ExprEditor 方式也许更难理解一些,所以我首先展示基本原理,然后再加入实际的转换。

清单 3 显示了如何用 ExprEditor 来报告面向方面的转换的可能目标的基本项目。这里我在自己的 VerboseEditor 中派生了 ExprEditor 子类,重写了三个基本的类方法 ―― 它们的名字都是 edit() ,但是有不同的参数类型。如 清单 1 中的代码,我实际上是在 DissectionTranslator 内部类的 onWrite() 方法中使用这个子类,对从 ClassPool 实例中加载的每一个类,在对类对象的 instrument() 方法的调用中传递一个实例。


清单 3. 一个类剖析程序

public class Dissect
{
    public static void main(String[] args) {
        if (args.length >= 1) {
            try {
                
                // set up class loader with translator
                Translator xlat = new DissectionTranslator();
                ClassPool pool = ClassPool.getDefault(xlat);
                Loader loader = new Loader(pool);
                    
                // invoke the "main" method of the application class
                String[] pargs = new String[args.length-1];
                System.arraycopy(args, 1, pargs, 0, pargs.length);
                loader.run(args[0], pargs);
                
            } catch (Throwable ex) {
                ex.printStackTrace();
            }
            
        } else {
            System.out.println
                ("Usage: Dissect main-class args...");
        }
    }
    
    public static class DissectionTranslator implements Translator
    {
        public void start(ClassPool pool) {}
        
        public void onWrite(ClassPool pool, String cname)
            throws NotFoundException, CannotCompileException {
            System.out.println("Dissecting class " + cname);
            CtClass clas = pool.get(cname);
            clas.instrument(new VerboseEditor());
        }
    }
    
    public static class VerboseEditor extends ExprEditor
    {
        private String from(Expr expr) {
            CtBehavior source = expr.where();
            return " in " + source.getName() + "(" + expr.getFileName() + ":" +
                expr.getLineNumber() + ")";
        }
        public void edit(FieldAccess arg) {
            String dir = arg.isReader() ? "read" : "write";
            System.out.println(" " + dir + " of " + arg.getClassName() +
                "." + arg.getFieldName() + from(arg));
        }
        public void edit(MethodCall arg) {
            System.out.println(" call to " + arg.getClassName() + "." +
                arg.getMethodName() + from(arg));
        }
        public void edit(NewExpr arg) {
            System.out.println(" new " + arg.getClassName() + from(arg));
        }
    }
}

清单 4 显示了对 清单 2 中的 BeanTest 程序运行清单 3 中的 Dissect 程序所产生的输出。它给出了加载的每一个类的每一个方法中所做的工作的详细分析,列出了所有方法调用、字段访问和新对象创建。


清单 4. 已剖析的 BeanTest

[dennis]$ java -cp .:javassist.jar Dissect BeanTest
Dissecting class BeanTest
 new Bean in BeanTest(BeanTest.java:7)
 write of BeanTest.m_bean in BeanTest(BeanTest.java:7)
 read of java.lang.System.out in print(BeanTest.java:11)
 new java.lang.StringBuffer in print(BeanTest.java:11)
 call to java.lang.StringBuffer.append in print(BeanTest.java:11)
 read of BeanTest.m_bean in print(BeanTest.java:11)
 call to Bean.getA in print(BeanTest.java:11)
 call to java.lang.StringBuffer.append in print(BeanTest.java:11)
 call to java.lang.StringBuffer.append in print(BeanTest.java:11)
 read of BeanTest.m_bean in print(BeanTest.java:11)
 call to Bean.getB in print(BeanTest.java:11)
 call to java.lang.StringBuffer.append in print(BeanTest.java:11)
 call to java.lang.StringBuffer.toString in print(BeanTest.java:11)
 call to java.io.PrintStream.println in print(BeanTest.java:11)
 read of BeanTest.m_bean in changeValues(BeanTest.java:16)
 new java.lang.StringBuffer in changeValues(BeanTest.java:16)
 call to java.lang.StringBuffer.append in changeValues(BeanTest.java:16)
 call to java.lang.StringBuffer.append in changeValues(BeanTest.java:16)
 call to java.lang.StringBuffer.toString in changeValues(BeanTest.java:16)
 call to Bean.setA in changeValues(BeanTest.java:16)
 read of BeanTest.m_bean in changeValues(BeanTest.java:17)
 new java.lang.StringBuffer in changeValues(BeanTest.java:17)
 call to java.lang.StringBuffer.append in changeValues(BeanTest.java:17)
 call to java.lang.StringBuffer.append in changeValues(BeanTest.java:17)
 call to java.lang.StringBuffer.toString in changeValues(BeanTest.java:17)
 call to Bean.setB in changeValues(BeanTest.java:17)
 new BeanTest in main(BeanTest.java:21)
 call to BeanTest.print in main(BeanTest.java:22)
 call to BeanTest.changeValues in main(BeanTest.java:23)
 call to BeanTest.print in main(BeanTest.java:24)
Dissecting class Bean
 write of Bean.m_a in Bean(Bean.java:10)
 write of Bean.m_b in Bean(Bean.java:11)
 read of Bean.m_a in getA(Bean.java:15)
 read of Bean.m_b in getB(Bean.java:19)
 write of Bean.m_a in setA(Bean.java:23)
 write of Bean.m_b in setB(Bean.java:27)
Bean values are originalA and originalB
Bean values are newA and newB

通过在 VerboseEditor 中实现适当的方法,可以容易地增加对报告强制类型转换、 instanceof 检查和 catch 块的支持。但是只列出有关这些组件项的信息有些乏味,所以让我们来实际修改项目吧。

进行剖析

清单 4对类的剖析列出了基本组件操作。容易看出在实现面向方面的功能时使用这些操作会多么有用。例如,报告对所选字段的所有写访问的记录器(logger)在许多应用程序中都会发挥作用。无论如何,我已经承诺要为您介绍如何完成 这类工作。

幸运的是,就本文讨论的主题来说, ExprEditor 不但让我知道代码中有什么操作,它还让我可以修改所报告的操作。在不同的 ExprEditor.edit() 方法调用中传递的参数类型分别定义一种 replace() 方法。如果向这个方法传递一个普通 Javassist 源代码格式的语句(在 第 4 部分中介绍),那么这个语句将编译为字节码,并且用来替换原来的操作。这使对字节码的切片和切块变得容易。

清单 5 显示了一个代码替换的应用程序。在这里我不是记录操作,而是选择实际修改存储在所选字段中的 String 值。在 FieldSetEditor 中,我实现了匹配字段访问的方法签名。在这个方法中,我只检查两样东西:字段名是否是我所查找的,操作是否是一个存储过程。找到匹配后,就用使用实际的 TranslateEditor 应用程序类中 reverse() 方法调用的结果来替换原来的存储。 reverse() 方法就是将原来字符串中的字母顺序颠倒并输出一条消息表明它已经使用过了。


清单 5. 颠倒字符串集

public class TranslateEditor
{
    public static void main(String[] args) {
        if (args.length >= 3) {
            try {
                
                // set up class loader with translator
                EditorTranslator xlat =
                    new EditorTranslator(args[0], new FieldSetEditor(args[1]));
                ClassPool pool = ClassPool.getDefault(xlat);
                Loader loader = new Loader(pool);
                
                // invoke the "main" method of the application class
                String[] pargs = new String[args.length-3];
                System.arraycopy(args, 3, pargs, 0, pargs.length);
                loader.run(args[2], pargs);
                
            } catch (Throwable ex) {
                ex.printStackTrace();
            }
            
        } else {
            System.out.println("Usage: TranslateEditor clas-name " +
              "field-name main-class args...");
        }
    }
    
    public static String reverse(String value) {
        int length = value.length();
        StringBuffer buff = new StringBuffer(length);
        for (int i = length-1; i >= 0; i--) {
            buff.append(value.charAt(i));
        }
        System.out.println("TranslateEditor.reverse returning " + buff);
        return buff.toString();
    }
    
    public static class EditorTranslator implements Translator
    {
        private String m_className;
        private ExprEditor m_editor;
        
        private EditorTranslator(String cname, ExprEditor editor) {
            m_className = cname;
            m_editor = editor;
        }
        
        public void start(ClassPool pool) {}
        
        public void onWrite(ClassPool pool, String cname)
            throws NotFoundException, CannotCompileException {
            if (cname.equals(m_className)) {
                CtClass clas = pool.get(cname);
                clas.instrument(m_editor);
            }
        }
    }
    
    public static class FieldSetEditor extends ExprEditor
    {
        private String m_fieldName;
        
        private FieldSetEditor(String fname) {
            m_fieldName = fname;
        }
        
        public void edit(FieldAccess arg) throws CannotCompileException {
            if (arg.getFieldName().equals(m_fieldName) && arg.isWriter()) {
                StringBuffer code = new StringBuffer();
                code.append("$0.");
                code.append(arg.getFieldName());
                code.append("=TranslateEditor.reverse($1);");
                arg.replace(code.toString());
            }
        }
    }
}

如果对 清单 2 中的 BeanTest 程序运行清单 5 中的 TranslateEditor 程序,结果如下:

 

[dennis]$ java -cp .:javassist.jar TranslateEditor Bean m_a BeanTest
TranslateEditor.reverse returning Alanigiro
Bean values are Alanigiro and originalB
TranslateEditor.reverse returning Awen
Bean values are Awen and newB

我成功地在每一次存储到 Bean.m_a 字段时,加入了一个对添加的代码的调用(一次是在构造函数中,一次是在 set 方法中)。我可以通过对从字段的加载实现类似的修改而得到反向的效果,不过我个人认为颠倒值比开始使用的值有意思得多,所以我选择使用它们。

包装 Javassist

本文介绍了用 Javassist 可以容易地完成系统字节码转换。将本文与上两期文章结合在一起,您应该有了在 Java 应用程序中实现自己面向方面的转换的坚实基础,这个转换过程可以作为单独的编译步骤,也可以在运行时完成。

要想对这种方法的强大之处有更好的了解,还可以分析用 Javassis 建立的 JBoss Aspect Oriented Programming Project (JBossAOP)。JBossAOP 使用一个 XML 配置文件来定义在应用程序类中完成的所有不同的操作。其中包括对字段访问或者方法调用使用拦截器,在现有类中添加 mix-in 接口实现等。JBossAOP 将被加入正在开发的 JBoss 应用程序服务器版本中,但是也可以在 JBoss 以外作为单独的工具提供给应用程序使用。

本系列的下一步将介绍 Byte Code Engineering Library (BCEL),这是 Apache Software Foundation 的 Jakarta 项目的一部分。BCEL 是 Java classworking 最广泛使用的一种框架。它使用与我们在最近这三篇文章中看到的 Javassist 方法的不同方法处理字节码,注重个别的字节码指令而不是 Javassist 所强调的源代码级别的工作。下个月将分析在字节码汇编器(assembler)级别工作的全部细节。

分享到:
评论

相关推荐

    agent+javassist例子

    `Java Agent`允许我们对Java应用程序进行预处理,比如字节码注入,而`javassist`库则提供了一个方便的方式来动态地操作和修改Java类的字节码。 `Java Agent`是Java平台提供的一种机制,允许开发者在程序运行前或...

    javaagent+javassist

    总的来说,javaagent和javassist的结合使用为Java开发者提供了强大的代码操作能力,允许我们在运行时对应用程序进行灵活的扩展和修改,极大地提升了开发的灵活性和效率。在实际项目中,如Spring AOP、AspectJ等框架...

    动态代理-jdk、cglib、javassist.zip

    动态代理在Java编程中是一种非常重要的技术,它允许我们在运行时创建对象的代理,从而可以在不修改原有代码的情况下,为对象添加额外的功能。本压缩包包含关于三种主要的动态代理实现方式:JDK动态代理、CGLIB以及...

    javassist,Java字节码工程工具包.zip

    Javassist在Java应用开发中扮演着重要的角色,尤其是在动态代理、AOP(面向切面编程)以及代码生成等场景下。 Javassist允许程序员在运行时动态修改类或创建新的类,而无需了解复杂的Java字节码指令集。通过提供一...

    javassist-3.18.1-GA.jar

    在Java世界里,这种技术通常被称为字节码工程,对于实现如AOP(面向切面编程)、动态代理和运行时代码优化等高级功能非常有用。 `javassist-3.18.1-GA.jar`是Javaassist的一个特定版本,GA代表“General ...

    javassistDemo

    Javaassist是一个开源库,它允许在运行时修改Java类和创建新的类。这个库在Java世界里被广泛用于动态代理、AOP(面向切面编程)以及类的...理解这些知识点对于深入学习Java的动态性、网络调试和AOP编程具有重要意义。

    Javassist18版20版22版的jar包

    Javaassist是一个开源库,主要用在Java平台上,用于在运行时动态修改类和类加载器。这个库在Java世界中扮演着重要的角色,因为它允许开发者在程序运行时对字节码进行操作,提供了对Java类的修改、创建以及分析的能力...

    Java动态代理机制详解(JDK 和CGLIB,Javassist,ASM)

    在Java中,我们可以使用JDK自带的动态代理或者第三方库如CGLIB、Javassist、ASM来实现。 **JDK动态代理**: JDK的动态代理主要依赖于`java.lang.reflect.Proxy`和`java.lang.reflect.InvocationHandler`两个类。...

    javassist试图简化Java字节码的编辑

    Java字节码编辑是Java开发中的一个高级主题,它允许开发者在运行时修改或增强类的行为。`javassist`库正是这样一个工具,它为Java...学习并熟练掌握`javassist`,将极大地提升你在Java动态编程和字节码操作方面的技能。

    javassistDemo.zip

    在Java应用程序中,这种能力非常有用,特别是在进行AOP(面向切面编程)或者在无法重新编译源代码的情况下需要修改类的行为时。 Javaassist库的核心功能包括创建新的类、接口,以及对现有类进行修改。它可以读取....

    jboss-javassist和JByteMode整合包

    通过Javassist,我们可以方便地生成和修改类的字节码,从而实现AOP(面向切面编程)等高级功能。它的主要特性包括: 1. 提供了类似于C/C++的API,使得字节码操作更加直观。 2. 支持处理类、接口、方法、字段等元数据...

    Java编程思想源码关联jar包

    Java编程思想是深入理解并掌握Java这门编程语言的关键,其中源码的分析与学习尤为重要。这个压缩包包含了几个在Java编程中常见的关联库,这些库对于理解和实践Java编程思想有着重要作用。 首先,我们来看看`...

    运用javassist和annotation修改class的特定method的class byte code

    Javaassist是一个开源库,它允许我们在运行时动态地修改或创建Java类。这个强大的工具广泛应用于框架、代理生成以及AOP(面向切面编程)等领域。在本文中,我们将深入探讨如何结合Javaassist和注解(Annotation)来...

    提高Java程序动态性的一个新途径.zip

    Java程序的动态性是软件开发中的一个重要概念,它关乎程序在运行时的灵活性、可扩展性和适应性。这篇资料“提高Java程序动态性的一个新途径”可能探讨了如何通过各种技术手段来增强Java应用程序的动态特性。下面我们...

    Thinking in Java Jar.rar_Thing In Java_fruity88_in_javassist-3.9

    《Thinking in Java》是Bruce Eckel的经典Java编程书籍,它深入浅出地讲解了Java语言的核心概念和技术。这本书强调了“思考”在编程中的重要性,不仅提供了丰富的代码示例,还鼓励读者通过实践来理解Java的精髓。...

    javassist-3.11

    6. **性能优化**:尽管Javassist提供了强大的动态字节码修改能力,但在某些情况下,如大量重复的字节码操作,其性能可能不如直接使用Java反射API。因此,理解何时使用Javassist是关键,以确保最佳性能。 7. **示例...

    javassist.jar源码

    这个库在Java编程中尤其有用,因为它允许程序员在不重新编译源代码的情况下修改、添加或删除类和方法。`javassist-3.7.ga.jar`是Javaassist的一个版本,ga代表“General Availability”,意味着这是一个稳定版本,...

    javassist-3.18.0-ga

    6. **兼容性**:`javassist-3.18.0-ga`版本支持Java 5及以上版本,这意味着它可以广泛应用于各种Java项目,包括那些基于旧版JDK的项目。 7. **应用领域**:Javaassist常用于动态代理框架(如Spring AOP)、代码生成...

    javassist-3.15.0-GA

    Javaassist是一个开源库,主要用在Java应用程序中动态修改类和方法的行为。它提供了一种在运行时分析、改变和增强类的能力,而无需重新编译。这个版本"javassist-3.15.0-GA"是Javaassist的一个特定发行版,用于支持...

Global site tag (gtag.js) - Google Analytics