在Java后端接口开发中,实体之间的关联关系非常常见,例如User与Order、Department与Employee。当使用Jackson把关联对象序列化成JSON返回给前端时,默认会遍历实体中的所有getter方法,把字段值一一输出。如果User实体里含有password、salt、身份证号等敏感字段,这些数据就会随接口响应泄露出去。一个直观的解决办法是给字段加transient关键字,但transient只对Java原生序列化有效,对Jackson无效;另一个办法是使用@JsonIgnore,但它会让字段在反序列化时也无法接收。实际上,Jackson的@JsonProperty注解除了重命名JSON字段,还提供了access属性,可以分别控制字段在序列化和反序列化阶段的可见性,非常适合在关联关系中隐藏敏感数据。

一、@JsonProperty的access属性如何工作
@JsonProperty是Jackson中最常用的注解之一,大多数开发者只使用它的value属性来指定JSON字段名,却忽略了access属性。access属性接收一个JsonProperty.Access枚举值,该枚举包含四个选项:AUTO、READ_ONLY、WRITE_ONLY和READ_WRITE。其中READ_ONLY表示字段只在序列化时输出,反序列化时不能从JSON读取;WRITE_ONLY表示字段只在反序列化时读入,序列化时不会输出;READ_WRITE是默认行为,两个阶段都参与;AUTO则根据getter和setter的可见性自动推断。
对于密码这类敏感数据,我们通常希望前端提交注册或登录请求时能够把password传进来,但查询用户信息时接口响应里不能出现password。WRITE_ONLY正好满足这个需求。它不是简单地把字段从JSON中删除,而是在Jackson的数据绑定过程中阻断序列化输出,同时保留反序列化输入。这一点与直接使用@JsonIgnore完全不同,因为@JsonIgnore会同时关闭两个方向。
需要注意的是,access属性不仅对普通字段生效,对关联对象同样生效。如果某个关联属性标注为WRITE_ONLY,那么反序列化时可以从JSON构造关联对象,但序列化时不会把关联对象展开。这种机制在敏感关联关系中非常有用,可以避免把整个子对象暴露出去。
二、在关联实体中隐藏敏感字段的完整示例
下面构建一个常见的用户订单关联场景。User实体包含id、username、password以及一个订单列表,Order实体通过owner字段反向引用User。目标是:用户信息接口返回User时,password不能出现在JSON中;但创建或更新用户时,password可以正常接收。
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonProperty.Access;
import java.util.List;
public class User {
private Long id;
private String username;
@JsonProperty(access = Access.WRITE_ONLY)
private String password;
private List<Order> orders;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
public List<Order> getOrders() { return orders; }
public void setOrders(List<Order> orders) { this.orders = orders; }
}
Order实体同样定义关联关系,并在owner属性上使用@JsonProperty(access = Access.WRITE_ONLY)或者直接使用@JsonIgnore,具体取决于业务是否需要从JSON中接收owner对象。如果创建订单时前端会提交完整的user对象,那么owner字段可以保持可写;如果只提交userId,则owner字段更适合用@JsonIgnore。
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonProperty.Access;
public class Order {
private Long id;
private String orderNo;
@JsonProperty(access = Access.WRITE_ONLY)
private User owner;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getOrderNo() { return orderNo; }
public void setOrderNo(String orderNo) { this.orderNo = orderNo; }
public User getOwner() { return owner; }
public void setOwner(User owner) { this.owner = owner; }
}
当使用ObjectMapper序列化一个User对象时,password字段不会出现在输出中,但orders列表会正常展开。如果Order实体中的owner字段标注为WRITE_ONLY,序列化Order时也不会出现owner,从而避免双向关联导致的无限递归。不过实际项目中,双向关联通常还会搭配@JsonManagedReference和@JsonBackReference使用,以更清晰地管理序列化方向。
反序列化时,即使JSON中没有password字段,只要实体定义了setter,Jackson也能正常处理其他字段;如果JSON中包含password字段,它会被正常赋值。这样就能保证敏感数据只进不出。需要注意的是,WRITE_ONLY只影响Jackson的序列化行为,并不影响数据库持久化。如果使用JPA或MyBatis等框架,字段依然会按照ORM映射规则写入数据库。
三、与@JsonIgnore、@JsonIgnoreProperties的对比
很多开发者在隐藏字段时首先想到@JsonIgnore,但从上面的分析可以看出,@JsonIgnore的语义是“完全忽略”,既不能序列化也不能反序列化。对于密码字段,如果使用@JsonIgnore,前端提交的password不会被接收,登录或注册接口就会丢失密码数据,需要额外通过其他方式处理。@JsonIgnoreProperties通常标注在类级别,可以一次性忽略多个字段,但它的效果与@JsonIgnore相同,同样是双向忽略。
下表对比了三者的适用场景:
| 注解 | 序列化输出 | 反序列化输入 | 典型场景 |
|---|---|---|---|
| @JsonProperty(access = Access.WRITE_ONLY) | 不输出 | 可接收 | 密码、验证码、支付密钥 |
| @JsonProperty(access = Access.READ_ONLY) | 输出 | 不可接收 | 数据库生成的主键、创建时间 |
| @JsonIgnore | 不输出 | 不可接收 | 完全不需要暴露的临时字段 |
| @JsonIgnoreProperties | 不输出 | 不可接收 | 类级别批量忽略多个字段 |
在关联关系中,如果子对象只需要通过父对象访问,而不需要从JSON反序列化时直接传入,可以使用@JsonIgnore或@JsonIgnoreProperties来忽略关联属性。但如果业务上需要接收关联对象但不想输出,那么@JsonProperty(access = Access.WRITE_ONLY)就是更合适的选择。例如订单创建接口可能允许前端提交完整的user对象,但订单查询接口不应该把user的详细信息再次展开,这时就可以在Order的owner字段上使用WRITE_ONLY。
另外,如果不想在实体类上添加Jackson注解,也可以使用DTO投影方案。在Service层或Controller层将实体转换为专门的响应DTO,DTO中只包含允许暴露的字段。这种方案与注解相比更灵活,但会增加类的数量和维护成本。对于中小型项目,直接在实体字段上使用@JsonProperty的access属性是成本最低的做法。
四、实战中容易踩的坑
第一个坑是Lombok与@JsonProperty同时使用时的字段名冲突。Lombok会根据字段名自动生成getter和setter,如果@JsonProperty同时指定了value属性,比如@JsonProperty(value = "pwd", access = Access.WRITE_ONLY),反序列化时前端必须传pwd而不是password。很多开发者因为忽略value属性导致字段接收不到,排查时容易误以为是access属性失效。
第二个坑是敏感字段被日志或toString方法输出。即使Jackson序列化时隐藏了password,如果实体类使用Lombok的@Data注解自动生成toString方法,日志打印对象时仍然会把password打出来。建议对包含敏感字段的实体排除toString输出,或者在日志框架中对敏感字段做脱敏处理。同样,调试时直接打印对象也可能泄露数据。
第三个坑是关联集合懒加载导致的序列化异常。在JPA中,List<Order> orders默认是懒加载的,如果Service层事务已经关闭,Jackson序列化User时访问orders会抛出LazyInitializationException。解决方式通常是在查询时使用JOIN FETCH,或者将关联集合设置为EAGER,或者在DTO投影中处理。这与隐藏敏感数据无关,但在关联关系序列化时经常同时出现。
第四个坑是WRITE_ONLY字段在更新操作中的行为。对于更新场景,如果前端提交的JSON中缺少password字段,Jackson不会修改实体中已有的password值;但如果JSON中显式传了null,password会被设置为null。这可能导致数据库中的密码被意外清空。实际开发中建议在更新用户时先根据id查询已有实体,再手动控制敏感字段的赋值逻辑,或者使用专门的更新DTO来避免这类问题。
第五个坑是Map类型字段的序列化行为。如果实体中有一个Map<String, Object>类型的扩展字段,里面可能包含敏感key,Jackson不会自动对Map内部的值应用@JsonProperty注解。这种情况下需要自定义序列化器,或者在写入Map之前就移除敏感数据。关联关系中的集合字段如果包含敏感对象,同样需要逐个检查对象内部的注解是否生效。
总结来说,在Java关联关系中隐藏敏感数据,@JsonProperty(access = Access.WRITE_ONLY)是一个精准且低成本的方案。它比@JsonIgnore更灵活,既保证了反序列化阶段的数据完整,又切断了序列化阶段的泄露路径。使用时需要注意关联方向、Lombok协作、懒加载以及更新操作中的空值处理,才能真正把敏感数据控制在安全边界内。
Java关联关系隐藏敏感数据@JsonProperty修改时间:2026-10-03 22:41:20