Java核心: 使用asm操作字节码

news2024/9/25 19:22:48

在上一篇<Java核心: 注解处理器>中我们提到,通过实现AbstractProcessor,并调用javac -processor能够生成代码来实现特殊逻辑。不过它存在两个明显的问题:

  1. 只能新增源文件来扩展逻辑,无法修改现有的类或方法
  2. 必须有一个单独的编译过程,调用javac -processor或者用Maven的annotationProcessor,不适用于已经编译好的jar

这一篇我们讲解ASM的目的就是解决问题1,它不但能建新类,还能修改已有类,比如为POJO类生成toString方法,为Service类的业务方法提供类似AOP的增强。本文的讲解思路如下

  1. asm的能力在于分析、生成和修改字节码,class文件的结构和字节码对使用和理解asm会有帮助,所以我们从讲解Class文件结构开始
  2. 理解asm对字节码操作建立的抽象模型,核心组件和工作流程
  3. 对类字节码和asm抽象都有概念后,从实战出发,解决3个实际问题:  生成类、生成toString方法、打印方法入参和执行耗时

1. Class文件结构

通过javap -v Account.class能够查看Class文件的详细信息,为了方便查看,我们做了删减,它看起是这样的

整个Class文件的内容包含很多内容,这里我们只列出其中的核心部分

  1. 类信息,包括类的访问标志(public、abstract)、名称、父类、接口、版本(编译.java文件的JDK版本)
  2. 常量池,包括类/方法/字段的引用和名称,以及代码中使用的字面常量等
  3. 类属性,通过类属性提供,如SourceFile表示源文件名称,RuntimeInvisibleAnnotations表示运行时注解、外部引用等
  4. 内部类,通过属性InnerClasses提供
  5. 字段,常量池保存字段引用Fieldref,记录了字段所属的类、字段类型和名称
  6. 方法,常量池保存方法引用Methodref,记录了方法所属的类、名称、参数和描述符等等
  7. 字节码,通过方法引用关联,能找到这个方法内的字节码,本地变量表(LocalVariableTable)、异常表(ExceptionTable)、注解信息(RuntimeVisibleAnnotations)等等

2. ASM工作模式

ASM提供了字节码的分析、生成和修改能力,它的能力当然是基于它对字节码的理解之上构建的。ASM支持两类API,一类是基于事件的(类型XML解析的SAX),一类是基于语法树的(类似于XML的DOM)。事件模型的性能会更好一些,这里我们主要讲解和使用事件模型。事件模型将ASM抽象成3个核心组件,ClassReader、Visitor(ClassVisitor、MethodVisitor等)、ClassWriter,整个字节码的处理过程可以想象成这样一张处理的数据流图,整个处理过程分为3步:

  1. 生成事件流,图中CR节点,表示ClassReader,用于读取类定义解析并触发事件
  2. 过滤和转换,图中空白节点,被抽象为Visitor,常见的有ClassVisitor、MethodVisitor、FieldVisitor等,接收事件触发,过滤/修改事件传递给CW,同时它还支持生成新事件
  3. 终结操作符,图中CW节点,表示ClassWriter,起始ClassWriter也是Visitor接口的实现,不同的是它的visit方法会生成类的字节码,比如调用ClassWriter.visitMethod会在类中新增方法

1. ClassVisitor

要使用asm的事件模型API,我们的核心任务就是定义和提供这个处理流程中的核心组件。我们来看看ClassVisitor的核心接口,它实际上是和Class文件结构对应的,它的抽象是基于Class文件的

1. 类信息 - visit(int version, int access, String name, String signature, String superName, String[] interfaces)

这个ClassReader开始访问某个类的起点,我们来看看每个参数的定义

参数

说明

举例

version

类的版本号,对应Java版本

V1_8,指Java 8的代码

access

访问标志,方法是否public、static、synchronized等等,见Opcodes.ACC_*定义

Opcodes.ACC_PUBLIC

name

类名,包名中的"."换成"/"

