Hibernate UserType存储LocalDate异常:PostgreSQL日期值无效
看起来你遇到了一个挺棘手的间歇性并发问题,像1986-12-363这种无效日期明显是序列化过程中出现了格式错乱。结合你的代码和异步执行场景,我梳理了几个核心问题点和对应的修复方案:
1. UserType的equals方法实现严重错误
你的UserJsonType.equals()直接返回false,这会让Hibernate始终认为该字段的对象已修改——哪怕实际没有任何变化,也会频繁触发更新操作。在异步并发场景下,这种不必要的更新会加剧线程间的竞态条件,大大增加数据错乱的概率。
修复代码:
@Override public boolean equals(Object x, Object y) throws HibernateException { if (x == y) return true; if (x == null || y == null) return false; return x.equals(y); }
2. ObjectMapper重复创建且配置不严谨
每次nullSafeGet和nullSafeSet都新建ObjectMapper,不仅性能低下,还可能因为重复配置导致潜在的序列化不一致。另外,默认配置下没有严格校验日期格式,会让错误的日期值“悄无声息”地被序列化存储。
修复代码:将ObjectMapper改为静态单例,并添加严格的日期校验配置:
public class UserJsonType implements UserType { // 静态单例,确保全局序列化配置一致 private static final ObjectMapper OBJECT_MAPPER; static { OBJECT_MAPPER = new ObjectMapper(); JavaTimeModule timeModule = new JavaTimeModule(); // 显式指定LocalDate的序列化格式,和User类的JsonFormat保持一致 timeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ISO_LOCAL_DATE)); timeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ISO_LOCAL_DATE)); OBJECT_MAPPER.registerModule(timeModule); // 禁用时间戳序列化,强制使用字符串格式 OBJECT_MAPPER.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); // 反序列化时遇到无效日期直接抛出异常,避免生成错误数据 OBJECT_MAPPER.enable(DeserializationFeature.FAIL_ON_INVALID_DATE); // 严格校验未知属性,避免序列化无关数据 OBJECT_MAPPER.enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); } // 修改nullSafeGet方法使用单例OBJECT_MAPPER @Override public Object nullSafeGet(ResultSet rs, String[] names, SharedSessionContractImplementor session, Object owner) throws HibernateException, SQLException { final String cellContent = rs.getString(names[0]); if (cellContent == null) { return null; } try { return OBJECT_MAPPER.readValue(cellContent.getBytes(StandardCharsets.UTF_8), returnedClass()); } catch (final Exception ex) { throw new RuntimeException("Failed to convert String to List<User>: " + ex.getMessage(), ex); } } // 修改nullSafeSet方法使用单例OBJECT_MAPPER @Override public void nullSafeSet(PreparedStatement ps, Object value, int index, SharedSessionContractImplementor session) throws HibernateException, SQLException { if (value == null) { ps.setNull(index, Types.JSON); return; } try { final String json = OBJECT_MAPPER.writeValueAsString(value); ps.setObject(index, json, Types.JSON); } catch (final Exception ex) { throw new RuntimeException("Failed to convert List<User> to String: " + ex.getMessage(), ex); } } // 其他方法保持不变... }
3. SQL类型与PostgreSQL JSON类型不匹配
你当前使用Types.JAVA_OBJECT和Types.OTHER,对于PostgreSQL的JSON/JSONB类型,Hibernate 5.3已经原生支持Types.JSON,这能更好地适配数据库类型,避免序列化/反序列化过程中的类型转换问题。
修复代码:
@Override public int[] sqlTypes() { return new int[]{Types.JSON}; }
同时确保你的PostgreSQL表中users字段的类型是jsonb(推荐,支持索引)或json。
4. 不可变对象的正确性与并发安全
你的ABC类中users是final List<User>,但如果这个List是可变类型(比如ArrayList),即使字段是final,List的内容依然可以被修改。而UserJsonType.isMutable()返回false,会让Hibernate认为该对象是不可变的,不会追踪内部变化,在异步并发修改时会导致数据错乱。
修复方案:
- 确保
users是不可变列表,在ABC的构造方法中转换为不可变实现:
public ABC(List<User> users) { // 使用Collections.unmodifiableList包装,或者Guava的ImmutableList this.users = Collections.unmodifiableList(new ArrayList<>(users)); }
- 如果确实需要修改列表内容,将
UserJsonType.isMutable()改为true,让Hibernate追踪内部变化。
5. 异步操作中的对象发布安全
在CompletableFuture.runAsync()中执行数据库操作时,要确保传入的ABC或User对象已经完全构造完成,避免发布未初始化的对象(比如在构造方法中直接提交异步任务)。未完全构造的对象可能导致线程看到未初始化的字段值,进而序列化出错误的日期。
验证点:确保在调用CompletableFuture.runAsync()之前,所有对象的字段都已经正确初始化完毕。
额外排查建议
- 开启Hibernate的SQL日志,查看序列化后的JSON参数,确认无效日期是在序列化阶段生成的,还是数据库存储阶段出现的。
- 在异步任务中添加异常捕获,记录详细的错误日志(包括对象的状态),帮助定位间歇性问题。
内容的提问来源于stack exchange,提问作者manish

