Spring Boot+Hibernate实体getter被调用两次的原因与优化方案
Hibernate中Getter重复调用的原因及最佳实践
一、为什么你的Getter会被调用两次?
Hibernate在实体的生命周期里,有两个关键场景会触发属性Getter的调用:
- 快照生成阶段的隐式调用:Hibernate加载实体时,会自动调用Getter获取属性值,生成一份内部"快照"(snapshot),用于后续的脏检查(判断实体是否被修改)。这次调用是Hibernate内部逻辑触发的,属于隐式操作,调试器通常无法捕获到断点,且会先于你的业务代码调用执行。
- 业务代码的显式调用:就是你自己业务逻辑中主动调用
getCredentials()的那次,这是你能调试到的调用。
你遇到的问题核心在于:第一次隐式调用Getter时,decode方法直接修改了this.credentials的引用(因为Map是引用类型),把原本加密的数据库值替换成了解密后的对象,导致第二次调用时拿到的已经是解密后的数据,打破了原本的读写逻辑。
二、最佳实践:将加解密逻辑封装到ORM层
Hibernate的Getter/Setter是为ORM层的属性访问设计的,不适合承载加解密这类业务逻辑。正确的做法是把加解密逻辑嵌入到Hibernate的类型处理体系中,完全和业务代码解耦,同时避免重复调用问题。
方案1:自定义Hibernate Jsonb加密类型
实现UserType接口,将加解密逻辑封装在类型的数据库读写方法中:
public class EncryptedJsonbType implements UserType { private final JsonbType jsonbType = new JsonbType(); @Override public int[] sqlTypes() { return jsonbType.sqlTypes(); } @Override public Class<?> returnedClass() { return Map.class; } @Override public boolean equals(Object x, Object y) { return jsonbType.equals(x, y); } @Override public int hashCode(Object x) { return jsonbType.hashCode(x); } @Override public Object nullSafeGet(ResultSet rs, String[] names, SharedSessionContractImplementor session, Object owner) throws SQLException { // 先读取数据库的jsonb值,再解密 Map<String, Object> encryptedMap = (Map<String, Object>) jsonbType.nullSafeGet(rs, names, session, owner); return decrypt(encryptedMap); } @Override public void nullSafeSet(PreparedStatement st, Object value, int index, SharedSessionContractImplementor session) throws SQLException { // 先加密业务数据,再转成jsonb写入数据库 Map<String, Object> decryptedMap = (Map<String, Object>) value; Map<String, Object> encryptedMap = encrypt(decryptedMap); jsonbType.nullSafeSet(st, encryptedMap, index, session); } // 实现剩余UserType接口方法(略) private Map<String, Object> encrypt(Map<String, Object> data) { // 你的自定义加密逻辑 } private Map<String, Object> decrypt(Map<String, Object> data) { // 你的自定义解密逻辑 } }
然后在实体类中使用这个自定义类型:
@Entity class MyEntity { @Column(name = "credentials", columnDefinition = "jsonb") @Type(type = "com.yourpackage.EncryptedJsonbType") private Map<String, Object> credentials; // 普通Getter/Setter,无需加解密逻辑 public Map<String, Object> getCredentials() { return credentials; } public void setCredentials(Map<String, Object> credentials) { this.credentials = credentials; } }
方案2:使用JPA Attribute Converter(更轻量)
如果加解密逻辑针对整个字段,用JPA的@Converter实现更简单:
@Converter(autoApply = false) public class CredentialsEncryptConverter implements AttributeConverter<Map<String, Object>, String> { private final ObjectMapper objectMapper = new ObjectMapper(); @Override public String convertToDatabaseColumn(Map<String, Object> attribute) { // 1. 将Map转为JSON字符串 2. 加密字符串 try { String json = objectMapper.writeValueAsString(attribute); return encrypt(json); } catch (JsonProcessingException e) { throw new RuntimeException("Failed to serialize credentials", e); } } @Override public Map<String, Object> convertToEntityAttribute(String dbData) { // 1. 解密数据库字符串 2. 转为Map try { String decryptedJson = decrypt(dbData); return objectMapper.readValue(decryptedJson, new TypeReference<Map<String, Object>>() {}); } catch (JsonProcessingException e) { throw new RuntimeException("Failed to deserialize credentials", e); } } private String encrypt(String data) { // 你的加密逻辑 } private String decrypt(String data) { // 你的解密逻辑 } }
实体类中配置转换器:
@Entity class MyEntity { @Column(name = "credentials", columnDefinition = "jsonb") @Convert(converter = CredentialsEncryptConverter.class) private Map<String, Object> credentials; // 普通Getter/Setter public Map<String, Object> getCredentials() { return credentials; } public void setCredentials(Map<String, Object> credentials) { this.credentials = credentials; } }
三、为什么不能在Getter中做加解密?
除了重复调用的问题,还存在这些隐患:
- 脏检查误判:Hibernate的快照基于第一次Getter调用的值,若Getter修改了原字段引用,会导致Hibernate误判实体被修改,触发不必要的数据库更新。
- 线程安全风险:若实体被多线程访问,Getter中的修改逻辑可能引发并发问题。
- 逻辑耦合:业务逻辑与ORM层耦合,不利于代码维护和单元测试。
内容的提问来源于stack exchange,提问作者Taserface
相关产品推荐
相关产品推荐