com/keyniu/shop/Product

signature

泛型标签名

superName

父类名,没有的话默认父类是Object

java/lang/Object

interfaces

接口名数组

new String[]{"java/io/Serializable"}

下面是我们举例的一组参数,各个参数值大概是长这样的

visit(V1_8, ACC_PUBLIC, "com/keyniu/shop/Product", null, "java/lang/Object", new String[]{"java/io/Serializable"});

2. 源文件 - visitSource(String source, String debug)

用于获取javap -v输出里的SourceFile和SourceDebugExtension,前一个字段是.java文件的名称,后一个是额外的调试信息,比如JSP编译为字节码

参数

说明

举例

source

编译当前class的.java文件名称

Account.java

debug

额外的DEBUG信息

null

下面是我们举例的一组参数

visitSource("Account.java", null);

3. 访问外部类 - visitOuterClass(String owner, String name, String descriptor)

参数

说明

举例

owner

外部类类名,格式com/keyniu/asm/Outer

com/keyniu/asm/Outer

name

外部类简单名,Outer

Outer

descriptor

外部类描述符

Lcom/keyniu/asm/Outer;

下面是我们举例的一组参数

visitOuterClass("com/keyniu/asm/Outer", "Outer", "Lcom/keyniu/asm/Outer;");

4. 访问类上的注解 - visitAnnotation(String descriptor, boolean visible)

参数

说明

举例

descriptor

注解描述符

Lcom/keyniu/asm/ToString;

visible

是否运行时可见,@Retention元注解

true

下面是我们举例的一组参数

visitAnnotation("Lcom/keyniu/asm/ToString;", true);

5. 访问字段 - visitField(int access, String name, String desc, String signature, Object value)

参数

说明

举例

access

访问标志,用于表示是否public、static、synchronized等

Opcodes.ACC_PUBLIC

name

字段名

remain

desc

类型描述符,对象类型(如String返回的是Ljava/lang/String;) 基本类型按预定义

I,大写i,表示int类型

signature

泛型签名

null

value

静态常量字段的初始值

下面是我们举例的一组参数

visitField(ACC_PUBLIC + ACC_FINAL, "remain", "I", null, null);

6. 访问方法 - visitMethod(int access, String name, String descriptor, String signature, String[] exceptions)

参数

说明

举例

access

访问标志,用于表示是否public、static、synchronized等

Opcodes.ACC_PUBLIC

name

方法名

transfer

transfer

格式是(参数类型)返回值类型,具体类型遵循Java里的通用类型描述符

(Ljava/lang/String;I)V

2. MethodVisitor

ClassVisitor有点像设计模式里的抽象工厂,除了处理由ClassReader触发的类读取上的事件,它还需要在访问注解、字段、方法是调用工厂方法创建其他的Visitor实例,涉及Visitor如下:

  1. ModuleVisitor,支持模块/包的读取写入操作
  2. AnnotationVisitor,支持处理类/方法/字段(visitAnnotation)和参数/返回值/异常/泛型类型(visitTypeAnnotation)的注解
  3. MethodVisitor,支持方法的处理
  4. FieldVisitor,支持字段的处理
  5. RecordComponentVisitor,支持record类型的字段处理

3. 实战: 准备工作

学习一大堆理论并不能搞明白如何游泳,有了基本的概念后,现在是时候下水实践一下了。实践之前,我们先准备好实践的材料

  1. 定义业务类Account,后续我们会对Account进行编辑,生成一个toString方法返回所有字段值的拼接;修改transfer方法,打印入参和执行耗时
  2. 注解@ToString,标注了@ToString的类生成toString方法
  3. 注解@Diagnostic,标注了@Diagnositc的方法,打印入参和执行耗时
  4. 自定义类加载器SingleClassClassLoader,将保存在byte[]的字节码加载为Class对象
