详解Java字节码编程之非常好用的javassist

2025-05-29 0 83

一、Javassist入门

(一)Javassist是什么

Javassist是可以动态编辑Java字节码的类库。它可以在Java程序运行时定义一个新的类,并加载到JVM中;还可以在JVM加载时修改一个类文件。Javassist使用户不必关心字节码相关的规范也是可以编辑类文件的。

(二)Javassist核心API

Javassist中每个需要编辑的class都对应一个CtCLass实例,CtClass的含义是编译时的类(compile time class),这些类会存储在Class Pool中(Class poll是一个存储CtClass对象的容器)。
CtClass中的CtField和CtMethod分别对应Java中的字段和方法。通过CtClass对象即可对类新增字段和修改方法等操作了。

详解Java字节码编程之非常好用的javassist

(三)简单示例

为了减少演示的复杂度,示例以及之后的操作,都在Maven项目下进行,因为我们可以直接引入依赖就可以达到我们导包的目的,很方便,不用再去下载jar包,然后自己手动导入了。

1、创建一个maven项目

如果你使用的是IDEA,可以像我一样;如果是其他工具,可以自行百度,或者按照自己的经验来创建即可。

详解Java字节码编程之非常好用的javassist

2、创建一个测试类,代码如下:

  1. package com.ssdmbbl.javassist;
  2. import javassist.*;
  3. import java.io.IOException;
  4. /**
  5. * @author zhenghui
  6. * @description: Javassist使用演示测试
  7. * @date 2021/4/6 6:38 下午
  8. */
  9. public class JavassistTest {
  10. public static void main(String[] args) throws CannotCompileException, IOException {
  11. ClassPool cp = ClassPool.getDefault();
  12. CtClass ctClass = cp.makeClass("com.ssdmbbl.javassist.Hello");
  13. ctClass.writeFile("./");
  14. }
  15. }

当运行这个代码的时候,可以看到已经在项目的根目录下创建了一个“com.ssdmbbl.javassist”包,在这个包下创建了“Hello.java”的java文件。

详解Java字节码编程之非常好用的javassist

内容如下:

  1. package com.ssdmbbl.javassist;
  2. public class Hello {
  3. public Hello() {
  4. }
  5. }

二、Javassist操作字节码示例

回想一下,咱们如果对一个Java正常操作的话,大概存在哪些操作呢?

  • 1、咱们会对一个类添加字段;
  • 2、咱们会对一个类添加方法;

好像没其他的了吧。其余的就是在方法里写代码了呗。

(一)新增一个方法

咱们继续套用上面简单示例的代码,在此基础之上进行新增一个方法。

新增方法的名字为"hello1",传递两个参数分别为int和double类型,并且没有返回值。

  1. package com.ssdmbbl.javassist;
  2. import javassist.*;
  3. import java.io.IOException;
  4. import java.net.URL;
  5. /**
  6. * @author zhenghui
  7. * @description: Javassist使用演示测试
  8. * @date 2021/4/6 6:38 下午
  9. */
  10. public class JavassistTest2 {
  11. public static void main(String[] args) throws CannotCompileException, IOException {
  12. //找到本文件的路径,与之保存在一起
  13. URL resource = JavassistTest2.class.getClassLoader().getResource("");
  14. String file = resource.getFile();
  15. System.out.println("文件存储路径:"+file);
  16. ClassPool cp = ClassPool.getDefault();
  17. CtClass ctClass = cp.makeClass("com.ssdmbbl.javassist.Hello");
  18. //创建一个类名为"hello",传递参数的顺序为(int,double),没有返回值的类
  19. /*
  20. CtMethod(…)源代码:
  21. public CtMethod(CtClass returnType,//这个方法的返回值类型,
  22. String mname, //(method name)方法的名字是什么
  23. CtClass[] parameters, //方法传入的参数类型是什么
  24. CtClass declaring //添加到哪个类中
  25. ) {….}
  26. */
  27. CtMethod ctMethod = new CtMethod(CtClass.voidType, "hello", new CtClass[]{CtClass.intType, CtClass.doubleType}, ctClass);
  28. //设置hello方法的权限为public
  29. ctMethod.setModifiers(Modifier.PUBLIC);
  30. //向ctClass中添加这个方法
  31. ctClass.addMethod(ctMethod);
  32. //写入本地
  33. ctClass.writeFile(file);
  34. }
  35. }

