使用ASM为一个类增加属性工具类

使用ASM可以方便的访问和修改一个类,或者在JVM载入前动态更新一个类也是可以的,如下,

为一个类动态增加一个属性,类Student如下:

java 复制代码
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;
	}
}

使用ASM实现一个类写入属性AddFieldAdapter,如下:

java 复制代码
public class AddFieldAdapter extends ClassAdapter {
    private int accessModifier;
    private String name;
    private String desc;
    private boolean isFieldPresent;
 
    public AddFieldAdapter(ClassVisitor cv, int accessModifier, String name, String desc) {
        super(cv);
        this.accessModifier = accessModifier;
        this.name = name;
        this.desc = desc;
    }
 
    @Override
    public FieldVisitor visitField(int access, String name, String desc,
            String signature, Object value) {
        if (name.equals(this.name)) {
            isFieldPresent = true;
        }
        return cv.visitField(access, name, desc, signature, value);
    }
 
    @Override
    public void visitEnd() {
        if (!isFieldPresent) {
        	// 若属性不存在则写入
            FieldVisitor fv = cv.visitField(accessModifier, name, desc, null, null);
            if (fv != null) {
                fv.visitEnd();
            }
        }
        cv.visitEnd();
    }
} 

实现一个类AddField调用上面的类去添加属性,如下:

java 复制代码
public class AddField {
	private Class<?> clazz;
    private ClassReader cr;
    private ClassWriter cw;
    private ClassAdapter ca;
    private File classFile;
 
    private final static String CLASS_FILE_SUFFIX = ".class";
 
    public AddField(Class<?> clazz) {
        this.clazz = clazz;
    }
 
    public void addPublicField(String fieldName, String fieldDesc) {
        if(cr == null) {
            try {
            	// com.demo2.Student
                cr = new ClassReader(clazz.getCanonicalName());
            } catch (IOException e) {
                e.printStackTrace();
            }
            cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);
        }
        if(ca == null) {
            ca = new AddFieldAdapter(cw, Opcodes.ACC_PUBLIC, fieldName, fieldDesc);
        } else {
            ca = new AddFieldAdapter(ca, Opcodes.ACC_PUBLIC, fieldName, fieldDesc);
        }
    }
 
    public void writeByteCode() {
        cr.accept(ca, ClassReader.SKIP_DEBUG);
        byte[] bys = cw.toByteArray();
        OutputStream os = null;
        try {
            os = new FileOutputStream(getFile());
            os.write(bys);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                os.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
 
    private File getFile() {
        if(classFile == null) {
            StringBuffer sb = new StringBuffer();
            sb.append(clazz.getResource("/"))
                .append(clazz.getCanonicalName().replace(".", File.separator))
                .append(CLASS_FILE_SUFFIX);
            classFile = new File(sb.substring(6));
        }
        return classFile;
    } 


    //测试方法
    public static void main(String[] args) {
        // 为 Student 添加字段
        AddField add = new AddField(Student.class);
        // 添加一个名为 address,类型为 java.lang.String 的 public 字段
        add.addPublicField("address", "Ljava/lang/String;");
        // 再增加一个名为 tel,类型为 int 的 public 方法
        add.addPublicField("tel", "I");
        // 重新生成 .class 文件
        add.writeByteCode();
    }
}

使用反编译工具查看生成的CLASS文件,即可看到动态添加的属性了。


ASM操作类属性

我们看到了ASM中的Core API中使用的是XXXVisitor操作类中的对应部分。本文将展示如何使用ASM中的Core API对类的属性的操作。

首先,我们定义一个类Student,如下:

java 复制代码
public class Student {
	public String name = "Jimmy";
	public String address = "China";
}

这里,我们将属性定义为public类型,目的是为了我们使用反射去调用这个属性,接下来我们要为这个类添加一个int类型的属性,名字叫age。

第一个问题,ASM的Core API允许我们在那些方法中来添加属性?

在ASM的Core API中你要为类添加属性就必须要自己去实现ClassVisitor这个接口,这个接口中的visitInnerClass、visitField、visitMethod和visitEnd方法允许我们进行添加一个类属性操作,其余的方法是不允许的。这里我们依然使用Core API中的ClassAdapter类,我们继承这个类,定义一个去添加属性的类,ClassAdapter实现了ClassVisitor。

第二个问题,我们要在这些方法中写什么样的代码才能添加一个属性?

在使用ASM的Core API添加一个属性时只需要调用一句语句就可以,如下

bash 复制代码
classVisitor.visitField(Opcodes.ACC_PUBLIC, "age", Type.getDescriptor(int.class),  
                null, null);  

第一个参数指定的是这个属性的操作权限,第二个参数是属性名,第三个参数是类型描述,第四个参数是泛型类型,第五个参数是初始化的值,这里需要注意一下的是第五个参数,这个参数只有属性为static时才有效,也就是说只有为static时,这个值才真正会赋值给我们添加的属性上,对于非static属性,它将被忽略。

​​​​​​​我们现在用两种方式来添加一个名字为age的属性:

第一种方式:

用ClassWriter直接创建

java 复制代码
ClassReader cr = new ClassReader(Student.class.getName());
		ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);
		ClassVisitor cv = new AddFieldClassAdapter(cw);
		cr.accept(cv, Opcodes.ASM4);
		byte[] code = cw.toByteArray();
		AddField loader = new AddField();
		Class<?> exampleClass = loader.defineClass(Student.class.getName(), code, 0, code.length);
		Object obj = exampleClass.newInstance();
		System.out.println(exampleClass.getField("name").get(obj));
		System.out.println(exampleClass.getField("age").get(obj));