1.  Account
package com.keyniu.asm;
@ToString
public class Account {
    private int remain = 99;
    @Diagnostic
    public void transfer(String sb, int amount) {
        System.out.println("transfer to " + sb + " amount: " + amount);
        try {
            Thread.sleep((int) (Math.random() * 1000));
        } catch (InterruptedException e) {
        }
    }
}
2. ToString
package com.keyniu.asm;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface ToString {
}
3. Diagnostic
package com.keyniu.asm.diagnostic;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Diagnostic {
}
4. SingleClassClassLoader
public class SingleClassClassLoader extends ClassLoader {
    private String name;
    private byte[] codes;

    public SingleClassClassLoader(String name, byte[] code) {
        this.name = name;
        this.codes = code;
    }

    public Class<?> findClass(String name) throws ClassNotFoundException {
        if (this.name.equals(name)) {
            return defineClass(name, codes, 0, codes.length);
        }
        throw new ClassNotFoundException(name);
    }
}

4. 实战: 新建类

万事俱备,我们开始第一个实战,从0到1的生成一个全新的类,提供默认构造函数。这个过程可以拆解为5步,对应代码里的"步骤x"阅读

  1. 创建ClassWriter,这里必要重要的是参数里的ClassWriter.COMPUTE_FRAMES,表示让asm自动计算局部变量表、操作数栈的大小
  2. 创建SimpleClass类,这一步执行完相当于class SimpleClass已经定义
  3. 使用MethodVisitor创建<init>方法(构造函数),创建字节码调用父类(java.lang.Object)的默认构造函数。这一点和Java代码里不同,ClassWriter不会自动生成默认构造函数
  4. 方法和类创建完成后,需要调用MethodVisitor.visitMaxs、MethodVisitor.visitEnd以及ClassWriter(ClassVisitor)的visitEnd方法,结束写入
  5. 使用自定义类加载器加载字节码,创建并使用对象,当然我们也可以将它写入到.class文件
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {

    ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);                                             // 步骤1
    // 创建Class
    cw.visit(Opcodes.V17, Opcodes.ACC_PUBLIC, "com/keyniu/asm/SimpleClass", null, "java/lang/Object", null);  // 步骤2
    // 创建方法
    MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);                       // 步骤3
    mv.visitVarInsn(Opcodes.ALOAD, 0); // this入栈
    mv.visitMethodInsn(Opcodes.INVOKESPECIAL,"java/lang/Object","<init>","()V",false); // 调用父类<init>
    mv.visitInsn(Opcodes.RETURN); // 退出方法
    mv.visitMaxs(0,0); // 触发计算局部变量表、操作数栈大小
    mv.visitEnd(); // 结束方法写入                                                                             // 步骤4
    cw.visitEnd(); // 结束类写入

    byte[] bs = cw.toByteArray();

    SingleClassClassLoader cl = new SingleClassClassLoader("com.keyniu.asm.SimpleClass", bs);                 // 步骤5
    Class<?> clazz = cl.findClass("com.keyniu.asm.SimpleClass");
    Object instance = clazz.newInstance();

    System.out.println(clazz.getName() + ": " + instance);
}

5. 实战: 生成toString方法

通过asm能够实现类似于lombok的操作,对于注解了@ToString的类自动生成toString方法,这个方法读取每个字段值拼接后返回。实现过程粗略的讲是这样的:

  1. 使用ClassReader读取并解析一个现有的类,触发事件,这里我们用的Account类
  2. 自定义ClassVisitor处理visitAnnotation事件,确认Account类上是否有@ToString注解
  3. 自定义ClassVisitor处理visitField事件,记录Account上所有的字段
  4. 自定义ClassVisitor处理visitEnd事件,使用MethodVisitor创建toString方法,编辑字节码,拼接字段值后返回

