User-Profile-Image
hankin
  • 5
  • Java
  • Kotlin
  • Spring
  • Web
  • SQL
  • MegaData
  • More
  • Experience
  • Enamiĝu al vi
  • 分类
    • Zuul
    • Zookeeper
    • XML
    • WebSocket
    • Web Notes
    • Web
    • Vue
    • Thymeleaf
    • SQL Server
    • SQL Notes
    • SQL
    • SpringSecurity
    • SpringMVC
    • SpringJPA
    • SpringCloud
    • SpringBoot
    • Spring Notes
    • Spring
    • Servlet
    • Ribbon
    • Redis
    • RabbitMQ
    • Python
    • PostgreSQL
    • OAuth2
    • NOSQL
    • Netty
    • MySQL
    • MyBatis
    • More
    • MinIO
    • MegaData
    • Maven
    • LoadBalancer
    • Kotlin Notes
    • Kotlin
    • Kafka
    • jQuery
    • JavaScript
    • Java Notes
    • Java
    • Hystrix
    • Git
    • Gateway
    • Freemarker
    • Feign
    • Eureka
    • ElasticSearch
    • Docker
    • Consul
    • Ajax
    • ActiveMQ
  • 页面
    • 归档
    • 摘要
    • 杂图
    • 问题随笔
  • 友链
    • Spring Cloud Alibaba
    • Spring Cloud Alibaba - 指南
    • Spring Cloud
    • Nacos
    • Docker
    • ElasticSearch
    • Kotlin中文版
    • Kotlin易百
    • KotlinWeb3
    • KotlinNhooo
    • 前端开源搜索
    • Ktorm ORM
    • Ktorm-KSP
    • Ebean ORM
    • Maven
    • 江南一点雨
    • 江南国际站
    • 设计模式
    • 熊猫大佬
    • java学习
    • kotlin函数查询
    • Istio 服务网格
    • istio
    • Ktor 异步 Web 框架
    • PostGis
    • kuangstudy
    • 源码地图
    • it教程吧
    • Arthas-JVM调优
    • Electron
    • bugstack虫洞栈
    • github大佬宝典
    • Sa-Token
    • 前端技术胖
    • bennyhuo-Kt大佬
    • Rickiyang博客
    • 李大辉大佬博客
    • KOIN
    • SQLDelight
    • Exposed-Kt-ORM
    • Javalin—Web 框架
    • http4k—HTTP包
    • 爱威尔大佬
    • 小土豆
    • 小胖哥安全框架
    • 负雪明烛刷题
    • Kotlin-FP-Arrow
    • Lua参考手册
    • 美团文章
    • Java 全栈知识体系
    • 尼恩架构师学习
    • 现代 JavaScript 教程
    • GO相关文档
    • Go学习导航
    • GoCN社区
    • GO极客兔兔-案例
    • 讯飞星火GPT
    • Hollis博客
    • PostgreSQL德哥
    • 优质博客推荐
    • 半兽人大佬
    • 系列教程
    • PostgreSQL文章
    • 云原生资料库
    • 并发博客大佬
Help?

Please contact us on our email for need any support

Support
    首页   ›   Java   ›   正文
Java

Java—Javassist(动态代理)(动态字节)

2021-07-02 17:10:07
1870  0 4
参考目录 隐藏
1) 概述
2) 1. 使用 Javassist 创建一个 class 文件
3) API运用
4) ClassPool
5) CtField
6) CtMethod
7) CtConstructor
8) 2. 调用生成的类对象
9) 通过反射的方式调用
10) 通过读取 .class 文件的方式调用
11) 通过接口的方式
12) 修改已有方法体,插入新的代码
13) 动态添加方法
14) 动态创建类
15) 例1:Helloworld
16) 例2:定义属性以及创建方法时多种入参和出参类型的使用
17) 例3:运行时重新加载类
18) 例4:字节码插桩监控方法采集信息
19) 例5:Bytecode指令码生成含有自定义注解的类和方法
20) 使用指令码生成方法

阅读完需:约 36 分钟

概述

Javassist是一个开源的分析、编辑和创建Java字节码的类库,可以直接编辑和生成Java生成的字节码。相对于bcel, asm等这些工具,开发者不需要了解虚拟机指令,就能动态改变类的结构,或者动态生成类。javassist简单易用, 快速。

Java 字节码以二进制的形式存储在 .class 文件中,每一个 .class 文件包含一个 Java 类或接口。Javaassist 就是一个用来 处理 Java 字节码的类库。它可以在一个已经编译好的类中添加新的方法,或者是修改已有的方法,并且不需要对字节码方面有深入的了解。同时也可以去生成一个新的类对象,通过完全手动的方式。

1. 使用 Javassist 创建一个 class 文件

首先需要引入jar包:

<dependency>
  <groupId>org.javassist</groupId>
  <artifactId>javassist</artifactId>
  <version>3.25.0-GA</version>
</dependency>

编写创建对象的类:

package com.rickiyang.learn.javassist;

import javassist.*;

/**
 * @author rickiyang
 * @date 2019-08-06
 * @Desc
 */
public class CreatePerson {

    /**
     * 创建一个Person 对象
     *
     * @throws Exception
     */
    public static void createPseson() throws Exception {
        ClassPool pool = ClassPool.getDefault();

        // 1. 创建一个空类
        CtClass cc = pool.makeClass("com.rickiyang.learn.javassist.Person");

        // 2. 新增一个字段 private String name;
        // 字段名为name
        CtField param = new CtField(pool.get("java.lang.String"), "name", cc);
        // 访问级别是 private
        param.setModifiers(Modifier.PRIVATE);
        // 初始值是 "xiaoming"
        cc.addField(param, CtField.Initializer.constant("xiaoming"));

        // 3. 生成 getter、setter 方法
        cc.addMethod(CtNewMethod.setter("setName", param));
        cc.addMethod(CtNewMethod.getter("getName", param));

        // 4. 添加无参的构造函数
        CtConstructor cons = new CtConstructor(new CtClass[]{}, cc);
        cons.setBody("{name = \"xiaohong\";}");
        cc.addConstructor(cons);

        // 5. 添加有参的构造函数
        cons = new CtConstructor(new CtClass[]{pool.get("java.lang.String")}, cc);
        // $0=this / $1,$2,$3... 代表方法参数
        cons.setBody("{$0.name = $1;}");
        cc.addConstructor(cons);

        // 6. 创建一个名为printName方法,无参数,无返回值,输出name值
        CtMethod ctMethod = new CtMethod(CtClass.voidType, "printName", new CtClass[]{}, cc);
        ctMethod.setModifiers(Modifier.PUBLIC);
        ctMethod.setBody("{System.out.println(name);}");
        cc.addMethod(ctMethod);

        //这里会将这个创建的类对象编译为.class文件
        cc.writeFile("/Users/yangyue/workspace/springboot-learn/java-agent/src/main/java/");
    }