执行后,就可以查看生成的代码了:

可以看到,我们并没有指定参数的名字,也会给生成var1、var2依次类推的名字。

var1和var2其实class变量表中存放的名字。

  1. package com.ssdmbbl.javassist;
  2. public class Hello {
  3. public void hello1(int var1, double var2) {
  4. }
  5. public Hello() {
  6. }
  7. }

可以设置的返回值类型:

  1. public static CtClass booleanType;
  2. public static CtClass charType;
  3. public static CtClass byteType;
  4. public static CtClass shortType;
  5. public static CtClass intType;
  6. public static CtClass longType;
  7. public static CtClass floatType;
  8. public static CtClass doubleType;
  9. public static CtClass voidType;

(二)新增一个变量

  1. URL resource = JavassistTest2.class.getClassLoader().getResource("");
  2. String file = resource.getFile();
  3. System.out.println("文件存储路径:"+file);
  4. ClassPool cp = ClassPool.getDefault();
  5. CtClass ctClass = cp.makeClass("com.ssdmbbl.javassist.Hello");
  6. //添加一个hello1的方法
  7. CtMethod ctMethod = new CtMethod(CtClass.voidType, "hello1", new CtClass[]{CtClass.intType, CtClass.doubleType}, ctClass);
  8. ctMethod.setModifiers(Modifier.PUBLIC);
  9. ctClass.addMethod(ctMethod);
  10. //添加一个int类型的,名字为value的变量
  11. CtField ctField = new CtField(CtClass.intType,"value",ctClass);
  12. ctField.setModifiers(Modifier.PRIVATE);
  13. ctClass.addField(ctField);
  14. ctClass.writeFile(file);

那么执行后的内容就是如下了:

  1. package com.ssdmbbl.javassist;
  2. public class Hello {
  3. private int value;
  4. public void hello1(int var1, double var2) {
  5. }
  6. public Hello() {
  7. }
  8. }

(三)给变量新增get和set方法

代码修改如下:

  1. public static void main(String[] args) throws CannotCompileException, IOException {
  2. URL resource = JavassistTest2.class.getClassLoader().getResource("");
  3. String file = resource.getFile();
  4. System.out.println("文件存储路径:"+file);
  5. ClassPool cp = ClassPool.getDefault();
  6. CtClass ctClass = cp.makeClass("com.ssdmbbl.javassist.Hello");
  7. //添加一个hello1的方法
  8. CtMethod ctMethod = new CtMethod(CtClass.voidType, "hello1", new CtClass[]{CtClass.intType, CtClass.doubleType}, ctClass);
  9. ctMethod.setModifiers(Modifier.PUBLIC);
  10. ctClass.addMethod(ctMethod);
  11. //添加一个int类型的,名字为value的变量
  12. CtField ctField = new CtField(CtClass.intType,"value",ctClass);
  13. ctField.setModifiers(Modifier.PRIVATE);
  14. ctClass.addField(ctField);
  15. //为value变量添加set方法
  16. CtMethod setValue = new CtMethod(CtClass.voidType, "setValue", new CtClass[]{CtClass.intType}, ctClass);
  17. setValue.setModifiers(Modifier.PUBLIC);
  18. ctClass.addMethod(setValue);
  19. //为value变量添加get方法
  20. CtMethod getValue = new CtMethod(CtClass.intType, "getValue", new CtClass[]{}, ctClass);
  21. getValue.setModifiers(Modifier.PUBLIC);
  22. ctClass.addMethod(getValue);
  23. ctClass.writeFile(file);
  24. }

