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

Spring Boot+Hibernate实体getter被调用两次的原因与优化方案

Hibernate中Getter重复调用的原因及最佳实践

一、为什么你的Getter会被调用两次?

Hibernate在实体的生命周期里,有两个关键场景会触发属性Getter的调用:

  1. 快照生成阶段的隐式调用:Hibernate加载实体时,会自动调用Getter获取属性值,生成一份内部"快照"(snapshot),用于后续的脏检查(判断实体是否被修改)。这次调用是Hibernate内部逻辑触发的,属于隐式操作,调试器通常无法捕获到断点,且会先于你的业务代码调用执行。
  2. 业务代码的显式调用:就是你自己业务逻辑中主动调用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.20 15:38:26