实体类字段取值:为什么方法比字段更好用
反射取值听起来是个老生常谈的话题,但实际写起来会遇到不少麻烦。
场景
设计一个通用工具:一边传进来一个实体类(比如 User.class),另一边是可配置的规则,比如 age > 18 或者 name == 'Bob',工具需要从 User 对象里把对应的字段值取出来。
看起来简单,但细分一下,会碰到几个棘手的情况:
- 有些字段不能对外暴露,比如魔法值、常量字段,需要在框架层面屏蔽掉。
- 有些字段是实时计算的,比如
remainExpertedTime,每次取值都要现算,不能直接读。 - 有些字段有并发逻辑,比如
读取计数器,取值的同时还要getAndIncrement。 - 有些字段是包装类型,比如
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 之后语义可能已经变了,扫到父类反而容易出问题。
最终约定
- 字段可以不写别名,默认用字段名;方法必须显式写
@Ctx("fieldName"),没有默认推导。 - 取值方法必须无参,有外部依赖的逻辑由实体自己在方法内封装好。
- 只扫描当前类,不往父类找,也不继承到子类——子类自己决定要开放哪些字段。
最后想说的话
其实回头看,这套方案核心就一个转变——把"取字段"升级成"调方法",这一步想通了,后面那些细节都水到渠成了。在现代的开发中,约定大于配置,虽然要做名称约定和行为约定,但只要文档能写好,这些反而不是大问题,能减少不少技术复杂度。