执行效果:

  1. package com.ssdmbbl.javassist;
  2. public class Hello {
  3. private int value;
  4. public void hello1(int var1, double var2) {
  5. }
  6. public void setValue(int var1) {
  7. }
  8. public int getValue() {
  9. }
  10. public Hello() {
  11. }
  12. }

(四)给方法内部添加代码

你是不是很好奇,set和get方法内部并没有代码,当程序运行的时候,肯定会出错的。
下面就来看一下。

我们预想的结果:

  1. private int value;
  2. public void setValue(int var1) {
  3. this.value = var1
  4. }
  5. public int getValue() {
  6. return this.value;
  7. }

修改如下:

  1. //为value变量添加set方法
  2. CtMethod setValue = new CtMethod(CtClass.voidType, "setValue", new CtClass[]{CtClass.intType}, ctClass);
  3. setValue.setModifiers(Modifier.PUBLIC);
  4. //设置方法体
  5. setValue.setBody("this.value = var1;");
  6. ctClass.addMethod(setValue);
  7. //为value变量添加get方法
  8. CtMethod getValue = new CtMethod(CtClass.intType, "getValue", new CtClass[]{}, ctClass);
  9. getValue.setModifiers(Modifier.PUBLIC);
  10. //设置方法体
  11. getValue.setBody("return this.value;");
  12. ctClass.addMethod(getValue);

很倒霉,说找不到这个var1这个变量

报错的堆栈信息如下:

Exception in thread "main" javassist.CannotCompileException: [source error] no such field: var1
at javassist.CtBehavior.setBody(CtBehavior.java:474)
at javassist.CtBehavior.setBody(CtBehavior.java:440)
at com.ssdmbbl.javassist.JavassistTest2.main(JavassistTest2.java:41)
Caused by: compile error: no such field: var1

这个原因我们前面其实提到了,因为在编译的时候,会把变量名抹掉,传递的参数会依次在局部变量表中的顺序。

如果传递:

  1. public void test(int a,int b,int c){
  2. }

那么a,b,c就对应本地变量表中的1,2,3的位置。

  1. —— ——
  2. a1
  3. b2
  4. c3
  5. —— ——

那么我们获取变量时就不能使用原始的名字了,在Javassist中访问方法中的参数使用的是$1, 2 , 2, 2,…,而不是直接使用原始的名字。

我们修改如下:

  1. //为value变量添加set方法
  2. CtMethod setValue = new CtMethod(CtClass.voidType, "setValue", new CtClass[]{CtClass.intType}, ctClass);
  3. setValue.setModifiers(Modifier.PUBLIC);
  4. //设置方法体
  5. setValue.setBody("this.value = $1;");
  6. ctClass.addMethod(setValue);
  7. //为value变量添加get方法
  8. CtMethod getValue = new CtMethod(CtClass.intType, "getValue", new CtClass[]{}, ctClass);
  9. getValue.setModifiers(Modifier.PUBLIC);
  10. //设置方法体
  11. getValue.setBody("return this.value;");
  12. ctClass.addMethod(getValue);

结果成功了:

  1. public class Hello {
  2. private int value;
  3. public void hello1(int var1, double var2) {
  4. }
  5. public void setValue(int var1) {
  6. this.value = var1;
  7. }
  8. public int getValue() {
  9. return this.value;
  10. }
  11. public Hello() {
  12. }
  13. }

再来一个:修改hello1方法体,使传递的两个参数相加然后赋值给value;

  1. //添加一个hello1的方法
  2. CtMethod ctMethod = new CtMethod(CtClass.voidType, "hello1", new CtClass[]{CtClass.intType, CtClass.doubleType}, ctClass);
  3. ctMethod.setModifiers(Modifier.PUBLIC);
  4. ctMethod.setBody("this.value = $1 + $2;");
  5. ctClass.addMethod(ctMethod);