我们需要定义自己的ClassVisitor,判断类是否ToString标注,收集类中的字段,创建toString方法,核心操作步骤如下,对应代码的”步骤x"来阅读

  1. 回调visit,记录当前的类名,这里的值是: com/keyniu/asm/Account
  2. 回调visitAnnotation,记录当前类十分有标注@ToString注解
  3. 回调visitField,记录类中的所有字段和描述符
  4. 回调visitEnd,在类遍历结束时,根据之前收集的信息,是否注解@ToString、类名、字段信息,生成toString方法的定义和字节码
    1. 创建StringBuilder,用于拼接toString的结果
    2. 拼类名和左括号,Account(
    3. 拼字段值,多个字段直接用","分隔
    4. 拼右括号,StringBuilder最终值的格式是:   Account(字段值1,字段值2)
  5. 调用StringBuilder.toString,将这个结果作为方法返回值返回
package com.keyniu.asm.lombok;

import org.objectweb.asm.*;

import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;

public class ToStringClassVisitor extends ClassVisitor {

    private boolean isAnnotated = false;
    private Map<String, String> fields = new LinkedHashMap<>();
    private String className;

    protected ToStringClassVisitor(ClassVisitor classVisitor) { // classVisitor接收一个ClassWriter用于生成字节码
        super(Opcodes.ASM9, classVisitor);
    }
    
    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        this.className = name;  // 步骤1,记录类名,格式com/keyniu/asm/Account

        super.visit(version, access, name, signature, superName, interfaces);
    }



    @Override
    public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
        if ("Lcom/keyniu/asm/lombok/ToString;".equals(descriptor)) {
            isAnnotated = true; // 步骤2,记录是否有标记@ToString注解
        }
        return super.visitAnnotation(descriptor, visible);
    }

    @Override
    public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
        fields.put(name, descriptor); // 步骤3,记录所有字段信息
        return super.visitField(access, name, descriptor, signature, value);
    }

    @Override
    public void visitEnd() {
        if (isAnnotated && fields.size() > 0) { // 步骤4,创建toString方法
            MethodVisitor mv = cv.visitMethod(Opcodes.ACC_PUBLIC, "toString", "()Ljava/lang/String;", null, null);
            mv.visitCode();
            // 步骤4.a 创建StringBuilder
            mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder");
            mv.visitInsn(Opcodes.DUP);
            mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);

            // 步骤4.b 拼接类名,执行完后StringBuilder="Account("
            mv.visitLdcInsn(className.substring(className.lastIndexOf("/") + 1) + "(");
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);

            Iterator<Map.Entry<String, String>> it = fields.entrySet().iterator();
            while (it.hasNext()) {
                Map.Entry<String, String> kv = it.next();
                String fieldName = kv.getKey();
                String fieldDesc = kv.getValue();
                mv.visitVarInsn(Opcodes.ALOAD, 0); // 载入this
                mv.visitFieldInsn(Opcodes.GETFIELD, className, fieldName, fieldDesc); // 步骤4.c 加载字段到操作数栈
                // 步骤4.c 拼接字段值到StringBuilder="Account(字段值"
                mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(" + fieldDesc + ")Ljava/lang/StringBuilder;", false);
                if (it.hasNext()) { // 步骤4.c 如果不是最后一个字段,拼接",", StringBuilder="Account(字段值,"
                    mv.visitLdcInsn(",");
                    mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
                }
            }

            // 步骤4.d 拼接右括号, StringBuilder="Account(字段值1,字段值2)"
            mv.visitLdcInsn(")");
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
            // 步骤5,将StringBuilder转为String,使用ARETURN返回             
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);

            mv.visitInsn(Opcodes.ARETURN);
            // 步骤6,计算本地变量表、操作数栈的大小
            mv.visitMaxs(0, 0);
            mv.visitEnd();

        }
        super.visitEnd();
    }
}

ToStringClassVisitor也准备好了之后,剩下要做的就是读取Account类,触发事件调用ToStringClassVisitor,生成字节码,并通过类加载器加载,测试toString方法了

