能否使用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代码就能完成增强:
- 先定义属性变更监听器接口:
public interface PropertyChangeListener { void onPropertyChanged(Object bean, String propertyName, Object oldValue, Object newValue); }
- 编写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
相关产品推荐
相关产品推荐