执行结果如下:

因为我们value是int,$1是int,$2是double,所以做了强制转型处理。

  1. public void hello1(int var1, double var2) {
  2. this.value = (int)((double)var1 + var2);
  3. }

(五)在方法体的前后分别插入代码

测试代码如下:

  1. //添加一个hello1的方法
  2. CtMethod ctMethod = new CtMethod(CtClass.voidType, "hello1", new CtClass[]{CtClass.intType, CtClass.doubleType}, ctClass);
  3. ctMethod.setModifiers(Modifier.PUBLIC);
  4. ctMethod.setBody("this.value = $1 + $2;");
  5. ctMethod.insertBefore("System.out.println(\\"我在前面插入了:\\"+$1);");
  6. ctMethod.insertAfter("System.out.println(\\"我在最后插入了:\\"+$1);");
  7. ctClass.addMethod(ctMethod);

结果如下:

  1. public void hello1(int var1, double var2) {
  2. System.out.println("我在前面插入了:" + var1);
  3. this.value = (int)((double)var1 + var2);
  4. Object var5 = null;
  5. System.out.println("我在最后插入了:" + var1);
  6. }

三、Javassist中的一些特殊参数示例讲解

在官方文档中看到有几个比较特殊的标识符,还有几个比较特殊的标识符需要了解。

标识符 作用
$0、$1、$2、 3 、 3、 3、… this和方法参数(1-N是方法参数的顺序)
$args 方法参数数组,类型为Object[]
$$ 所有方法参数,例如:m($$)相当于m($1,$2,…)
$cflow(…) control flow 变量
$r 返回结果的类型,在强制转换表达式中使用。
$w 包装器类型,在强制转换表达式中使用。
$_ 返回的结果值
$sig 类型为java.lang.Class的参数类型对象数组
$type 类型为java.lang.Class的返回值类型
$class 类型为java.lang.Class的正在修改的类

下面咱们来一起分别来看一下,分析一下,怎么用,有什么用吧。

只介绍几个常见和常用到的吧。
有兴趣的话,剩下的可以看官方文档:http://www.javassist.org/tutorial/tutorial2.html

(一)$0,$1,$2,…

这个其实咱们已经在上面用到过了,再来细说一下吧。$0代表this,$1, 2 , 2, 2,…,依次对应方法中参数的顺序。
如果有:

  1. public void test(int a,int b,int c){}

那么如果想引用a和b和c的话,需要这样:

  1. ctMethod.setBody("return $1 + $2 + $3;");

对了还有:静态方法是没有$0的,所以静态方法下$0是不可用的。

(二)$args

$args变量表示所有参数的数组,它是一个Object类型的数组(new Object[]{…}),如果参数中有原始类型的参数,会被转换成对应的包装类型。比如原始数据类型为int,则会被转换成java.lang.Integer,然后存储在args中。

例如我们测试代码如下:

  1. CtMethod ctMethod = new CtMethod(CtClass.voidType, "hello1", new CtClass[]{CtClass.intType, CtClass.doubleType}, ctClass);
  2. ctMethod.setModifiers(Modifier.PUBLIC);
  3. ctMethod.setBody("System.out.println($args);");

编译后的结果如下:

  1. public void hello1(int var1, double var2) {
  2. System.out.println(new Object[]{new Integer(var1), new Double(var2)});
  3. }

(三)$$