public static void main(String[] args) throws Exception {
    System.out.println(Path.of(""));

    String className = "com.keyniu.asm.Account";
    // 读字节码
    ClassReader cr = new ClassReader(className);
    // 写字节码
    ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES);
    ClassVisitor cv = new ToStringClassVisitor(cw);
    // 跑事件流
    cr.accept(cv, ClassWriter.COMPUTE_FRAMES); // 0表示不计算最大堆栈大小和最大局部变量表大小

    // 获取修改后的字节码并写入到文件
    byte[] transformedBytes = cw.toByteArray();
    SingleClassClassLoader cl = new SingleClassClassLoader(className, transformedBytes);
    Class<?> clazz = cl.findClass(className);

    Object instance = clazz.newInstance();
    Method toString = clazz.getDeclaredMethod("toString");
    System.out.println(toString.invoke(instance));
}

执行main方法查看输出,可以确定我们的实现已经生效,如果在Account里新增一个String字段,值等于"randy",那么输出会自动变成Account(99,randy)

6. 实战: 打印参数和执行耗时

打印参数和执行耗时对于我们排查问题很有帮助。我们定义一个@Diagnostic注解,使用asm对注解了@Diagnostic的方法进行修改,打印入参,记录调用耗时。处理过程可以的分为4步:

  1. 准备测试用类Account和注解Diagnostic
  2. 实现ClassVisitor,覆盖visitMethod方法,目的是注入自己的MethodVisitor实现
  3. 实现MethodVisitor,在visitAnnotation中判断当前方法是否有@Diagnostic注解,在visitCode时打印参数,记录开始执行时间
  4. 实现MethodVisitor,在visitInsn中判断是否是方法执行的最后一条指令(返回或抛异常),是的话计算耗时并打印

第1步是准备测试类,Account和Diagnostic的定义在之前已经给出。第2步是实现自己的ClassVisitor,内部逻辑也相当简单,只需要覆写visitMethod方法,返回我们的DiagnosticMethodVisitor即可。

package com.keyniu.asm.diagnostic;

import org.objectweb.asm.*;

public class DiagnosticClassVisitor extends ClassVisitor {

    protected DiagnosticClassVisitor(ClassVisitor classVisitor) {
        super(Opcodes.ASM9, classVisitor);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
        return new DiagnosticMethodVisitor(name, mv);
    }
}

第3步是实现DiagnosticMethodVisitor,在visitAnnotation时判断方法是否标注@Diagnostic,在visitCode中读取并打印入参,记录方法开始执行的时间(System.currentTimeMillis)到本地变量中

package com.keyniu.asm.diagnostic;

import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

import java.util.UUID;

public class DiagnosticMethodVisitor extends MethodVisitor {

    private boolean isDiagnostic = false;
    private String methodName;
    private String traceId = UUID.randomUUID().toString();

    protected DiagnosticMethodVisitor(String methodName, MethodVisitor methodVisitor) {
        super(Opcodes.ASM9, methodVisitor);
        this.methodName = methodName;
    }

    @Override
    public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
        if ("Lcom/keyniu/asm/diagnostic/Diagnostic;".equals(descriptor)) {  // 判断注解是否为我们感兴趣的@Diagnostic
            isDiagnostic = true;
        }
        return super.visitAnnotation(descriptor, visible);
    }

    @Override
    public void visitCode() {
        if (isDiagnostic) {
            mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");   // 将System.out放入操作数栈,后续会调用out.println

            mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder");                                   // 创建StringBuilder,并调用构造函数<init>

            mv.visitInsn(Opcodes.DUP);
            mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
            
   
            mv.visitLdcInsn("traceId: " + traceId + " call <" + this.methodName + "> with params: ");   // 使用ldc指令,放入字符常量,打印参数的前置
            // 弹出栈顶的两个元素(StringBuilder的引用、要拼接的参数)调用append方法,将返回值(StringBuilder自己)压入栈顶,后续类似命令不再解释,参照这里
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
            
            mv.visitVarInsn(Opcodes.ALOAD, 1); // 加载index=1的值(第1个参数), index=0是this引用
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
            
            mv.visitLdcInsn(" , "); // 插入分隔符
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);

            mv.visitVarInsn(Opcodes.ILOAD, 2); // 加载index=2的值(第2个参数)
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(I)Ljava/lang/StringBuilder;", false);
            // 将StringBuilder转为String,压入栈顶,为输出做好准备                                       
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
            // 输出内容
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
            // 获取当前时间,记录到本地变量
            mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J");
            mv.visitVarInsn(Opcodes.LSTORE, 4);
            mv.visitMaxs(0, 0);
        }
        super.visitCode();
    }

}

