Java 17迁移后JUnit测试中LocalDateTime精度不匹配问题
问题现象
此前在Java 11 + Docker PostgreSQL 12环境下运行正常的单元测试,迁移至Java 17后大量失败,核心问题为LocalDateTime类型的精度不匹配。测试报错示例:
[ERROR] UnitTest.test1:334
expected: CustomType (3185, 1, Description, 2022-12-01T22:10:27.924910854, 2022-12-01T22:10:27.924951801, user, false)
but was: CustomType (3185, 1, Description, 2022-12-01T22:10:27.924911, 2022-12-01T22:10:27.924952, user, false)
[ERROR] UnitTest.test2:60
expected: CustomType2 (3182, Test, Hello, 2022-12-01T22:10:27.653447384, user, index, null, null, false, null)
but was: CustomType2 (3182, Test, Hello, 2022-12-01T22:10:27.653447, user, index, null, null, false, null)
[ERROR] UnitTest.test3:268
expected: CustomType (3184, 1, Description, 2022-12-01T22:10:27.863401246, 2022-12-01T22:10:27.863438735, user, false)
but was: CustomType (3184, 1, Description, 2022-12-01T22:10:27.863401, 2022-12-01T22:10:27.863439, user, false)
测试逻辑:手动实例化jOOQ自动生成的对象作为预期值,存入PostgreSQL(对应字段类型为timestamp)后取出对比,jOOQ生成的实体类中时间字段为LocalDateTime而非Instant。
单元测试代码
// Unit Test CustomType instance = CustomType.builder() // ...其余字段省略 .lastUpdatedAt(LocalDateTime.now()) .build(); CustomType result = repository.insert(instance); instance.setId(result.getId()); Assertions.assertThat(result).isEqualTo(instance); // AssertJ
Repository代码
// Repository public CustomType insert(CustomType data) { data.setId(null); CustomTypeRecord record = new CustomTypeRecord(); // ...此处使用record无需关注 ;) record.from(data); data = mainDsl.insertInto(CUSTOM_TYPE) .set(record) .returning() .fetchOneInto(CustomType.class); return data; }
已确认:源码无自定义LocalDateTime格式化逻辑,jOOQ已升级至3.17.5,核心依赖如下:
$ ./mvnw dependency:list ... [INFO] --- maven-dependency-plugin:3.3.0:list (default-cli) @ graphql-api --- [INFO] [INFO] The following files have been resolved: [INFO] org.springframework.boot:spring-boot-starter-jooq:jar:2.7.4:compile -- module spring.boot.starter.jooq [auto] [INFO] org.springframework.boot:spring-boot-starter-jdbc:jar:2.7.4:compile -- module spring.boot.starter.jdbc [auto] [INFO] com.zaxxer:HikariCP:jar:4.0.3:compile -- module com.zaxxer.hikari [INFO] org.springframework:spring-jdbc:jar:5.3.23:compile -- module spring.jdbc [auto] [INFO] jakarta.activation:jakarta.activation-api:jar:1.2.2:compile -- module jakarta.activation [INFO] jakarta.xml.bind:jakarta.xml.bind-api:jar:2.3.3:compile -- module java.xml.bind [INFO] org.springframework:spring-tx:jar:5.3.23:compile -- module spring.tx [auto] [INFO] org.springframework:spring-core:jar:5.3.23:compile -- module spring.core [auto] [INFO] org.springframework:spring-jcl:jar:5.3.23:compile -- module spring.jcl [auto] [INFO] org.jooq:jooq:jar:3.17.5:compile -- module org.jooq [INFO] io.r2dbc:r2dbc-spi:jar:0.9.1.RELEASE:compile -- module r2dbc.spi [auto] [INFO] org.reactivestreams:reactive-streams:jar:1.0.4:compile -- module org.reactivestreams [auto] [INFO] org.jooq:jooq-codegen:jar:3.17.5:compile -- module org.jooq.codegen [INFO] org.jooq:jooq-meta:jar:3.17.5:compile -- module org.jooq.meta [INFO] org.jooq:jooq-postgres-extensions:jar:3.17.5:compile -- module org.jooq.postgres.extensions [INFO] org.springframework.boot:spring-boot-starter-web:jar:2.7.4:compile -- module spring.boot.starter.web [auto] ...
问题原因
- Java版本对LocalDateTime精度的处理差异:Java 11中
LocalDateTime.now()默认保留纳秒级精度,但Java 17下,部分JDK实现或底层依赖(如JDBC驱动)与PostgreSQL交互时,会对纳秒精度做截断或四舍五入处理; - PostgreSQL
timestamp类型的精度限制:PostgreSQL的timestamp默认仅支持微秒级(6位小数)存储,而Java的LocalDateTime支持纳秒级(9位小数),存入数据库时会自动截断到微秒,取出后精度与预期值不匹配; - JDBC驱动版本适配问题:Java 17下,旧版PostgreSQL JDBC驱动的精度处理逻辑可能与Java 11存在差异,导致取出的LocalDateTime精度不符合预期。
解决办法
方案1:统一精度,截断预期值到微秒
在创建预期值时,主动将LocalDateTime截断到微秒,匹配数据库存储精度:
CustomType instance = CustomType.builder() // ...其余字段省略 .lastUpdatedAt(LocalDateTime.now().truncatedTo(ChronoUnit.MICROS)) .build();
方案2:调整数据库字段精度(业务允许时)
将PostgreSQL中的timestamp字段改为timestamp(9),支持纳秒级存储,确保存入和取出的精度一致:
ALTER TABLE custom_type ALTER COLUMN last_updated_at TYPE timestamp(9);
方案3:自定义断言逻辑,忽略精度差异
如果无需修改业务代码或数据库,可在断言时忽略LocalDateTime的纳秒部分:
// 方式1:递归比较时忽略时间字段,单独比较精度 Assertions.assertThat(result) .usingRecursiveComparison() .ignoringFields("lastUpdatedAt", "createdAt") // 替换为实际时间字段名 .isEqualTo(instance); Assertions.assertThat(result.getLastUpdatedAt().truncatedTo(ChronoUnit.MICROS)) .isEqualTo(instance.getLastUpdatedAt().truncatedTo(ChronoUnit.MICROS)); // 方式2:直接自定义时间比较规则 Assertions.assertThat(result.getLastUpdatedAt()) .isCloseTo(instance.getLastUpdatedAt(), within(1, ChronoUnit.MICROS));
方案4:升级JDBC驱动至Java 17兼容版本
确认PostgreSQL JDBC驱动版本是否适配Java 17,建议升级到42.5.0及以上的稳定版本,避免版本兼容导致的精度处理差异。
内容的提问来源于stack exchange,提问作者user4695271