变量 是 所 有 参 数 的 缩 写 , 参 数 用 逗 号 分 割 , 例 如 : m ( 是所有参数的缩写,参数用逗号分割,例如:m( 是所有参数的缩写,参数用逗号分割,例如:m()相当于:m($1,$2,$3,…)。

如何使用呢?

我们经常做一些代码优化,把较为复杂的方法内部的逻辑,提炼出来,提炼到一个公共的方法里。

或者说一个方法调用另一个方法,这两个方法传递的参数是一样的时候就用到了。

例如原java:

  1. public Object m1(String name,String age){
  2. …省略10000行代码逻辑
  3. }

提炼后的:

提炼出来的代码,我们也可以在其他地方使用(所谓的公共的代码)。

  1. public Object m1(String name,String age){
  2. …省略20行代码逻辑
  3. return m2(name,age);
  4. }
  5. private Object m2(String name,String age){
  6. }

那么我们就造一个这个场景来说明一下用法吧。

简单点来说,就是一个方法调用另一个方法,传递全参数时。

  1. CtMethod hello2 = new CtMethod(CtClass.voidType, "hello2", new CtClass[]{CtClass.intType, CtClass.doubleType}, ctClass);
  2. hello2.setModifiers(Modifier.PUBLIC);
  3. hello2.setBody("this.value = $1 + $2;");
  4. ctClass.addMethod(hello2);
  5. //添加一个hello1的方法
  6. CtMethod hello1 = new CtMethod(CtClass.voidType, "hello1", new CtClass[]{CtClass.intType, CtClass.doubleType}, ctClass);
  7. hello1.setModifiers(Modifier.PUBLIC);
  8. hello1.setBody("this.value = $1 + $2;");
  9. hello1.insertAfter("hello2($$);");

可以看到我们hello1调用hello2时,需要传递全部参数。此时即可写成$$方法,当然了也可以写成hello2($1,$2)。

编译后的结果:

  1. public void hello2(int var1, double var2) {
  2. this.value = (int)((double)var1 + var2);
  3. }
  4. public void hello1(int var1, double var2) {
  5. this.value = (int)((double)var1 + var2);
  6. Object var5 = null;
  7. this.hello2(var1, var2);
  8. }

(四)$cflow(…)

$cflow 的全名为:control flow,这是一个只读变量,返回指定方法递归调用的深度。

我们以计算n的斐波拉契数列为例,来演示一下如何使用。

我们正确的递归算法代码如下:

  1. public int f(int n){
  2. if(n <= 1){
  3. return n;
  4. }
  5. return f(n1) + f(n 2)
  6. }

对于上面这段代码,我们可以这样写:

  1. CtMethod f = new CtMethod(CtClass.intType,"f", new CtClass[]{CtClass.intType}, ctClass);
  2. f.setBody("{if($1 <= 1){" +
  3. " return $1;" +
  4. "}" +
  5. "return f($1 – 1) + f( $1 – 2);}");
  6. f.setModifiers(Modifier.PUBLIC);

编译后的:

  1. public int f(int var1) {
  2. return var1 <= 1 ? var1 : this.f(var1 1) + this.f(var1 2);
  3. }

那么我们只想在递归到前n次的时候打印log,我们该怎么做呢。

例如,我们下面写的是"$cflow(f) == 1"时

  1. CtMethod f = new CtMethod(CtClass.intType,"f", new CtClass[]{CtClass.intType}, ctClass);
  2. f.setBody("{if($1 <= 1){" +
  3. " return $1;" +
  4. "}" +
  5. "return f($1 – 1) + f( $1 – 2);}");
  6. f.setModifiers(Modifier.PUBLIC);
  7. //在代码body的前面插入
  8. f.useCflow("f");
  9. f.insertBefore("if($cflow(f) == 1){" +
  10. " System.out.println(\\"我执行了,此时的n是:\\"+$1);" +
  11. " }");

编译后的代码:

  1. //
  2. // Source code recreated from a .class file by IntelliJ IDEA
  3. // (powered by FernFlower decompiler)
  4. //
  5. package com.ssdmbbl.javassist;
  6. import javassist.runtime.Cflow;
  7. public class Hello {
  8. public static Cflow _cflow$0 = new Cflow();
  9. public int f(int var1) {
  10. if (_cflow$0.value() == 1) {
  11. System.out.println("我执行到来第2次,此时的n是:" + var1);
  12. }
  13. boolean var6 = false;
  14. int var10000;
  15. try {
  16. var6 = true;
  17. _cflow$0.enter();
  18. if (var1 <= 1) {
  19. var10000 = var1;
  20. var6 = false;
  21. } else {
  22. var10000 = this.f(var1 1) + this.f(var1 2);
  23. var6 = false;
  24. }
  25. } finally {
  26. if (var6) {
  27. boolean var3 = false;
  28. _cflow$0.exit();
  29. }
  30. }
  31. int var8 = var10000;
  32. _cflow$0.exit();
  33. return var8;
  34. }
  35. public Hello() {
  36. }
  37. }

通过查看源码,发先关键的一个地方是调用了Cflow对象的enter方法:

  1. public static Cflow _cflow$0 = new Cflow();
  2. _cflow$0.enter();

点进enter()的内部发现,调用了get().inc()方法:

  1. public class Cflow extends ThreadLocal<Cflow.Depth> {
  2. protected static class Depth {
  3. private int depth;
  4. Depth() { depth = 0; }
  5. int value() { return depth; }
  6. void inc() { ++depth; }
  7. void dec() { depth; }
  8. }
  9. @Override
  10. protected synchronized Depth initialValue() {
  11. return new Depth();
  12. }
  13. /**
  14. * Increments the counter.
  15. */
  16. public void enter() { get().inc(); }
  17. /**
  18. * Decrements the counter.
  19. */
  20. public void exit() { get().dec(); }
  21. /**
  22. * Returns the value of the counter.
  23. */
  24. public int value() { return get().value(); }
  25. }

而inc()方法控制着一个全局变量的增加操作。

  1. void inc() { ++depth; }

boolean var6 = false;相当于是一个开关,控制着是否开始和结束。

当参数var1<=1时,即结束

  1. if (var1 <= 1) {
  2. var10000 = var1;
  3. var6 = false;
  4. }

我们可以使用反射来验证一下,测试代码如下:

  1. package com.ssdmbbl.javassist;
  2. import java.lang.reflect.InvocationTargetException;
  3. import java.lang.reflect.Method;
  4. /**
  5. * @author zhenghui
  6. * @description:
  7. * @date 2021/4/8 10:20 上午
  8. */
  9. public class Test {
  10. public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException {
  11. Class<?> aClass = Class.forName("com.ssdmbbl.javassist.Hello");
  12. //初始化这个类
  13. Object obj = aClass.newInstance();
  14. //获取所有的方法
  15. Method[] methods = aClass.getMethods();
  16. //遍历所有的方法
  17. for (Method method : methods) {
  18. //当方法为f的时候,进行调用
  19. if(method.getName().equals("f")){
  20. //调用并传递参数为5,即f(5)
  21. method.invoke(obj,5);
  22. }
  23. }
  24. }
  25. }

执行的结果:

为什么是2次呢?原因上面也说了:if(var1 <= 1){…},所以是

我执行了,此时的n是:4
我执行了,此时的n是:3

相关链接

1、javassist的API接口文档

http://www.javassist.org/html/index.html

2、javassist的github开源地址

https://github.com/jboss-javassist/javassist

3、javassist的官网

http://www.javassist.org/

4、javassist官方的英文教程

http://www.javassist.org/tutorial/tutorial.html

到此这篇关于详解Java字节码编程之非常好用的javassist 的文章就介绍到这了,更多相关Java javassist 内容请搜索快网idc以前的文章或继续浏览下面的相关文章希望大家以后多多支持快网idc!

原文链接:https://blog.csdn.net/qq_17623363/article/details/115511192

收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。

快网idc优惠网 建站教程 详解Java字节码编程之非常好用的javassist https://www.kuaiidc.com/108092.html

相关文章

发表评论
暂无评论