第4步是在方法结束前,重新取一个当前时间,减去开始时间,就是方法的执行时间并打印。在visitInst回调,我们能取得方法中的每个指令,在方法返回(RETURN)或抛异常(ATHROW)前,正是插入这段逻辑的合适位置。



public class DiagnosticMethodVisitor extends MethodVisitor {

    ...
    @Override
    public void visitInsn(int opcode) {
        if (isDiagnostic) {
            if ((Opcodes.IRETURN <= opcode && opcode <= Opcodes.RETURN) || Opcodes.ATHROW == opcode) { // 方法返回之前
                mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
                mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J");
                mv.visitVarInsn(Opcodes.LLOAD, 4);
                mv.visitInsn(Opcodes.LSUB);

                mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
                mv.visitLdcInsn("traceId: " + traceId + " call <" + this.methodName + "> timeCost: ");
                mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "print", "(Ljava/lang/String;)V", false);
                mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(J)V", false);
            }
        }
        super.visitInsn(opcode);
    }

}

到这里整个Diagnostic工具已经开发完成了,下面我们创建一段测试代码,来看看怎么用,是否能达成预期的效果

package com.keyniu.asm.diagnostic;

import com.keyniu.asm.utils.SingleClassClassLoader;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.util.TraceClassVisitor;

import java.io.PrintWriter;
import java.lang.reflect.Method;
import java.nio.file.Path;

public class DiagnosticMain {

    public static void main(String[] args) throws Exception {
        System.out.println(Path.of(""));

        String className = "com.keyniu.asm.Account";
        // 读字节码
        ClassReader cr = new ClassReader(className);
        // 写字节码
        ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES);
        ClassVisitor cv = new TraceClassVisitor(new DiagnosticClassVisitor(cw), new PrintWriter(System.out));
        // 跑事件流
        cr.accept(cv, ClassWriter.COMPUTE_FRAMES); // 0表示不计算最大堆栈大小和最大局部变量表大小

        // 获取修改后的字节码并写入到文件
        byte[] transformedBytes = cw.toByteArray();
        SingleClassClassLoader cl = new SingleClassClassLoader(className, transformedBytes);
        Class<?> clazz = cl.findClass(className);

        Object instance = clazz.newInstance();
        Method toString = clazz.getDeclaredMethod("transfer", String.class, int.class);
        System.out.println(toString.invoke(instance, "randy", 9));
    }

}

看输出我们确定想要的目标已经实现了。额外要提一下的是这里我们用TraceClassVisitor包装了DiagnosticClassVisitor目的是打印最终的字节码(如下图),并不影响实际的执行。

A. 参考资料

  1. The Class File Format,Chapter 4. The class File Format
  2. JMV Instruction Set,Chapter 6. The Java Virtual Machine Instruction Set
  3. ASM Guide,https://asm.ow2.io/asm4-guide.pdf
     

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1705866.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Sql Server】随机查询一条表记录,并重重温回顾下自定义函数的封装和使用

大家好&#xff0c;我是全栈小5&#xff0c;欢迎来到《小5讲堂》。 这是《Sql Server》系列文章&#xff0c;每篇文章将以博主理解的角度展开讲解。 温馨提示&#xff1a;博主能力有限&#xff0c;理解水平有限&#xff0c;若有不对之处望指正&#xff01; 目录 前言随机查询语…

首发AI新功能 贝锐向日葵远程控制IT精英版助力IT运维

