Upgrade to Java 21 + Spring Boot 3.5 (javax→jakarta, Security 6, DGS/MyBatis majors)#175
Upgrade to Java 21 + Spring Boot 3.5 (javax→jakarta, Security 6, DGS/MyBatis majors)#175devin-ai-integration[bot] wants to merge 20 commits into
Conversation
…ase-0c-sqlite-upgrade Bump org.xerial:sqlite-jdbc from 3.36.0.3 to 3.46.0.0
…ase-0d-spotless-jacoco Bump Spotless to 6.25.0 and JaCoCo to 0.8.12
- Migrate all domain entities (Article, Comment) from Joda DateTime to Instant - Rewrite DateTimeCursor to use Instant.ofEpochMilli/toEpochMilli - Rewrite DateTimeHandler (MyBatis TypeHandler) for java.time.Instant - Rewrite JacksonCustomizations serializer for Instant - Update DTOs (ArticleData, CommentData) to use Instant - Update query services (ArticleQueryService, CommentQueryService) - Update GraphQL datafetchers to use DateTimeFormatter - Update all test files to use Instant - Remove joda-time:joda-time:2.10.13 from build.gradle
…ase-0a-joda-to-java-time Replace org.joda.time.DateTime with java.time.Instant across entire codebase
- Bump jjwt-api, jjwt-impl, jjwt-jackson to 0.12.6 in build.gradle
- Rewrite DefaultJwtService for JJWT 0.12.6 API:
- Remove SignatureAlgorithm enum, use Jwts.SIG.HS512
- setSubject() -> subject(), setExpiration() -> expiration()
- signWith(key) -> signWith(key, Jwts.SIG.HS512)
- parserBuilder().setSigningKey().build().parseClaimsJws()
-> parser().verifyWith().build().parseSignedClaims()
- getBody() -> getPayload()
- Extend test secret to 64 bytes (HS512 minimum enforced by 0.12.6)
…ase-0b-jjwt-upgrade Upgrade JJWT from 0.11.2 to 0.12.6
- Update gradle-wrapper.properties to gradle-8.10.2-bin.zip - Regenerate wrapper scripts (gradlew, gradlew.bat) and JAR - Bump Spring Boot plugin 2.6.3 -> 2.7.18 for Gradle 8 compat - Bump dependency-management plugin 1.0.11 -> 1.1.6 - Bump Mockito to 5.14.2 and force ByteBuddy 1.15.4 for Java 21 - Fix Spotless target to avoid implicit task dependencies - Fix Selenium BasePage to use Duration API (Selenium 4.x)
…ase-1-gradle-upgrade Upgrade Gradle wrapper from 7.4 to 8.10.2
…dle 8, DGS/MyBatis)
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
| .updatedAt( | ||
| DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") | ||
| .withZone(ZoneOffset.UTC) | ||
| .format(comment.getCreatedAt())) |
There was a problem hiding this comment.
🟡 buildCommentResult uses getCreatedAt() for updatedAt instead of getUpdatedAt()
In CommentDatafetcher.buildCommentResult(), the updatedAt field is populated using comment.getCreatedAt() instead of comment.getUpdatedAt(). The CommentData class (src/main/java/io/spring/application/data/CommentData.java:19-20) has separate createdAt and updatedAt fields, and the database schema (src/main/resources/db/migration/V1__create_tables.sql:47-48) has distinct created_at and updated_at columns. While this is a pre-existing issue (the old Joda code had the same bug), the PR touched these lines during the migration and carried the bug forward.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Good catch, and confirmed correct — updatedAt should use getUpdatedAt() (as ArticleDatafetcher does). However this is a pre-existing bug on main, not introduced here: the original Joda code was .updatedAt(ISODateTimeFormat.dateTime().withZoneUTC().print(comment.getCreatedAt())). The migration deliberately preserves wire-format/behavior (source-parity), so the joda→java.time rewrite of these lines kept the identical (buggy) behavior rather than silently changing the API response inside a large framework-upgrade PR.
Leaving it as-is to keep this PR behavior-neutral; recommending a separate small follow-up PR to fix getCreatedAt() → getUpdatedAt() here (no test currently locks this — CommentsApiTest constructs CommentData with createdAt in both slots — so a fix should add a test asserting updatedAt != createdAt).
| public static class RealWorldModules extends SimpleModule { | ||
| public RealWorldModules() { | ||
| addSerializer(DateTime.class, new DateTimeSerializer()); | ||
| addSerializer(Instant.class, new InstantSerializer()); | ||
| } | ||
| } |
There was a problem hiding this comment.
🚩 Custom Instant serializer may conflict with auto-configured JavaTimeModule
The old code registered a custom serializer for Joda's DateTime.class, which had no conflict with any auto-configured Jackson module. The new code at src/main/java/io/spring/JacksonCustomizations.java:25 registers a custom serializer for Instant.class, which conflicts with the InstantSerializer from jackson-datatype-jsr310's JavaTimeModule (auto-configured by Spring Boot 3). In practice, Spring Boot registers @Bean modules after auto-discovered ones, so the custom serializer should take precedence and produce the expected yyyy-MM-dd'T'HH:mm:ss.SSS'Z' format. However, this ordering is implicit and could break if module registration order changes in a future Spring Boot version. A more robust approach would be to configure JavaTimeModule's serialization format or explicitly disable JavaTimeModule auto-configuration.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
The custom @Bean Module serializer does take precedence and the wire format is verified by a passing test: ArticleApiTest.should_get_article_success asserts article.createdAt equals DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")...format(time) (ArticleApiTest.java:78-83). That test is green in this PR, so the custom InstantSerializer is winning over JavaTimeModule's default Instant handling and the yyyy-MM-dd'T'HH:mm:ss.SSS'Z' contract is preserved.
Agreed the precedence is implicit. Since behavior is currently correct and test-locked, I'm leaving it for this migration PR; a reasonable future hardening (separate from this upgrade) would be to set the format on JavaTimeModule / JacksonProperties or register the serializer via Jackson2ObjectMapperBuilderCustomizer so the ordering is explicit rather than relying on @Bean-after-autoconfig registration.
- Bump Spring Boot plugin to 3.5.0, dependency-management to 1.1.7 - Bump DGS codegen plugin to 7.0.3, DGS starter to 10.6.0 - Set sourceCompatibility/targetCompatibility to Java 21 - Update mybatis-spring-boot-starter to 3.0.3 - Replace graphql-dgs-spring-boot-starter with graphql-dgs-spring-graphql-starter - Upgrade REST-Assured to 5.4.0, httpclient5 to 5.4.4 - Migrate javax.servlet/javax.validation to jakarta namespace - Rewrite WebSecurityConfig using SecurityFilterChain bean with lambda DSL - Update GraphQL datafetchers for DGS 10.x PageInfo API - Update GraphQLCustomizeExceptionHandler for new DataFetcherExceptionHandler API - Update CustomizeExceptionHandler for Spring 6 HttpStatusCode parameter - Update CI workflows to Java 21 - Add dependency resolution strategy for java-dataloader compatibility
…ase-2-boot3-java21 feat: Upgrade to Spring Boot 3.5.0 and Java 21
…ound Ran ./gradlew dependencies for runtimeClasspath and testRuntimeClasspath and confirmed zero javax.validation:validation-api or other Jakarta-conflicting jars are present. The Spring Boot 3.x BOM correctly resolves all validation deps to jakarta.validation.
…t deps - selenium-java 4.15.0 → 4.21.0 - webdrivermanager 5.6.2 → 5.8.0 - testng 7.8.0 → 7.10.2 - extentreports 5.1.1 → 5.1.2 (latest)
…ase-3c-classpath-verification Phase 3c: classpath verification — no Jakarta-conflicting jars found
…ase-3b-test-deps Phase 3b: bump Selenium, WebDriverManager, TestNG, and ExtentReports test deps
| } | ||
| // Phase 3c classpath verification: confirmed no javax.validation:validation-api | ||
| // or other Jakarta-conflicting jars on runtimeClasspath or testRuntimeClasspath. | ||
| // All validation dependencies resolve to jakarta.validation (Spring Boot 3.x BOM). |
There was a problem hiding this comment.
🚩 Flyway SQLite support may require additional dependency with Spring Boot 3.5
Spring Boot 3.5.0 ships with Flyway 10+ which modularized database-specific support into separate artifacts. The build only declares implementation 'org.flywaydb:flyway-core' at build.gradle:65. If the bundled Flyway version requires org.flywaydb:flyway-database-sqlite for SQLite support, the application would fail at startup. This depends on the exact Flyway version managed by the Spring Boot 3.5.0 BOM — if it's Flyway 10+, SQLite support was moved out of core. This should be verified by running the application or tests against the SQLite database.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Verified — this does not apply to this project's usage. The Boot 3.5 BOM resolves flyway-core to 11.7.2 (no flyway-database-sqlite on the classpath), and SQLite migrations still run successfully:
RealworldApplicationTests.contextLoadsis@SpringBootTestwith the default profile, so it usesspring.datasource.url=jdbc:sqlite:dev.db(file-based) and boots the full context — which triggers Flyway auto-configuration to applyV1__create_tables.sql+V2__seed_data.sqlat startup. That test passes (tests="1" failures="0" errors="0"), exercising the exact app-startup migration path you flagged.- The MyBatis repository tests (
DbTestBase→@MybatisTest,@ActiveProfiles("test"), in-memoryjdbc:sqlite::memory:,spring.flyway.target=1) also apply V1 via Flyway and pass (e.g.ArticleRepositoryTransactionTest,MyBatisArticleRepositoryTest).
So Flyway 11 handles SQLite here without the separate module, and the green @SpringBootTest context-load test will catch any regression if the managed Flyway version ever drops bundled SQLite support. No change needed.
Summary
Upgrades the app from Java 11 + Spring Boot 2.6.3 to Java 21 + Spring Boot 3.5.3, including the framework-major "big bang" (
javax→jakarta, Spring Security 6, graphql-java/DGS/MyBatis majors) and a modernized build/test toolchain. Built up on the integration branch in dependency order — Phase 0 (independent dep pre-work) → Phase 1 (build config) → Phase 2 (framework big bang) → Phase 3 (test/E2E finish) — each phase merged and gated.Verification (Java 21):
./gradlew clean test spotlessCheck→ BUILD SUCCESSFUL, 68 tests, 0 failures, spotless clean. CI runsclean test -x jacocoTestCoverageVerification. ThejacocoTestCoverageVerificationgate (~0.31 instruction ratio) is pre-existing onmainand already excluded from CI; it was left untouched (not weakened). TheseleniumTestE2E smoke suite ran green on real Chrome 137.Build / toolchain
sourceCompatibility/targetCompatibility11 → 21.org.springframework.boot2.6.3 → 3.5.3,io.spring.dependency-management1.0.11 → 1.1.7.gradle.ymlJDK 11 → 21 (zulu).Framework (javax → jakarta, Security 6)
javax.validation.*/javax.servlet.*→jakarta.*across all sources (javax.crypto.*stays JDK). No explicitjavax.validationjar on the classpath, so@Validresolves to the jakarta provider — the HTTP 422 controller tests pass (dual-provider silent-skip pitfall avoided).WebSecurityConfig: droppedWebSecurityConfigurerAdapterfor a@Bean SecurityFilterChainlambda DSL:same route rules;
PasswordEncoder/CorsConfigurationSourcebeans kept.Framework-coupled dependencies
5.0.6→7.0.3; pinnedgraphql-dgs-spring-boot-starter:4.9.21→ DGS platform BOMgraphql-dgs-platform-dependencies:9.2.2+ unversioned (classic) starter. codegen 7 now generates its ownPageInfo, sotypeMapping = ["PageInfo": "graphql.relay.PageInfo"]keeps datafetcher code unchanged. graphql-java 22 changedDataFetcherExceptionHandler.onException(...)→handleException(...)returningCompletableFuture—GraphQLCustomizeExceptionHandlerrewritten accordingly.2.2.2→3.0.4; Flyway BOM-managed (runs the SQLite migrations in tests with no extra module).CustomizeExceptionHandler.handleMethodArgumentNotValid: Spring 6 paramHttpStatus→HttpStatusCode.java.time.Instant; jjwt0.11.2→0.12.6(parser()/verifyWith()/parseSignedClaims,Keys.hmacShaKeyFor); sqlite-jdbc3.46.0.0; spotless6.25.0; jacoco0.8.12.Tests / E2E
4.5.1→5.5.1(all 4 artifacts); removedmockito-inline:4.0.0(Mockito 5 fromspring-boot-starter-testis inline by default);@MockBean→@MockitoBeanacross the 9 API test classes.4.31.0(dropped the misleading explicit4.15.0pin);webdrivermanager5.6.2→5.9.2;testng7.8.0→7.10.2;httpclient5BOM-managed (Boot 3.5HttpClientAutoConfigurationneedsTlsSocketStrategy).seleniumTestsmoke suite passes on Chrome 137.Note
targetnow excludesbuild/**(Gradle 8 strict task validation flags a spotless input overlapping another task's output dir).Link to Devin session: https://partner-workshops.devinenterprise.com/sessions/be5aba3063104b3194133398d225f3fd
Requested by: @mbatchelor81