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

如何实现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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 00:24:52