从1947年晶体管发明开始&#xff0c;以计算机为核心载体的IT技术狂奔发展至今&#xff0c;这期间IT运维方式也在不断的发展变化。 我们经历了50s-70s的肉身更换电子管&#xff1b; 经历了80s-90s的本地软件运维&#xff1b; 经历了90s-00s互联网的澎湃发展&#xff0c;Telne…

VMware vSphere Distributed Services Engine 和利用 DPU 实现网络加速

VMware相关学习专栏&#xff1a;虚拟化技术 vSphere 8.0 通过加速数据处理单元 (DPU) 上的网络功能实现了突破性的工作负载性能。 vSphere 8.0 通过加速 DPU 上的网络功能实现了突破性工作负载性能&#xff0c;从而满足现代分布式工作负载的吞吐量和延迟需求。借助 vSphere Dis…

【C++题解】1133. 字符串的反码

问题&#xff1a;1133. 字符串的反码 类型&#xff1a;字符串 题目描述&#xff1a; 一个二进制数&#xff0c;将其每一位取反&#xff0c;称之为这个数的反码。下面我们定义一个字符的反码。 如果这是一个小写字符&#xff0c;则它和字符 a 的距离与它的反码和字符 z 的距离…

godot4.2 + GDextension c++在 vs code 中断点调试配置

游戏开发中如果做不到自己编写的代码做断点调试&#xff0c;无不是瞎子摸象&#xff0c;特别是C这么底层的语言。这2天开始在VS studio中折腾&#xff0c;一直折腾不出结果&#xff0c;几次想要放弃GODOT。最终今天在VS code中搞定了这断点调试C代码。 在上一篇文章我已经做好了…

windows部署ollama+maxkb+vscode插件continue打造本地AI

windows部署ollamamaxkbvscode插件continue打造本地AI 前言下载ollamadocker desktopvscode插件continue 安装安装ollama设置环境变量 安装docker desktop部署maxkb容器 安装vscode插件模型搜索和推荐 前言 我采用docker运行maxkb&#xff0c;本地运行ollama形式。可能是windo…

深度神经网络——贝叶斯与朴素贝叶斯定理

概述 贝叶斯定理是概率论中一个非常重要的概念&#xff0c;它提供了一种在已知某些相关事件的概率时&#xff0c;计算另一个事件发生概率的方法。在你提供的内容中&#xff0c;贝叶斯定理被描述为一种“魔法”&#xff0c;因为它能够使计算机通过分析大量的数据来预测人们可能…

今日好料推荐(Altium Designer + 仿真器驱动)

今日好料推荐&#xff08;Altium Designer 仿真器驱动&#xff09; 参考资料在文末获取&#xff0c;关注我&#xff0c;获取优质资源。 Altium Designer Altium Designer 是一种高度集成的电子设计自动化 (EDA) 软件工具&#xff0c;广泛应用于电子电路和印刷电路板 (PCB) …

汇编原理(三)编程

源程序&#xff1a; 汇编指令&#xff1a;有对应的机器码与其对应 伪指令&#xff1a;无对应的机器码&#xff0c;是由编译器来执行的指令&#xff0c;编译器根据伪指令来进行相关的编译工作。 ex1:XXX segment、XXX ends这两个是一对成对使用的伪指令&#xff0c;且必须会被用…

充电器快充协议与PW6606快充电压诱骗芯片

随着科技的快速发展&#xff0c;手机、平板等电子产品对于充电速度的要求越来越高&#xff0c;快充技术应运而生。在市场上&#xff0c;我们常见的快充充电器主要分为两种类型&#xff1a;A口充电器&#xff08;USB口&#xff09;和TYPE C口充电器。每种类型都拥有其独特的快充…

埃文科技携数据要素产品亮相第七届数字中国建设峰会

第七届数字中国建设峰会&#xff08;以下简称“峰会”&#xff09;于2024年5月24日至25日在福建省福州市举办。此次峰会是国家数据工作体系优化调整后举办的首次数字中国建设峰会。本届峰会由国家发展改革委、国家数据局、国家网信办、科技部、国务院国资委、福建省人民政府共同…