    public static void main(String[] args) {
        try {
            createPseson();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

执行上面的 main 函数之后,会在指定的目录内生成 Person.class 文件:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.rickiyang.learn.javassist;

public class Person {
    private String name = "xiaoming";

    public void setName(String var1) {
        this.name = var1;
    }

    public String getName() {
        return this.name;
    }

    public Person() {
        this.name = "xiaohong";
    }

    public Person(String var1) {
        this.name = var1;
    }

    public void printName() {
        System.out.println(this.name);
    }
}

跟预想的一样。

在 Javassist 中,类 Javaassit.CtClass 表示 class 文件。一个 GtClass (编译时类)对象可以处理一个 class 文件,ClassPool是 CtClass 对象的容器。它按需读取类文件来构造 CtClass 对象,并且保存 CtClass 对象以便以后使用。

1. ClassPool:javassist的类池,使用ClassPool 类可以跟踪和控制所操作的类,它的工作方式与 JVM 类装载器非常相似

2. CtClass: CtClass提供了类的操作,如在类中动态添加新字段、方法和构造函数、以及改变类、父类和接口的方法。

3. CtField:类的属性,通过它可以给类创建新的属性,还可以修改已有的属性的类型,访问修饰符等

4. CtMethod:类中的方法,通过它可以给类创建新的方法,还可以修改返回类型,访问修饰符等, 甚至还可以修改方法体内容代码

5. CtConstructor:与CtMethod类似

API运用

ClassPool

        // 类库, jvm中所加载的class
	ClassPool pool = ClassPool.getDefault();
	// 加载一个已知的类, 注:参数必须为全量类名
	CtClass ctClass = pool.get("com.itheima.Student");
	// 创建一个新的类, 类名必须为全量类名
	CtClass tClass = pool.makeClass("com.itheima.Calculator");

CtField

        // 获取已知类的属性
	CtField ctField = ctClass.getDeclaredField("name");
	// 构建新的类的成员变量
	CtField ctFieldNew = new CtField(CtClass.intType,"age",ctClass);
	// 设置类的访问修饰符为public
	ctFieldNew.setModifiers(Modifier.PUBLIC);
	// 将属性添加到类中
	ctClass.addField(ctFieldNew);

CtMethod

        // 获取已有方法
	//创建新的方法, 参数1:方法的返回类型,参数2:名称,参数3:方法的参数,参数4:方法所属的类
	CtMethod ctMethod = new CtMethod(CtClass.intType, "calc", new CtClass[]
{CtClass.intType,CtClass.intType}, tClass);
	// 设置方法的访问修饰
	ctMethod.setModifiers(Modifier.PUBLIC);
	// 将新建的方法添加到类中
	ctClass.addMethod(ctMethod);
	// 方法体内容代码 $1代表第一个参数,$2代表第二个参数
	ctMethod.setBody("return $1 + $2;"); 

	CtMethod ctMethod = ctClass.getDeclaredMethod("sayHello");

CtConstructor

// 获取已有的构造方法, 参数为构建方法的参数类型数组
CtConstructor ctConstructor = ctClass.getDeclaredConstructor(new CtClass[]{});
// 创建新的构造方法
CtConstructor ctConstructor = new CtConstructor(new CtClass[]{CtClass.intType},ctClass); ctConstructor.setModifiers(Modifier.PUBLIC);
ctConstructor.setBody("this.age = $1;");
ctClass.addConstructor(ctConstructor);
// 也可直接创建
ctConstructor = CtNewConstructor.make("public Student(int age){this.age=age;}", ctClass);

需要注意的是 ClassPool 会在内存中维护所有被它创建过的 CtClass,当 CtClass 数量过多时,会占用大量的内存,API中给出的解决方案是 有意识的调用CtClass的detach()方法以释放内存。

ClassPool需要关注的方法:

  1. getDefault : 返回默认的ClassPool 是单例模式的,一般通过该方法创建我们的ClassPool;
  2. appendClassPath, insertClassPath : 将一个ClassPath加到类搜索路径的末尾位置 或 插入到起始位置。通常通过该方法写入额外的类搜索路径,以解决多个类加载器环境中找不到类的尴尬;
  3. toClass : 将修改后的CtClass加载至当前线程的上下文类加载器中,CtClass的toClass方法是通过调用本方法实现。需要注意的是一旦调用该方法,则无法继续修改已经被加载的class;
  4. get , getCtClass : 根据类路径名获取该类的CtClass对象,用于后续的编辑。

CtClass需要关注的方法:

  1. freeze : 冻结一个类,使其不可修改;
  2. isFrozen : 判断一个类是否已被冻结;
  3. prune : 删除类不必要的属性,以减少内存占用。调用该方法后,许多方法无法将无法正常使用,慎用;
  4. defrost : 解冻一个类,使其可以被修改。如果事先知道一个类会被defrost, 则禁止调用 prune 方法;
  5. detach : 将该class从ClassPool中删除;
  6. writeFile : 根据CtClass生成 .class 文件;
  7. toClass : 通过类加载器加载该CtClass。

上面我们创建一个新的方法使用了CtMethod类。CtMthod代表类中的某个方法,可以通过CtClass提供的API获取或者CtNewMethod新建,通过CtMethod对象可以实现对方法的修改。

CtMethod中的一些重要方法:

  1. insertBefore : 在方法的起始位置插入代码;
  2. insterAfter : 在方法的所有 return 语句前插入代码以确保语句能够被执行,除非遇到exception;
  3. insertAt : 在指定的位置插入代码;
  4. setBody : 将方法的内容设置为要写入的代码,当方法被 abstract修饰时,该修饰符被移除;
  5. make : 创建一个新的方法。

注意到在上面代码中的:setBody()的时候我们使用了一些符号:

// $0=this / $1,$2,$3... 代表方法参数
cons.setBody("{$0.name = $1;}");

具体还有很多的符号可以使用,但是不同符号在不同的场景下会有不同的含义,所以在这里就不在赘述,可以看javassist 的说明文档。http://www.javassist.org/tutorial/tutorial2.html

2. 调用生成的类对象

通过反射的方式调用

上面的案例是创建一个类对象然后输出该对象编译完之后的 .class 文件。那如果我们想调用生成的类对象中的属性或者方法应该怎么去做呢?javassist也提供了相应的api,生成类对象的代码还是和第一段一样,将最后写入文件的代码替换为如下:

// 这里不写入文件,直接实例化
Object person = cc.toClass().newInstance();
// 设置值
Method setName = person.getClass().getMethod("setName", String.class);
setName.invoke(person, "cunhua");
// 输出值
Method execute = person.getClass().getMethod("printName");
execute.invoke(person);

然后执行main方法就可以看到调用了 printName方法。

通过读取 .class 文件的方式调用

ClassPool pool = ClassPool.getDefault();
// 设置类路径
pool.appendClassPath("/Users/yangyue/workspace/springboot-learn/java-agent/src/main/java/");
CtClass ctClass = pool.get("com.rickiyang.learn.javassist.Person");
Object person = ctClass.toClass().newInstance();
//  ...... 下面和通过反射的方式一样去使用

通过接口的方式

上面两种其实都是通过反射的方式去调用,问题在于我们的工程中其实并没有这个类对象,所以反射的方式比较麻烦,并且开销也很大。那么如果你的类对象可以抽象为一些方法得合集,就可以考虑为该类生成一个接口类。这样在newInstance()的时候我们就可以强转为接口,可以将反射的那一套省略掉了。

还拿上面的Person类来说,新建一个PersonI接口类:

package com.rickiyang.learn.javassist;

/**
 * @author rickiyang
 * @date 2019-08-07
 * @Desc
 */
public interface PersonI {

    void setName(String name);

    String getName();

    void printName();

}

实现部分的代码如下:

ClassPool pool = ClassPool.getDefault();
pool.appendClassPath("/Users/yangyue/workspace/springboot-learn/java-agent/src/main/java/");

// 获取接口
CtClass codeClassI = pool.get("com.rickiyang.learn.javassist.PersonI");
// 获取上面生成的类
CtClass ctClass = pool.get("com.rickiyang.learn.javassist.Person");
// 使代码生成的类,实现 PersonI 接口
ctClass.setInterfaces(new CtClass[]{codeClassI});

// 以下通过接口直接调用 强转
PersonI person = (PersonI)ctClass.toClass().newInstance();
System.out.println(person.getName());
person.setName("xiaolv");
person.printName();

使用起来很轻松。


添加Student类

public class Student {
    private int age; private String name;
    public int getAge() { 
        return age;
    }
    public void setAge(int age) { 
        this.age = age;
    }
    public String getName() { 
        return name;
    }
    public void setName(String name) { 
        this.name = name;
    }
    public void sayHello(){
        System.out.println("Hello, My name is " + this.name);
    }
}

修改已有方法体,插入新的代码

对已有的student类中的sayHello方法,当调用时,控制台会输出: Hello, My name is 张三(name=张三)

需求:通过动态修改sayHello方法,当调用sayHello时,除了输出已经的内容外,再输出当前学生的age信息 创建JavassistDemo测试类,代码实现如下:

public class JavassistDemo {
    public void t1() throws Exception{
        // 类库池, jvm中所加载的class
        ClassPool pool = ClassPool.getDefault();
        // 获取指定的Student类
        CtClass ctClass = pool.get("com.itheima.Student");
        // 获取sayHello方法
        CtMethod ctMethod = ctClass.getDeclaredMethod("sayHello");
        // 在方法的代码后追加 一段代码
        ctMethod.insertAfter("System.out.println(\"I'm \" + this.age + \" years old.\");");
        // 使用当前的ClassLoader加载被修改后的类
        Class<Student> newClass = ctClass.toClass();

        Student stu = newClass.newInstance();
        stu.setName("张三");
        stu.setAge(18);
        stu.sayHello();
    }
}	

动态添加方法

接下来我们给Student类添加一个计算的方法,但不是直接在Student类中添加,而是使用javassist,动态添加

   public void t2() throws Exception {
    // 类库池, jvm中所加载的class
    ClassPool pool = ClassPool.getDefault();
    // 获取指定的Student类
    CtClass ctClass = pool.get("com.itheima.Student");
    // 创建calc方法, 带两个参数,参数的类型都为int类型
    CtMethod ctMethod = new CtMethod(CtClass.intType, "calc",
            new CtClass[]{CtClass.intType, CtClass.intType}, ctClass);
    // 设置方法的访问修饰
    ctMethod.setModifiers(Modifier.PUBLIC);
    // 设置方法体代码
    ctMethod.setBody("return $1 + $2;");
    // 添加新建的方法到原有的类中
    ctClass.addMethod(ctMethod);
    // 加载修改后的类
    ctClass.toClass();
    // 创建对象
    Student stu = new Student();
    // 获取calc方法
    Method dMethod = Student.class.getDeclaredMethod("calc", new Class[]{int.class, int.class});
    // 反射调用 方法
    Object result = dMethod.invoke(stu, 10, 20);
    // 打印结果
    System.out.println(String.format("调用calc方法,传入参数:%d,%d", 10, 20));
    System.out.println("返回结果:" + (int) result);
   }

动态创建类

下面我们再来个神的魔术,无中生有

public void t3() throws Exception{
   ClassPool pool = ClassPool.getDefault();
   // 创建teacher类
   CtClass teacherClass = pool.makeClass("com.itheima.Teacher");
   // 设置为公有类
   teacherClass.setModifiers(Modifier.PUBLIC);
   // 获取String类型
   CtClass stringClass = pool.get("java.lang.String");
   // 获取list类型
   CtClass listClass = pool.get("java.util.List");
   // 获取学生的类型
   CtClass studentClass = pool.get("com.itheima.Student");
   // 给teacher添加name属性
   CtField nameField = new CtField(stringClass, "name", teacherClass);
   nameField.setModifiers(Modifier.PUBLIC);
   teacherClass.addField(nameField);
   // 给teacher类添加students属性
   CtField studentList = new CtField(listClass, "students",teacherClass);
   studentList.setModifiers(Modifier.PUBLIC);
   teacherClass.addField(studentList); 
   // 给teacher类添加无参构造方法
   CtConstructor ctConstructor = CtNewConstructor.make("public Teacher() {this.name=\"abc\";this.students = new java.util.ArrayList();}", teacherClass);
    teacherClass.addConstructor(ctConstructor); 
    // 给teacher类添加addStudent方法
    CtMethod m = new CtMethod(CtClass.voidType, "addStudent", new CtClass[]{studentClass}, teacherClass);
    m.setModifiers(Modifier.PUBLIC);
    // 添加学生对象到students属性中, $1代表参数1
    m.setBody("this.students.add($1);");
    teacherClass.addMethod(m); 
    // 给teacher类添加sayHello方法
    m = new CtMethod(CtClass.voidType, "sayHello", new CtClass[]{}, teacherClass);
    m.setModifiers(Modifier.PUBLIC);
    m.setBody("System.out.println(\"Hello, My name is \" + this.name);");
    m.insertAfter("System.out.println(\"I have \" + this.students.size() + \" students\");");
    teacherClass.addMethod(m); 
    // 加载修改后的类
    Class<?> cls = teacherClass.toClass();
    // 实例teacher对象
    Object obj = cls.newInstance();
    // 获取addStudent方法
    Method method = cls.getDeclaredMethod("addStudent", Student.class);
  }

例1:Helloworld

实现目标

public class HelloWorld2 {
    public static void main(String[] var0) {
        System.out.println("javassist hi helloworld");
    }

    public HelloWorld2() {
    }
}

实现代码

public class GenerateClazzMethod {


    public static void main(String[] args) throws IOException, CannotCompileException, NotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {

        // 创建 ClassPool,它是一个基于HashMap实现的 CtClass 对象容器。
        ClassPool pool = ClassPool.getDefault();

        // 创建类 classname:创建类路径和名称
        CtClass ctClass = pool.makeClass("com.enmalvi.Javassist.HelloWorld2");

        // 添加方法 接下来就是给类添加方法。包括;方法的属性、类型、名称、入参、出参和方法体的内容。
        CtMethod mainMethod = new CtMethod(CtClass.voidType, "main", new CtClass[]{pool.get(String[].class.getName())}, ctClass);
        mainMethod.setModifiers(Modifier.PUBLIC + Modifier.STATIC);
        mainMethod.setBody("{System.out.println(\"javassist hi helloworld\");}");
        ctClass.addMethod(mainMethod);

        // 创建无参数构造方法 在方法创建好后还需要创建一个空的构造函数,每一个类都会在编译后生成这样一个构造函数。
        CtConstructor ctConstructor = new CtConstructor(new CtClass[]{}, ctClass);
        ctConstructor.setBody("{}");
        ctClass.addConstructor(ctConstructor);

        // 输出类内容
        // 当方法创建完成后,我们使用 ctClass.writeFile() 进行输出方法的内容信息。也就可以看到通过我们使用 Javassist 生成类的样子。
        ctClass.writeFile();

        // 测试调用
        Class clazz = ctClass.toClass();
        Object obj = clazz.newInstance();

        // 最后就是我们的反射调用 main 方法,测试输出结果。
        Method main = clazz.getDeclaredMethod("main", String[].class);
        main.invoke(obj, (Object)new String[1]);

    }

}

这段代码分为几块内容来实现功能,分别包括;

  1. 创建 ClassPool,它是一个基于HashMap实现的 CtClass 对象容器。
  2. 使用 CtClass,创建我们的类信息,也就是类的路径和名称。
  3. 接下来就是给类添加方法。包括;方法的属性、类型、名称、入参、出参和方法体的内容。
  4. 在方法创建好后还需要创建一个空的构造函数,每一个类都会在编译后生成这样一个构造函数。
  5. 当方法创建完成后,我们使用 ctClass.writeFile() 进行输出方法的内容信息。也就可以看到通过我们使用 Javassist 生成类的样子。
  6. 最后就是我们的反射调用 main 方法,测试输出结果。

例2:定义属性以及创建方法时多种入参和出参类型的使用

大致了解到创建在使用字节码编程的时候基本离不开三个核心类;ClassPool、CtClass、CtMethod,它们分别管理着对象容器、类和方法。但是我们还少用一样就是字段;CtFields

在学习之前先重点列一下相关的知识点,如下;

  1. CtClass.doubleType、intType、floatType等 8 个基本类型和一个voidType,也就是空的返回类型。
  2. 传递和返回的是对象类型时,那么需要时用;pool.get(Double.class.getName(),进行设置。
  3. 当需要设置多个入参时,需要在数组中以此设置入参类型;new CtClass[]{CtClass.doubleType, CtClass.doubleType}。
  4. 在方法体中需要取得入参并计算时,需要使用 $1、$2 …,数字表示入参的位置。$0 是 this。
  5. CtField 设置属性字段,并赋值。
  6. Javassist 中的装箱/拆箱。

实现目标

public class demo {
    private static final double π;

    public double calculateCircularArea(double var1) {
        return π * var1 * var1;
    }

    public Double sumOfTwoNumbers(double var1, double var3) {
        return var1 + var3;
    }

    public demo() {
    }
}

目标实现

public class GenerateClazzMethod {

    public static void main(String[] args) throws CannotCompileException, NotFoundException, IOException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {

        ClassPool cp =ClassPool.getDefault();

        CtClass ctClass=cp.makeClass("com.enmalvi.Javassist.Javassist02.demo");

        /**
         * CtField,属性字段的创建。这就像我们正常写代码一样,需要设定属性的;名称、类型以及是 public 的还是 private 的以及 static 和 final 等。
         * 都可以通过 Modifier.PRIVATE + Modifier.STATIC + Modifier.FINAL,通过组合来控制。
         * 同样这也适用于对方法类型的设置。同时需要在添加属性的地方,设置初始值。
         */
        CtField ctField=new CtField(CtClass.doubleType,"π",ctClass);
        ctField.setModifiers(Modifier.PRIVATE+Modifier.STATIC+Modifier.FINAL);
        ctClass.addField(ctField);

        /**
         * 那么需要通过符号 $+数字,来获取入参。这个数字就是当前入参的位置。比如取第一个入参:$1,以此类推。
         */
        CtMethod ctMethod=new CtMethod(CtClass.doubleType,"calculateCircularArea",new CtClass[]{CtClass.doubleType},ctClass);
        ctMethod.setModifiers(Modifier.PUBLIC);
        // 这里的 ; 一定要写
        ctMethod.setBody("{return π * $1 * $1 ;}");
        ctClass.addMethod(ctMethod);

        /**
         * 之后是我们的多种入参类型,在这开始我们也提到了。
         * 如果是基本类型入参都可以使用 CtClass.doubleType,对象类型入参使用 pool.get(类.class.getName) 获取。
         */
        CtMethod ctMethod1=new CtMethod(cp.get(Double.class.getName()),"sumOfTwoNumbers",new CtClass[]{CtClass.doubleType, CtClass.doubleType},ctClass);
        ctMethod1.setModifiers(Modifier.PUBLIC);
        ctMethod1.setBody("{return Double.valueOf($1 + $2);}");
        ctClass.addMethod(ctMethod1);
        // 输出类的内容
        ctClass.writeFile();

        // 测试调用
        Class clazz = ctClass.toClass();
        Object obj = clazz.newInstance();

        Method method_calculateCircularArea = clazz.getDeclaredMethod("calculateCircularArea", double.class);
        Object obj_01 = method_calculateCircularArea.invoke(obj, 1.23);
        System.out.println("圆面积:" + obj_01);

        Method method_sumOfTwoNumbers = clazz.getDeclaredMethod("sumOfTwoNumbers", double.class, double.class);
        Object obj_02 = method_sumOfTwoNumbers.invoke(obj, 1, 2);
        System.out.println("两数和:" + obj_02);

    }

}

这里面有几个核心点,讲解如下;

  1. CtField,属性字段的创建。这就像我们正常写代码一样,需要设定属性的;名称、类型以及是 public 的还是 private 的以及 static 和 final 等。都可以通过 Modifier.PRIVATE + Modifier.STATIC + Modifier.FINAL,通过组合来控制。同样这也适用于对方法类型的设置。同时需要在添加属性的地方,设置初始值。
  2. 接下来是我们设置了一个求圆面积的方法,如果说在方法体中需要使用到入参类型。那么需要通过符号 $+数字,来获取入参。这个数字就是当前入参的位置。比如取第一个入参:$1,以此类推。
  3. 之后是我们的多种入参类型,在这开始我们也提到了。如果是基本类型入参都可以使用 CtClass.doubleType,对象类型入参使用 pool.get(类.class.getName) 获取。
  4. 最终同样我们会把使用字节码编译的 class 输出到工程目录下 ctClass.writeFile()。
  5. 在Javassist中并不会给类型做拆箱和装箱操作,需要显式的处理。例如上面案例中,需要将 double 使用 Double.valueOf 进行转换。

这张基本描述了一个类方法在创建时候不同参数的含义

这里重点强调了属性字段创建,同时需要给属性字段赋值。在 Javassist 是不会进行类型的自动装箱和拆箱的,需要我们进行手动处理,否则生成类在执行会报类型错误。

例3:运行时重新加载类

通过前面两个 javassist 的基本内容,大体介绍了;类池(ClassPool)、类(CtClass)、属性(CtField)、方法(CtMethod),的使用方式,并通过创建不同类型的入参出参方法,基本可以掌握如何使用这样的代码结构进行字节码编程。

那么,我们可以尝试使用 javassist 去修改一个正在执行中的类里面的方法内容。也就是在运行时重新加载类信息

原本的方法

public class ApiTest {

    public String queryGirlfriendCount(String boyfriendName) {
        return boyfriendName + "的前女友数量:" + (new Random().nextInt(10) + 1) + " 个";
    }

}

目标是在运行时改变方法的结果将数量变成0返回输出

实现代码

/**
 * 尝试使用 javassist 去修改一个正在执行中的类里面的方法内容。也就是在运行时重新加载类信息
 *
 * Javassist 对 ASM 这样的字节码操作封装起来提供的API确实很好操作,在一些场景下也不需要考虑 JVM 中局部变量和操作数栈。
 * 但如果需要更高的性能,可以考虑使用 ASM。
 * @author xujiahui
 */
public class GenerateClazzMethod {
    public static void main(String[] args) throws Exception {
        ApiTestKt apiTest = new ApiTestKt();
        System.out.println("你到底几个前女友!!!");

        // 模拟谢飞机老婆一顿查询
        new Thread(() -> {
            while (true){
                System.out.println(apiTest.queryGirlfriendCount("谢飞机"));
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();


        // 监听 8000 端口,在启动参数里设置
        // java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8000
        /**
         * javassist.tools.HotSwapper,是 javassist 的包中提供的热加载替换类操作。
         * 在执行时需要启用 JPDA(Java平台调试器体系结构)。
         */
        HotSwapper hs = new HotSwapper(8000);

        ClassPool pool = ClassPool.getDefault();
        CtClass ctClass = pool.get(ApiTestKt.class.getName());

        /**
         * ctMethod.setBody,重写方法的内容在上面两个章节已经很清楚的描述了。
         * $1 是获取方法中的第一个入参,大括号{}里是具体执行替换的方法体。
         */
        // 获取方法
        CtMethod ctMethod = ctClass.getDeclaredMethod("queryGirlfriendCount");
        // 重写方法
        ctMethod.setBody("{ return $1 + \"的前女友数量:\" + (0L) + \" 个\"; }");

        // 加载新的类
        System.out.println(":: 执行HotSwapper热插拔,修改谢飞机前女友数量为0个!");
        /**
         * 最后使用 hs.reload 执行热加载替换操作,这里的 ctClass.toBytecode() 获取的是处理后类的字节码。
         */
        hs.reload(ApiTestKt.class.getName(), ctClass.toBytecode());

    }

}
  1. 多线程模拟循环调用,这个方法会一直执行查询。在后续修改类之后输出的结果信息会有不同。
  2. javassist.tools.HotSwapper,是 javassist 的包中提供的热加载替换类操作。在执行时需要启用 JPDA(Java平台调试器体系结构)。
  3. ctMethod.setBody,重写方法的内容在上面两个章节已经很清楚的描述了。$1 是获取方法中的第一个入参,大括号{}里是具体执行替换的方法体。
  4. 最后使用 hs.reload 执行热加载替换操作,这里的 ctClass.toBytecode() 获取的是处理后类的字节码。

这个有个很关键的点就是除了要添加java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8000之外还要引入tools的jar包,一般这个包都在JDK的安装目录下

结果:

Listening for transport dt_socket at address: 8000
你到底几个前女友!!!
谢飞机的前女友数量:3 个
谢飞机的前女友数量:5 个
谢飞机的前女友数量:8 个
:: 执行HotSwapper热插拔,修改谢飞机前女友数量为0个!
谢飞机的前女友数量:4 个
谢飞机的前女友数量:5 个
谢飞机的前女友数量:0 个
谢飞机的前女友数量:0 个
谢飞机的前女友数量:0 个
谢飞机的前女友数量:0 个
谢飞机的前女友数量:0 个
谢飞机的前女友数量:0 个
谢飞机的前女友数量:0 个
谢飞机的前女友数量:0 个
谢飞机的前女友数量:0 个
...

例4:字节码插桩监控方法采集信息

字节码编程插桩这种技术常与 Javaagent 技术结合用在系统的非入侵监控中,这样就可以替代在方法中进行硬编码操作。比如,你需要监控一个方法,包括;方法信息、执行耗时、出入参数、执行链路以及异常等。那么就非常适合使用这样的技术手段进行处理。

为了能让这部分最核心的内容体现出来,本文会只使用 Javassist 技术对一段方法字节码进行插桩操作

需要测试的方法

public class ApiTest {

    public Integer strToInt(String str01, String str02) {
        return Integer.parseInt(str01);
    }

}

方法说明的实体类

public class MethodDescription {

    private String clazzName;                // 类名称
    private String methodName;               // 方法名称
    private List<String> parameterNameList;  // 参数名称[集合]
    private List<String> parameterTypeList;  // 参数类型[集合]
    private String returnType;               // 返回类型

    public String getClazzName() {
        return clazzName;
    }

    public void setClazzName(String clazzName) {
        this.clazzName = clazzName;
    }

    public String getMethodName() {
        return methodName;
    }

    public void setMethodName(String methodName) {
        this.methodName = methodName;
    }

    public List<String> getParameterNameList() {
        return parameterNameList;
    }

    public void setParameterNameList(List<String> parameterNameList) {
        this.parameterNameList = parameterNameList;
    }

    public List<String> getParameterTypeList() {
        return parameterTypeList;
    }

    public void setParameterTypeList(List<String> parameterTypeList) {
        this.parameterTypeList = parameterTypeList;
    }

    public String getReturnType() {
        return returnType;
    }

    public void setReturnType(String returnType) {
        this.returnType = returnType;
    }
}

监视器的实现方法

public class Monitor {

    public static final int MAX_NUM = 1024 * 32;
    private final static AtomicInteger index = new AtomicInteger(0);
    private final static AtomicReferenceArray<MethodDescription> methodTagArr = new AtomicReferenceArray<>(MAX_NUM);

    /**
     * 缓存方法的信息
     * @param clazzName
     * @param methodName
     * @param parameterNameList
     * @param parameterTypeList
     * @param returnType
     * @return
     */
    public static int generateMethodId(String clazzName, String methodName, List<String> parameterNameList, List<String> parameterTypeList, String returnType) {

        MethodDescription methodDescription = new MethodDescription();
        methodDescription.setClazzName(clazzName);
        methodDescription.setMethodName(methodName);
        methodDescription.setParameterNameList(parameterNameList);
        methodDescription.setParameterTypeList(parameterTypeList);
        methodDescription.setReturnType(returnType);

        int methodId = index.getAndIncrement();
        if (methodId > MAX_NUM) return -1;
        methodTagArr.set(methodId, methodDescription);
        return methodId;
    }

    /**
     * 这里一共有两个方法,一个用于记录正常情况下的监控信息。另外一个用于记录异常时候的信息。
     * 如果是实际的业务场景中,就可以通过这样的方法使用 MQ 将监控信息发送给服务端记录起来并做展示。
     *
     * @param methodId
     * @param startNanos
     * @param parameterValues
     * @param returnValues
     */
    public static void point(final int methodId, final long startNanos, Object[] parameterValues, Object returnValues) {
        MethodDescription method = methodTagArr.get(methodId);
        System.out.println("监控 - Begin");
        System.out.println("方法:" + method.getClazzName() + "." + method.getMethodName());
        System.out.println("入参:" + JSON.toJSONString(method.getParameterNameList()) + " 入参[类型]:" + JSON.toJSONString(method.getParameterTypeList()) + " 入数[值]:" + JSON.toJSONString(parameterValues));
        System.out.println("出参:" + method.getReturnType() + " 出参[值]:" + JSON.toJSONString(returnValues));
        System.out.println("耗时:" + (System.nanoTime() - startNanos) / 1000000 + "(s)");
        System.out.println("监控 - End\r\n");
    }

    public static void point(final int methodId, Throwable throwable) {
        MethodDescription method = methodTagArr.get(methodId);
        System.out.println("监控 - Begin");
        System.out.println("方法:" + method.getClazzName() + "." + method.getMethodName());
        System.out.println("异常:" + throwable.getMessage());
        System.out.println("监控 - End\r\n");
    }

}

实现代码

public class GenerateClazzMethod extends ClassLoader {

    public static void main(String[] args) throws Exception {

        ClassPool pool = ClassPool.getDefault();

        // 获取类
        CtClass ctClass = pool.get(com.enmalvi.Javassist.Javassist04.ApiTest.class.getName());
        ctClass.replaceClassName("com.enmalvi.Javassist.Javassist04.ApiTest", "com.enmalvi.Javassist.Javassist04.ApiTest007");
        String clazzName = ctClass.getName();

        // 获取方法
        CtMethod ctMethod = ctClass.getDeclaredMethod("strToInt");
        String methodName = ctMethod.getName();

        // 方法信息:methodInfo.getDescriptor();
        // MethodInfo 中包括了方法的信息;名称、类型等内容。
        // 可以输出方法描述信息。(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/Integer;,其实就是方法的出入参和返回值。
        MethodInfo methodInfo = ctMethod.getMethodInfo();

        // 方法:入参信息
        // LocalVariableAttribute,获取方法的入参的名称。
        // parameterTypes,获取方法入参的类型。
        CodeAttribute codeAttribute = methodInfo.getCodeAttribute();
        LocalVariableAttribute attr = (LocalVariableAttribute) codeAttribute.getAttribute(LocalVariableAttribute.tag);
        CtClass[] parameterTypes = ctMethod.getParameterTypes();

        // 获取方法的入参需要判断方法的类型,静态类型的方法还包含了 this 参数。AccessFlag.STATIC。
        // 通过 methodInfo.getAccessFlags() 获取方法的标识,之后通过 与运算,AccessFlag.STATIC,
        // 判断方法是否为静态方法。因为静态方法会影响后续的参数名称获取,静态方法第一个参数是 this ,需要排除。
        boolean isStatic = (methodInfo.getAccessFlags() & AccessFlag.STATIC) != 0;  // 判断是否为静态方法
        int parameterSize = isStatic ? attr.tableLength() : attr.tableLength() - 1; // 静态类型取值
        List<String> parameterNameList = new ArrayList<>(parameterSize);            // 入参名称
        List<String> parameterTypeList = new ArrayList<>(parameterSize);            // 入参类型
        StringBuilder parameters = new StringBuilder();                             // 参数组装;$1、$2...,$$可以获取全部,但是不能放到数组初始化

        for (int i = 0; i < parameterSize; i++) {
            parameterNameList.add(attr.variableName(i + (isStatic ? 0 : 1))); // 静态类型去掉第一个this参数
            parameterTypeList.add(parameterTypes[i].getName());
            if (i + 1 == parameterSize) {
                parameters.append("$").append(i + 1);
            } else {
                parameters.append("$").append(i + 1).append(",");
            }
        }

        // 方法:出参信息
        // 对于方法的出参信息,只需要获取出参类型。
        CtClass returnType = ctMethod.getReturnType();
        String returnTypeName = returnType.getName();

        // 方法:生成方法唯一标识ID
        // 在监控的适合,不可能每一次调用都把所有方法信息汇总输出出来。这样做不只是性能问题,而是这些都是固定不变的信息,没有必要让每一次方法执行都输出。
        // 好!那么在方法编译时候,给每一个方法都生成一个唯一ID,用ID关联上方法的固定信息。也就可以把监控数据通过ID传递到外面。
        int idx = com.enmalvi.Javassist.Javassist04.Monitor.generateMethodId(clazzName, methodName, parameterNameList, parameterTypeList, returnTypeName);

        // $1 $2 ... 用于获取不同位置的参数。$$ 可以获取全部入参,但是不太适合用在数值传递中。

        // 定义属性
        // 定义一个 long 类型的属性,startNanos。并通过 insertBefore 插入到方法内容的开始处。
        ctMethod.addLocalVariable("startNanos", CtClass.longType);
        // 这里定义一个数组类型的属性,Object[],用于记录入参信息。
        ctMethod.addLocalVariable("parameterValues", pool.get(Object[].class.getName()));

        // 方法前加强
        ctMethod.insertBefore("{ startNanos = System.nanoTime(); parameterValues = new Object[]{" + parameters.toString() + "}; }");

        // 方法后加强
        // 这里通过静态方法将监控参数传递给外部;idx、startNanos、parameterValues、$_出参值
        // kt--todo
//        ctMethod.insertAfter("{ com.enmalvi.Javassist.Javassist04.Monitor.point(" + idx + ", startNanos);}", false); // 如果返回类型非对象类型,$_ 需要进行类型转换
        ctMethod.insertAfter("{ com.enmalvi.Javassist.Javassist04.Monitor.point(" + idx + ", startNanos, parameterValues, $_);}", false); // 如果返回类型非对象类型,$_ 需要进行类型转换

        // 方法;添加TryCatch
        // 但是如果方法抛出异常,那么这个时候就不能做到收集监控信息了。所以还需要给方法添加上 TryCatch。

        // addCatch 最开始执行就包裹原有方法内的内容,最后执行就包括所有内容。它依赖于顺序操作,其他的方法也是这样;insertBefore、insertAfter。
        // 这里通过 addCatch 将方法包装在 TryCatch 里面。
        // 再通过在 catch 中调用外部方法,将异常信息输出。
        // 同时有一个点需要注意,$e,用于获取抛出异常的内容。
        ctMethod.addCatch("{ com.enmalvi.Javassist.Javassist04.Monitor.point(" + idx + ", $e); throw $e; }", ClassPool.getDefault().get("java.lang.Exception"));   // 添加异常捕获

        // 输出类的内容
        ctClass.writeFile();

        // 测试调用
        byte[] bytes = ctClass.toBytecode();
        Class<?> clazzNew = new GenerateClazzMethod().defineClass("com.enmalvi.Javassist.Javassist04.ApiTest007", bytes, 0, bytes.length);

        // 反射获取 main 方法
        Method method = clazzNew.getMethod("strToInt", String.class, String.class);
        Object obj_01 = method.invoke(clazzNew.newInstance(), "1", "2");
        System.out.println("正确入参:" + obj_01);

        Object obj_02 = method.invoke(clazzNew.newInstance(), "a", "b");
        System.out.println("异常入参:" + obj_02);

    }

结果

监控 - Begin
方法:com.enmalvi.Javassist.Javassist04.ApiTest007.strToInt
入参:["str01","str02"] 入参[类型]:["java.lang.String","java.lang.String"] 入数[值]:["1","2"]
出参:java.lang.Integer 出参[值]:1
耗时:30(s)
监控 - End

正确入参:1
监控 - Begin
方法:com.enmalvi.Javassist.Javassist04.ApiTest007.strToInt
异常:For input string: "a"
监控 - End
  • 基于 Javassist 字节码操作框架可以非常方便的去进行字节码增强,也不需要考虑纯字节码编程下的指令码控制。但如果考虑性能以及更加细致的改变,还是需要使用到 ASM 。
  • methodInfo.getDescriptor(),可以输出方法描述信息。(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/Integer;,其实就是方法的出入参和返回值。
  • $1 $2 ... 用于获取不同位置的参数。$$ 可以获取全部入参,但是不太适合用在数值传递中。
  • 获取方法的入参需要判断方法的类型,静态类型的方法还包含了 this 参数。AccessFlag.STATIC。
  • addCatch 最开始执行就包裹原有方法内的内容,最后执行就包括所有内容。它依赖于顺序操作,其他的方法也是这样;insertBefore、insertAfter。

例5:Bytecode指令码生成含有自定义注解的类和方法

整体来说对 Javassist 已经有一个基本的使用认知。那么在 Javassist 中不仅提供了高级 API 用于创建和修改类、方法,还提供了低级 API 控制字节码指令的方式进行操作类、方法。

有了这样的 javassist API在一些特殊场景下就可以使用字节码指令控制方法。

1、实现目标

  1. 使用指令码修改原有方法返回值
  2. 使用指令码生成一样的方法

测试方法

@RpcGatewayClazz(clazzDesc = "用户信息查询服务", alias = "api", timeOut = 500)
public class ApiTest {

    @RpcGatewayMethod(methodDesc = "查询息费", methodName = "interestFee")
    public double queryInterestFee(String uId){
        return BigDecimal.TEN.doubleValue();  // 模拟息费计算返回
    }

}

自定义注解

public @interface RpcGatewayClazz {

    String clazzDesc() default "";
    String alias() default "";
    long timeOut() default 350;

}

public @interface RpcGatewayMethod {

    String methodName() default "";
    String methodDesc() default "";
    
}

这里使用的注解是测试中自定义的,模拟一个相当于网关接口的暴漏。

实现代码

public class GenerateClazzMethod {

    public static void main(String[] args) throws Exception {

        ClassPool pool = ClassPool.getDefault();
        // 类、注解
        CtClass ctClass = pool.get(ApiTest.class.getName());
        // 通过集合获取自定义注解
        Object[] clazzAnnotations = ctClass.getAnnotations();
        RpcGatewayClazz rpcGatewayClazz = (RpcGatewayClazz) clazzAnnotations[0];
        System.out.println("RpcGatewayClazz.clazzDesc:" + rpcGatewayClazz.clazzDesc());
        System.out.println("RpcGatewayClazz.alias:" + rpcGatewayClazz.alias());
        System.out.println("RpcGatewayClazz.timeOut:" + rpcGatewayClazz.timeOut());

        // 方法、注解
        CtMethod ctMethod = ctClass.getDeclaredMethod("queryInterestFee");
        RpcGatewayMethod rpcGatewayMethod = (RpcGatewayMethod) ctMethod.getAnnotation(RpcGatewayMethod.class);
        System.out.println("RpcGatewayMethod.methodName:" + rpcGatewayMethod.methodName());
        System.out.println("RpcGatewayMethod.methodDesc:" + rpcGatewayMethod.methodDesc());

        // 获取指令码
        MethodInfo methodInfo = ctMethod.getMethodInfo();
        CodeAttribute codeAttribute = methodInfo.getCodeAttribute();
        CodeIterator iterator = codeAttribute.iterator();
        while (iterator.hasNext()) {
            int idx = iterator.next();
            int code = iterator.byteAt(idx);
            System.out.println("指令码:" + idx + " > " + Mnemonic.OPCODE[code]);
        }

        // 通过指令码改写方法
        ConstPool cp = methodInfo.getConstPool();
        Bytecode bytecode = new Bytecode(cp);
        // addDconst,将 double 型0推送至栈顶
        bytecode.addDconst(0);
        // addReturn,返回 double 类型的结果
        bytecode.addReturn(CtClass.doubleType);
        methodInfo.setCodeAttribute(bytecode.toCodeAttribute());

        // 输出字节码
        ctClass.writeFile();

    }

}

此时的方法的返回值已经被修改

@RpcGatewayClazz(
    clazzDesc = "用户信息查询服务",
    alias = "api",
    timeOut = 500L
)
public class ApiTest {
    public ApiTest() {
    }

    @RpcGatewayMethod(
        methodDesc = "查询息费",
        methodName = "interestFee"
    )
    public double queryInterestFee(String uId) {
        return 0.0D;
    }
}

可以看到查询息费的返回结果已经是 0.0D。

2、实现目前

使用指令码生成方法

public class HelloWorld {

    public static void main(String[] args) throws Exception {

        ClassPool pool = ClassPool.getDefault();

        // 创建类信息
        CtClass ctClass = pool.makeClass("com/enmalvi/Javassist/Javassist05/HelloWorld");

        // 添加方法
        // 主要是创建方法的时候需要传递;返回类型、方法名称、入参类型,以及最终标记方法的可访问量。
        CtMethod mainMethod = new CtMethod(CtClass.doubleType, "queryInterestFee", new CtClass[]{pool.get(String.class.getName())}, ctClass);
        mainMethod.setModifiers(Modifier.PUBLIC);

        MethodInfo methodInfo = mainMethod.getMethodInfo();

        ConstPool cp = methodInfo.getConstPool();

        // 类添加注解
        // AnnotationsAttribute,创建自定义注解标签
        AnnotationsAttribute clazzAnnotationsAttribute = new AnnotationsAttribute(cp, AnnotationsAttribute.visibleTag);
        // Annotation,创建实际需要的自定义注解,这里需要传递自定义注解的类路径
        Annotation clazzAnnotation = new Annotation("com/enmalvi/Javassist/Javassist05/RpcGatewayClazz", cp);
        // addMemberValue,用于添加自定义注解中的值。需要注意不同类型的值 XxxMemberValue 前缀不一样;StringMemberValue、LongMemberValue
        clazzAnnotation.addMemberValue("clazzDesc", new StringMemberValue("用户信息查询服务", cp));
        clazzAnnotation.addMemberValue("alias", new StringMemberValue("api", cp));
        clazzAnnotation.addMemberValue("timeOut", new LongMemberValue(500L, cp));
        // setAnnotation,最终设置自定义注解。如果不设置,是不能生效的。
        clazzAnnotationsAttribute.setAnnotation(clazzAnnotation);
        ctClass.getClassFile().addAttribute(clazzAnnotationsAttribute);

        // 方法添加注解
        AnnotationsAttribute methodAnnotationsAttribute = new AnnotationsAttribute(cp, AnnotationsAttribute.visibleTag);
        Annotation methodAnnotation = new Annotation("com/enmalvi/Javassist/Javassist05/RpcGatewayMethod", cp);
        methodAnnotation.addMemberValue("methodName", new StringMemberValue("查询息费", cp));
        methodAnnotation.addMemberValue("methodDesc", new StringMemberValue("interestFee", cp));
        methodAnnotationsAttribute.setAnnotation(methodAnnotation);
        // 设置类的注解与设置方法的注解,前面的内容都是一样的。唯独需要注意的是方法的注解,需要设置到方法的;addAttribute 上。
        methodInfo.addAttribute(methodAnnotationsAttribute);

        // 指令控制
        // Javassist 中的指令码通过,Bytecode 的方式进行添加。基本所有的指令你都可以在这里使用,它有非常强大的 API。
        Bytecode bytecode = new Bytecode(cp);
        // addGetstatic,获取指定类的静态域, 并将其压入栈顶
        bytecode.addGetstatic("java/math/BigDecimal", "TEN", "Ljava/math/BigDecimal;");
        // addInvokevirtual,调用实例方法
        bytecode.addInvokevirtual("java/math/BigDecimal", "doubleValue", "()D");
        // addReturn,从当前方法返回double
        bytecode.addReturn(CtClass.doubleType);
        // 最终讲字节码添加到方法中,也就是会变成方法体。

        methodInfo.setCodeAttribute(bytecode.toCodeAttribute());
        // 添加方法
        ctClass.addMethod(mainMethod);
        // 输出类信息到文件夹下
        ctClass.writeFile();

    }

}

生成的结果

@RpcGatewayClazz(
    clazzDesc = "用户信息查询服务",
    alias = "api",
    timeOut = 500L
)
public class HelloWorld {
    @RpcGatewayMethod(
        methodName = "查询息费",
        methodDesc = "interestFee"
    )
    public double queryInterestFee(String var1) {
        return BigDecimal.TEN.doubleValue();
    }

    public HelloWorld() {
    }
}

如本文“对您有用”,欢迎随意打赏作者,让我们坚持创作!

4 打赏
Enamiĝu al vi
不要为明天忧虑.因为明天自有明天的忧虑.一天的难处一天当就够了。
543文章 68评论 294点赞 594555浏览

随机文章
Spring—WebApplicationContext介绍
5年前
SpringSecurity—OAuth 2(九)第三方应用优化
5年前
RESTful风格
5年前
SpringMVC笔记17—拦截器
5年前
SpringCloud—负载均衡器概览
5年前
博客统计
  • 日志总数:543 篇
  • 评论数目:68 条
  • 建站日期:2020-03-06
  • 运行天数:1927 天
  • 标签总数:23 个
  • 最后更新:2024-12-20
Copyright © 2025 网站备案号: 浙ICP备20017730号 身体没有灵魂是死的,信心没有行为也是死的。
主页
页面
  • 归档
  • 摘要
  • 杂图
  • 问题随笔
博主
Enamiĝu al vi
Enamiĝu al vi 管理员
To be, or not to be
543 文章 68 评论 594555 浏览
测试
测试
看板娘
赞赏作者

请通过微信、支付宝 APP 扫一扫

感谢您对作者的支持!

 支付宝 微信支付