实体类字段取值:为什么方法比字段更好用

反射取值听起来是个老生常谈的话题,但实际写起来会遇到不少麻烦。

场景

设计一个通用工具:一边传进来一个实体类(比如 User.class),另一边是可配置的规则,比如 age > 18 或者 name == 'Bob',工具需要从 User 对象里把对应的字段值取出来。

看起来简单,但细分一下,会碰到几个棘手的情况:

  1. 有些字段不能对外暴露,比如魔法值、常量字段,需要在框架层面屏蔽掉。
  2. 有些字段是实时计算的,比如 remainExpertedTime,每次取值都要现算,不能直接读。
  3. 有些字段有并发逻辑,比如 读取计数器,取值的同时还要 getAndIncrement
  4. 有些字段是包装类型,比如 Wrapper<Ref<String>>,上下文其实只需要最里面那个 String

怎么设计才能应对这些情况,同时让后续维护成本低一点?

思路

最直接的想法是用 Field 反射,访问控制那条也好处理,加个注解标记哪些字段可以访问,没标记的一律拒绝。

但到了实时计算、并发控制、类型拆包这几个场景,纯 Field 反射就力不从心了。仔细想想这三个场景的共同点:虽然对外看起来是"取一个值",但背后其实都有一段逻辑要跑,本质上是个方法调用,不是字段读取。

所以思路也就清晰了——把注解打到方法上,用方法的返回值代表"取到的字段值"。这样一个注解就能统一处理所有场景:

@Ctx
public class ContextDto {

    // 不允许访问
    public static final String magicId = "const";

    // 不允许访问
    private int ctxId;

    @Ctx // 标记可访问
    private Integer level;
    
    @Ctx("trace_id") // 自定义别名
    private String serverWrapTraceId;

    @Ctx("remainExpertedTime")
    public long getRemainExpertedTime() {
        // 实时逻辑
    }

    @Ctx("syncInt")
    public synchronized int getSyncInt() {
        // 并发控制逻辑
    }

    private Wrapper<Ref<String>> valWrapper;

    @Ctx("wrappedVal")
    public String getWrappedVal() {
        // 拆包逻辑
        return valWrapper.get().get();
    }
}

还有几个细节要想清楚

基本方向定了,但有几个地方如果不提前约定好,后面会踩坑。

1. 方法名怎么对应字段名?

现在的做法是 @Ctx("name") 显式写字段名,这有个隐患——字符串容易拼错,编译期不报错,运行时规则引擎找不到字段才发现。这个问题靠约定来规避,后面会说。

2. 取值方法必须无参吗?

是的,取值方法不能有入参,计算逻辑只能依赖实体自身的状态,不允许外部 IO。这个约束应该显式写进文档,甚至可以在框架层面做校验。

3. 继承的情况怎么处理?

如果 User 继承了 BaseEntity,父类上也有 @Ctx 方法,要不要往上扫描?往上找的话,继承链深了逻辑会变复杂,而且父类方法被子类 override 之后语义可能已经变了,扫到父类反而容易出问题。

最终约定

  1. 字段可以不写别名,默认用字段名;方法必须显式写 @Ctx("fieldName"),没有默认推导。
  2. 取值方法必须无参,有外部依赖的逻辑由实体自己在方法内封装好。
  3. 只扫描当前类,不往父类找,也不继承到子类——子类自己决定要开放哪些字段。

最后想说的话

其实回头看,这套方案核心就一个转变——把"取字段"升级成"调方法",这一步想通了,后面那些细节都水到渠成了。在现代的开发中,约定大于配置,虽然要做名称约定和行为约定,但只要文档能写好,这些反而不是大问题,能减少不少技术复杂度。