10分钟就可以用AI制作绘本小故事?自媒体涨粉变现利器,实战练习,轻松上手!

大家好&#xff0c;我是向阳 关于最近有老铁私信我如何使用AI制作儿童画册进行AI变现&#xff0c;由于需求的朋友不少&#xff0c;那么本次我将从头到尾将整个创作过程简单和各位介绍下&#xff0c;也欢迎各位老铁一些学习交流。 本次制作过程中所有工具均不需要付费也无需魔…

YOLOv10涨点改进:如何魔改注意力进行二次创新,高效替换PSA | NEU-DET为案列进行展开

&#x1f4a1;&#x1f4a1;&#x1f4a1;本文改进&#xff1a;替换YOLOv10中的PSA进行二次创新&#xff0c;1&#xff09;EMA替换 PSA中的多头自注意力模块MHSA注意力&#xff1b;2&#xff09; EMA直接替换 PSA&#xff1b; 在NEU-DET案列进行可行性验证&#xff0c;1&#x…

Laravel 图片添加水印

和这个配合使用 Laravel ThinkPhP 海报生成_laravel 制作海报-CSDN博客 代码 //水印 $x_length $imageInfo[0]; $y_length $imageInfo[1];$color imagecolorallocatealpha($posterImage, 255, 255, 255, 70); // 增加透明度参数alpha$font_size 40; //字体大小 $angle …

RAG架构的数据准备流程

虽然现成的大型语言模型 (LLM) 功能强大&#xff0c;但企业发现&#xff0c;根据其专有数据定制 LLM 可以释放更大的潜力。检索增强生成 (RAG) 已成为这种定制的主要方法之一。RAG 模型将大型语言模型强大的语言理解能力与检索组件相结合&#xff0c;使其能够从外部数据源收集相…

螺旋矩阵(算法题)

文章目录 螺旋矩阵解题思路 螺旋矩阵 给你一个正整数 n &#xff0c;生成一个包含 1 到 n2 所有元素&#xff0c;且元素按顺时针顺序螺旋排列的 n x n 正方形矩阵 matrix 。 示例 1&#xff1a; 输入&#xff1a;n 3 输出&#xff1a;[[1,2,3],[8,9,4],[7,6,5]]解题思路 模…

全局数据 与 singleton 类的选择

1&#xff0c;singleton 相对于全局数据的优势 使用 Singleton 类相对于全局数据具有以下好处&#xff1a; 1.1. 延迟初始化&#xff1a;Singleton 类可以实现延迟初始化&#xff0c;即在需要时才创建实例&#xff0c;而全局数据在程序启动时就会被初始化。这可以节省资源并提…

SpringBoot——整合RabbitMQ收发消息

目录 RabbitMQ消息队列 项目总结 新建一个SpringBoot项目 pom.xml application.properties配置文件 index.html前端页面 RabbitMQConfig配置类 RabbitMQProducer生产者 RabbitMQConsumer消费者 IndexController控制器 SpringbootRabbitmqApplication启动类 测试 Ra…

CSS 介绍及用法,常用属性

一、CSS介绍 A. 简介 CSS全称&#xff1a;全称为层叠样式表&#xff08;Cascading Style Sheets&#xff09;&#xff0c;是一种用于描述网页外观和格式的计算机语言。CSS可以使网页的布局更加丰富和多样化&#xff0c;并且可以将样式信息与网页内容分离&#xff0c;使得网…

【JAVASE】接口(上)

一&#xff1a;接口的概念 在现实生活中&#xff0c;接口的例子比比皆是&#xff0c;比如&#xff1a;笔记本上上的USB接口。 电脑上的USB口上可以插:U盘、鼠标、键盘等。 电源插座插孔上可以插入&#xff1a;电脑、电视机等。 通过以上例子可以看出&#xff1a;接口就是公共…