You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

能否使用ByteBuddy/ASM监听对象字段赋值并触发监听器通知?

运行时字节码操纵实现属性变更通知

完全可以通过运行时字节码操纵工具实现这个需求,主流工具包括ByteBuddy、ASM、CGLIB,下面用实际代码演示实现思路。

首先贴出原类代码:

public class MyBean {
   private String name;

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

   public void setName(String name) {
      // 能否在name被赋予新值时收到通知?
      this.name = name; 
   }
}

实现思路

核心是在运行时修改setName方法的字节码,在属性赋值逻辑前后插入监听器通知代码——通常是先获取旧值,执行原赋值操作,再触发监听器的回调方法。

用ByteBuddy实现(API简洁易上手)

ByteBuddy是高层字节码操作库,无需直接处理字节码指令,用Java代码就能完成增强:

  1. 先定义属性变更监听器接口:
public interface PropertyChangeListener {
    void onPropertyChanged(Object bean, String propertyName, Object oldValue, Object newValue);
}
  1. 编写ByteBuddy增强逻辑:
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.matcher.ElementMatchers;

public class BeanEnhancer {
    public static void main(String[] args) throws Exception {
        // 实例化监听器,用Lambda实现回调逻辑
        PropertyChangeListener listener = (bean, propName, oldVal, newVal) -> 
            System.out.printf("属性[%s]变更:旧值=%s,新值=%s%n", propName, oldVal, newVal);

        // 动态修改MyBean的字节码
        Class<?> enhancedBeanClass = new ByteBuddy()
                .redefine(MyBean.class) // 重定义已加载的MyBean类
                .method(ElementMatchers.named("setName")) // 匹配setName方法
                .intercept(MethodDelegation.to(new SetNameInterceptor(listener))) // 用拦截器替换原方法逻辑
                .make()
                .load(MyBean.class.getClassLoader()) // 加载增强后的类
                .getLoaded();

        // 测试增强后的类
        MyBean bean = (MyBean) enhancedBeanClass.getConstructor().newInstance();
        bean.setName("张三");
        bean.setName("李四");
    }

    // 方法拦截器:负责插入通知逻辑
    static class SetNameInterceptor {
        private final PropertyChangeListener listener;

        public SetNameInterceptor(PropertyChangeListener listener) {
            this.listener = listener;
        }

        public void intercept(MyBean bean, String newName) {
            // 先获取旧值
            String oldName = bean.getName();
            // 执行原setName的赋值逻辑
            bean.setName(newName);
            // 触发监听器通知
            listener.onPropertyChanged(bean, "name", oldName, newName);
        }
    }
}

运行这段代码,控制台会输出两次属性变更日志,说明通知逻辑生效。

用ASM实现(底层字节码操作,更灵活)

ASM是底层字节码操作库,需要直接操作JVM指令,适合性能敏感场景:

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

import java.io.FileOutputStream;
import java.io.IOException;

public class AsmBeanEnhancer {
    public static void main(String[] args) throws IOException {
        // 读取原MyBean类的字节码
        ClassReader cr = new ClassReader(MyBean.class.getName());
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

        // 自定义类访问器,修改setName方法
        cr.accept(new ClassVisitor(Opcodes.ASM9, cw) {
            @Override
            public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
                MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
                // 匹配setName方法:方法名是setName,方法描述符为接收String返回void
                if ("setName".equals(name) && "(Ljava/lang/String;)V".equals(desc)) {
                    return new SetNameMethodVisitor(mv);
                }
                return mv;
            }
        }, ClassReader.SKIP_DEBUG);

        // 输出增强后的字节码到文件(也可以直接加载到JVM)
        byte[] enhancedBytes = cw.toByteArray();
        try (FileOutputStream fos = new FileOutputStream("MyBeanEnhanced.class")) {
            fos.write(enhancedBytes);
        }
    }

    // 自定义方法访问器,插入通知逻辑
    static class SetNameMethodVisitor extends MethodVisitor {
        public SetNameMethodVisitor(MethodVisitor mv) {
            super(Opcodes.ASM9, mv);
        }

        @Override
        public void visitCode() {
            // 第一步:获取旧值并保存到局部变量
            mv.visitVarInsn(Opcodes.ALOAD, 0); // 加载this对象
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "MyBean", "getName", "()Ljava/lang/String;", false); // 调用getName
            mv.visitVarInsn(Opcodes.ASTORE, 2); // 把旧值存到局部变量2

            // 执行原setName方法的所有代码
            super.visitCode();

            // 第二步:发送变更通知
            mv.visitVarInsn(Opcodes.ALOAD, 0); // this
            mv.visitLdcInsn("name"); // 属性名
            mv.visitVarInsn(Opcodes.ALOAD, 2); // 旧值
            mv.visitVarInsn(Opcodes.ALOAD, 1); // 新值(原方法的参数)
            // 调用全局通知器的静态方法(需提前定义该类)
            mv.visitMethodInsn(Opcodes.INVOKESTATIC, "PropertyChangeNotifier", "notify", 
                "(Ljava/lang/Object;Ljava/lang/String;Ljava/lang/Object;Ljava/lang/Object;)V", false);
        }
    }
}

// 全局通知器类
class PropertyChangeNotifier {
    private static PropertyChangeListener listener = 
        (bean, propName, oldVal, newVal) -> 
            System.out.printf("属性[%s]变更:旧值=%s,新值=%s%n", propName, oldVal, newVal);

    public static void notify(Object bean, String propertyName, Object oldValue, Object newValue) {
        listener.onPropertyChanged(bean, propertyName, oldValue, newValue);
    }
}

注意事项

  • 如果MyBean已经被JVM加载,需要使用**重定义(redefine)**模式修改字节码;如果还未加载,可以直接创建增强后的子类或修改原类字节码。
  • 不同工具适用场景不同:ByteBuddy适合快速开发,ASM适合性能敏感场景,CGLIB常用于Spring等框架的动态代理。

内容的提问来源于stack exchange,提问作者lance-java

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 04:20:17