在这里,如果我们的age没有被添加进去那么(2)这个地方将会报错,看一下结果:

Jimmy

0

第二种方式,写一个adapter过滤器,重写visitEnd方法

java 复制代码
public class AddFieldClassAdapter extends ClassVisitor implements Opcodes{
 
	public AddFieldClassAdapter(ClassVisitor cv) {
		super(Opcodes.ASM4, cv);
	}
	
	
	@Override
	public void visitEnd() {
		cv.visitField(Opcodes.ACC_PUBLIC, "age", Type.getDescriptor(int.class),  
                null, null);
		super.visitEnd();
	}
 
}
java 复制代码
public class AddField extends ClassLoader implements Opcodes{
	public static void main(String args[]) throws IOException, SecurityException, NoSuchFieldException, InstantiationException, IllegalAccessException{
		ClassReader cr = new ClassReader(Student.class.getName());
		ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);
		ClassVisitor cv = new AddFieldClassAdapter(cw);
		cr.accept(cv, Opcodes.ASM4);
 
		byte[] code = cw.toByteArray();
		AddField loader = new AddField();
		Class<?> exampleClass = loader.defineClass(Student.class.getName(), code, 0, code.length);
		Object obj = exampleClass.newInstance();
		System.out.println(exampleClass.getField("name").get(obj));
		System.out.println(exampleClass.getField("age").get(obj));
	}
}

结果如下:

Jimmy

0

和第一种方式返回的结果一一致。

接下来,我们在visitField方法中再次添加age属性,如下:

java 复制代码
public class AddFieldClassAdapter extends ClassVisitor implements Opcodes{
 
	public AddFieldClassAdapter(ClassVisitor cv) {
		super(Opcodes.ASM4, cv);
	}
	
	@Override
	public FieldVisitor visitField(int access, String name, String desc,
            String signature, Object value) {
		cv.visitField(Opcodes.ACC_PUBLIC, "age", Type.getDescriptor(int.class),  
              null, null);
		return super.visitField(access, name, desc, signature, value);
    }
	
 
 
}

这时,我们再次运行测试类,结果如下:

Exception in thread "main" java.lang.ClassFormatError: Duplicate field name&signature in class file field/Student

at java.lang.ClassLoader.defineClass1(Native Method)

at java.lang.ClassLoader.defineClassCond(ClassLoader.java:631)

at java.lang.ClassLoader.defineClass(ClassLoader.java:615)

at java.lang.ClassLoader.defineClass(ClassLoader.java:465)

at field.AddField.main(AddField.java:21)

没有重复的名字,而我们添加的是age也不重复,为什么会报重复属性名错误呢?

原因是,在我们的Transform类中的visitField方法,这个方法会在每次属性被访问时调用,而ASM在对这个类操作时会遍历到每个属性,也就是说有一个属性就会调用一次visitField方法,有两个属性就会调用两次visitField方法,所以当我们原类中有两个属性时visitField方法被调用了两次,因此创建了两个同名的age属性。

为了验证visitField是被多次调用, 我那么可以做一个实验,在visitField()方法中加入System.out.println("name:" + name);把每次调用的属性名称都打印出来。

java 复制代码
public class AddFieldClassAdapter extends ClassVisitor implements Opcodes{
 
	public AddFieldClassAdapter(ClassVisitor cv) {
		super(Opcodes.ASM4, cv);
	}
	
	@Override
	public FieldVisitor visitField(int access, String name, String desc,
            String signature, Object value) {
		System.out.println("name:" + name);
 
		return super.visitField(access, name, desc, signature, value);
    }
	
 
 
}

结果:

name:name

name:address

把Student所有的属性都打印出来,这证明了visitField()是被多次调用的!

相关推荐
Scott9999HH3 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
腻害兔3 小时前
【若依项目-产品经理视角】深度拆解 RuoYi-Vue-Pro 商城模块:从商品管理到交易引擎,50 张表撑起一整套电商系统
java·大数据·vue.js·产品经理·ai编程
码智社4 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海4 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖5 小时前
JDK 26 新特性详解
java·开发语言
马优晨6 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区7 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大8 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai8 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
维天说8 小时前
CLI-Switch 2026年3月版历史设计:Hook、TTY 隔离与 JSON 状态
java·服务器·json