如何实现Java POJO的Setter调用(字段修改)检测,减少样板代码?
需求与现有实现痛点
POJO常被用作Model/DTO,用于数据库的插入或更新操作。不同业务场景下的验证逻辑不同:插入时需要验证并写入所有字段;更新时只需处理已修改的字段。因此,Java中需要能追踪POJO字段是否被修改(即Setter方法是否被调用)的机制。
目前已实现两种方案,但都存在较多样板代码:
- 让POJO继承通用抽象类
AbstractDTO,在Setter中手动调用setAt方法记录修改字段 - 通过反射实现
touch方法,Setter中调用该方法
希望找到能自动调用追踪逻辑、样板代码更少的解决方案。
现有实现代码
抽象类AbstractDTO实现
public abstract class AbstractDTO { private Map<String, Object> modifiedFields = new HashMap<>(); public AbstractDTO() { } @JsonIgnore @XmlTransient protected void setAt(String key, Object value) { this.modifiedFields.put(key, value); } @JsonIgnore @XmlTransient public void resetModifiedFields() { this.modifiedFields.clear(); } @JsonIgnore @XmlTransient public Map<String, Object> getModifiedFields() { return this.modifiedFields; } }
Product实现类
public class Product extends AbstractDTO { private Long productId; public Long getProductId() { return this.productId; } public void setProductId(Long productId) { this.productId = productId; this.setAt("productId", productId); } }
检测修改字段的代码
boolean isModified = pojo.getModifiedFields().containsKey(field.getName());
反射实现的touch方法
public void touch () { StackWalker walker = StackWalker.getInstance(RETAIN_CLASS_REFERENCE); StackWalker.StackFrame frame = walker.walk(frames -> { Iterator<StackWalker.StackFrame> it = frames.iterator(); StackWalker.StackFrame a = it.next(); return it.next(); }); String methodName = frame.getMethodName(); try { String withoutSet = methodName.substring(3); String fieldname = Character.toLowerCase(withoutSet.charAt(0)) + withoutSet.substring(1); Class<?> declaringClass = frame.getDeclaringClass(); Field field = frame.getDeclaringClass().getDeclaredField(fieldname); String getterName = "get" + withoutSet; Method getter = declaringClass.getMethod(getterName); Object value = getter.invoke(this); this.modifiedFields.put(field.getName(), value); } catch (Exception e) { throw new RuntimeException("unable to find field!"); } }
无样板代码的解决方案
1. 基于Lombok扩展:自定义注解+编译期处理
利用Lombok的扩展能力,自定义一个@TrackModified注解,再编写注解处理器,让Lombok生成Setter时自动插入追踪逻辑。这样POJO只需添加两个注解,完全不用手写Setter和追踪调用。
改造后的Product类示例:
@TrackModified @Setter public class Product extends AbstractDTO { private Long productId; // 无需手动编写Setter,Lombok自动生成带追踪逻辑的版本 }
2. 动态代理:运行时拦截Setter
如果POJO是基于接口实现的,可以用动态代理包装对象,拦截所有Setter调用,执行完原方法后自动记录修改。
代理类示例:
public class ModifiedTrackingProxy<T> implements InvocationHandler { private final T target; private final Map<String, Object> modifiedFields = new HashMap<>(); private ModifiedTrackingProxy(T target) { this.target = target; } public static <T> T wrap(T target) { return (T) Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), new ModifiedTrackingProxy<>(target) ); } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Object result = method.invoke(target, args); String methodName = method.getName(); if (methodName.startsWith("set") && method.getParameterCount() == 1) { String fieldName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4); modifiedFields.put(fieldName, args[0]); } return result; } public Map<String, Object> getModifiedFields() { return modifiedFields; } }
使用方式:
Product product = new ProductImpl(); // 假设Product是接口,ProductImpl是实现类 Product trackedProduct = ModifiedTrackingProxy.wrap(product); trackedProduct.setProductId(100L); // 直接获取修改记录 trackedProduct.getModifiedFields();
3. 字节码增强:类加载时修改代码
用ASM或Byte Buddy这类字节码工具,在类加载阶段自动给所有Setter方法插入追踪逻辑,完全无需修改业务代码。
以Byte Buddy为例,编写Java Agent:
public class ModifiedTrackingAgent { public static void premain(String args, Instrumentation inst) { new AgentBuilder.Default() .type(ElementMatchers.isSubtypeOf(AbstractDTO.class)) .transform((builder, typeDesc, classLoader, module) -> builder.method(ElementMatchers.nameStartsWith("set") .and(ElementMatchers.hasParameterCount(1))) .intercept(MethodDelegation.to(TrackingInterceptor.class) .andThen(SuperMethodCall.INSTANCE)) ) .installOn(inst); } public static class TrackingInterceptor { @RuntimeType public static void intercept(@This AbstractDTO dto, @Origin Method method, @Argument(0) Object value) { String methodName = method.getName(); String fieldName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4); dto.setAt(fieldName, value); } } }
启动应用时添加Java Agent参数即可,所有继承AbstractDTO的类的Setter都会自动记录修改,完全无侵入。
内容的提问来源于stack exchange,提问作者funkrusher
相关产品推荐
相关产品推荐

