Skip to content

feature: migrate javax to jakarta and Spring Security 6 (Java 21 upgrade, Stage 3 of 4) - #1051

Open
devin-ai-integration[bot] wants to merge 1 commit into
devin/1787850098-spring-boot-3-upgradefrom
devin/1787851635-jakarta-migration
Open

devin-ai-integration[bot] wants to merge 1 commit into
devin/1787850098-spring-boot-3-upgradefrom
devin/1787851635-jakarta-migration

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Aug 27, 2026

Copy link
Copy Markdown

Summary

Stage 3 of 4 of the Java 11 -> 21 upgrade: make the source compile and the tests pass on Spring Boot 3.5.3 / Spring Security 6. Base is Stage 2's devin/1787850098-spring-boot-3-upgrade, where ./gradlew clean build died at :compileJava with ~84 errors.

After this PR: ./gradlew clean build and ./gradlew test are green (68 tests, 0 failures, 0 errors, 0 skipped), and the app boots and serves authenticated REST + GraphQL traffic.

Per Stage 2's decisions, sourceCompatibility/targetCompatibility stay '11', the TargetJvmVersion = 17 resolution attribute stays (Stage 4 removes both), and jjwt stays on 0.11.5.

1. javax -> jakarta (20 source files, 38 imports)

javax.validation.* -> jakarta.validation.*, javax.validation.constraints.* -> jakarta.validation.constraints.*, javax.servlet[.http].* -> jakarta.servlet[.http].*. Imports were migrated file by file, not by a tree-wide sed.

Deliberately left as javax — JDK packages, not Jakarta EE, in infrastructure/service/DefaultJwtService.java:

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

These are java.base/JCE types and have no jakarta equivalent. A blind rename here would not compile. No other javax.* import survives in src/; src/test had none to begin with.

2. Spring Security 6 configuration

WebSecurityConfigurerAdapter is gone in Spring Security 6, so WebSecurityConfig is now a plain @Configuration exposing a SecurityFilterChain bean built with the lambda DSL. The jwtTokenFilter(), passwordEncoder() and corsConfigurationSource() beans are unchanged, and the endpoint matrix is a 1:1 translation — same order, same matchers, same outcomes.

Before:

public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.csrf().disable()
        .cors().and()
        .exceptionHandling().authenticationEntryPoint(new HttpStatusEntryPoint(UNAUTHORIZED)).and()
        .sessionManagement().sessionCreationPolicy(STATELESS).and()
        .authorizeRequests()
        .antMatchers(HttpMethod.OPTIONS).permitAll()
        .antMatchers("/graphiql").permitAll()
        .antMatchers("/graphql").permitAll()
        .antMatchers(GET, "/articles/feed").authenticated()
        .antMatchers(POST, "/users", "/users/login").permitAll()
        .antMatchers(GET, "/articles/**", "/profiles/**", "/tags").permitAll()
        .anyRequest().authenticated();
    http.addFilterBefore(jwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);
  }
}

After:

public class WebSecurityConfig {
  @Bean
  public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http.csrf(csrf -> csrf.disable())
        .cors(Customizer.withDefaults())
        .exceptionHandling(h -> h.authenticationEntryPoint(new HttpStatusEntryPoint(UNAUTHORIZED)))
        .sessionManagement(s -> s.sessionCreationPolicy(STATELESS))
        .authorizeHttpRequests(r -> r
            .requestMatchers(HttpMethod.OPTIONS).permitAll()
            .requestMatchers("/graphiql").permitAll()
            .requestMatchers("/graphql").permitAll()
            .requestMatchers(GET, "/articles/feed").authenticated()
            .requestMatchers(POST, "/users", "/users/login").permitAll()
            .requestMatchers(GET, "/articles/**", "/profiles/**", "/tags").permitAll()
            .anyRequest().authenticated())
        .addFilterBefore(jwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);
    return http.build();
  }
}

Notes:

  • requestMatchers(HttpMethod.OPTIONS) is the method-only overload, matching the old antMatchers(HttpMethod.OPTIONS) (any path, OPTIONS verb).
  • Authentication remains entirely the stateless JwtTokenFilter reading Authorization: Token <jwt>; no AuthenticationManager, UserDetailsService or form/basic login was introduced, and nothing was loosened. Unauthenticated GET /user still returns 401 via HttpStatusEntryPoint.
  • Spring Boot logs its usual generated-development-password line because no UserDetailsService is defined. It is inert here — nothing authenticates against it — so no workaround was added.

3. Other Spring Boot 3 / DGS 9 breakages

  • DGS exception handler: DataFetcherExceptionHandler.onException(...) was replaced by CompletableFuture<DataFetcherExceptionHandlerResult> handleException(...). GraphQLCustomizeExceptionHandler now returns CompletableFuture.completedFuture(...) and delegates to defaultHandler.handleException(...).

  • Relay PageInfo: DGS codegen 8 generates io.spring.graphql.types.PageInfo from the schema, and ArticlesConnection/CommentsConnection builders now require it, so graphql.relay.DefaultPageInfo/DefaultConnectionCursor were dropped in favor of the generated builder:

    PageInfo.newBuilder()
        .startCursor(pager.getStartCursor() == null ? null : pager.getStartCursor().toString())
        .endCursor(pager.getEndCursor() == null ? null : pager.getEndCursor().toString())
        .hasPreviousPage(pager.hasPrevious())
        .hasNextPage(pager.hasNext())
        .build();
  • Spring 6 HttpStatusCode: CustomizeExceptionHandler's overridden handler signatures now take HttpStatusCode instead of HttpStatus.

Two build.gradle changes were unavoidable to get the suite green (everything Stage 2 fenced off is untouched):

  1. codegen { clientCoreConventionsEnabled = false }. The DGS codegen plugin 8.1.0 client-core convention puts graphql-dgs-codegen-shared-core:8.1.0 on implementation, which drags in graphql-dgs-platform-dependencies:10.0.4 and wins conflict resolution against the app's graphql-dgs-spring-boot-starter:9.2.2 — a mixed DGS graph (autoconfig 9.2.2, core 10.0.4). That failed 2 tests at context startup:

    NoClassDefFoundError: com/netflix/graphql/dgs/internal/DefaultDgsQueryExecutor$ReloadSchemaIndicator
      -> Error processing condition on DgsAutoConfiguration.dgsQueryExecutor
    RealworldApplicationTests > contextLoads() FAILED
    ArticleRepositoryTransactionTest > transactional_test() FAILED
    

    Disabling the convention keeps codegen generating sources while leaving the runtime graph aligned on DGS 9.2.2. The app uses only DGS runtime annotations (@DgsComponent, @DgsData, @DgsQuery, @DgsMutation) and generated io.spring.graphql.types, no codegen client-core API, and graphql-dgs-codegen-shared-core is now absent from runtimeClasspath.

  2. Spotless target narrowed from a fileTree(rootDir) with build/generated* excludes to 'src/**/*.java' — the old form tripped Gradle 8 task-input validation on generated build output. Same set of hand-written sources, no formatting policy change. spotlessJavaApply also absorbed the one pre-existing google-java-format drift in DefaultJwtServiceTest that Stage 2 flagged.

Verification

Gradle 8.5, JVM 21.0.11.

  • ./gradlew compileJava compileTestJava — zero errors.
  • ./gradlew spotlessJavaApply run before committing; ./gradlew clean build (which includes spotlessJavaCheck) — BUILD SUCCESSFUL.
  • ./gradlew test68 tests, 0 failures, 0 errors, 0 skipped. No test was deleted, disabled, @Disabled-ed, weakened, or had its assertions changed.

Runtime smoke test (./gradlew bootRun, SQLite/Flyway migrated dev.db)

Register:

POST /users  {"user":{"username":"stage31787852083","email":"stage3.1787852083@[REDACTED SECRET].com","password":"Stage3Password!"}}
201 {"user":{"token":"eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJjYTU5NzQ3Ny0zOGRiLTQ3MDQtODc5ZC0yM2M2NDU2ODJiM2UiLCJleHAiOjE3ODc5Mzg0ODN9.rOcr1zIpIPkAGiym9tC3Z7bN-sG5MX3UvI-F45k6Ljz24gqht2qp6r0WiKvpKvmNfIxd9MXyKtLMjAletzcOtw","email":"stage3.1787852083@[REDACTED SECRET].com","username":"stage31787852083","bio":"","image":"https://static.productionready.io/images/smiley-cyrus.jpg"}}

Login:

POST /users/login  {"user":{"email":"stage3.1787852083@[REDACTED SECRET].com","password":"Stage3Password!"}}
200 {"user":{"token":"eyJhbGciOiJIUzUxMiJ9...cOtw","email":"stage3.1787852083@[REDACTED SECRET].com","username":"stage31787852083","bio":"","image":"https://static.productionready.io/images/smiley-cyrus.jpg"}}

Authenticated current user, and the unauthenticated control:

GET /user   (Authorization: Token <jwt>)
200 {"user":{"token":"eyJhbGciOiJIUzUxMiJ9...cOtw","email":"stage3.1787852083@[REDACTED SECRET].com","username":"stage31787852083","bio":"","image":"https://static.productionready.io/images/smiley-cyrus.jpg"}}

GET /user   (no Authorization header)
401 (empty body)

GraphQL, including the Relay connections that exercise the new generated PageInfo (a second run registered relay1787852226 and created an article, since no test covers the datafetchers):

POST /graphql  {"query":"{ tags }"}
200 {"data":{"tags":[]}}

POST /graphql  (Authorization: Token <jwt>)
{"query":"{ articles(first: 10) { pageInfo { hasNextPage hasPreviousPage startCursor endCursor } edges { cursor node { slug title } } } }"}
200 {"data":{"articles":{"pageInfo":{"hasNextPage":false,"hasPreviousPage":false,"startCursor":"1787852226549","endCursor":"1787852226549"},"edges":[{"cursor":"1787852226549","node":{"slug":"stage-3-relay-verification","title":"Stage 3 Relay Verification"}}]}}}

POST /graphql  (Authorization: Token <jwt>)
{"query":"{ article(slug: \"stage-3-relay-verification\") { comments(first: 10) { pageInfo { hasNextPage hasPreviousPage startCursor endCursor } edges { node { body } } } } }"}
200 {"data":{"article":{"comments":{"pageInfo":{"hasNextPage":false,"hasPreviousPage":false,"startCursor":null,"endCursor":null},"edges":[]}}}}

Remaining failures / handoff notes for Stage 4

No remaining test failures. The suite is fully green, so Stage 4 starts from a green build.

Stage 4 must:

  1. Move sourceCompatibility/targetCompatibility (or a java.toolchain) to 21 and delete the TargetJvmVersion = 17 bridge block in build.gradle — the two go together; leaving the attribute behind once the toolchain is 21 is stale config.
  2. Re-check codegen { clientCoreConventionsEnabled = false } if it touches DGS versions. It is a version-alignment fix, not a Java-version one, so it should stay unless DGS is upgraded to the 10.x line — in which case aligning graphql-dgs-spring-boot-starter to 10.x is the better resolution and the flag can go.
  3. Note that no DGS datafetcher has test coverage. The PageInfo change above was verified only by the manual GraphQL queries in this PR; a toolchain bump won't re-verify it. Worth adding a datafetcher test at some point (out of scope here).
  4. jjwt is still on 0.11.5. The 0.12.x migration (Jwts.parserBuilder(), signWith, setX -> x) remains a separate follow-up, deliberately not bundled into the version upgrade.

Devin-Org: engineering

Link to Devin session: https://app.devin.ai/sessions/76fd1d83a7f7423599ed4dede411dc22
Open in Devin Desktop: https://app.devin.ai/desktop/session/76fd1d83a7f7423599ed4dede411dc22?variant=devin
Requested by: @Azhao15


Note

Devin errored when opening this Pull Request as Azhao15.
As a fallback, Devin opened this PR as itself.


Devin Review

Status Commit
⚪ Not started

Run Devin Review

Devin Review (Staging)
Devin Review

Co-Authored-By: andrew.zhao <andrew.zhao@cognition.ai>
@devin-ai-integration

Copy link
Copy Markdown
Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 potential issues.

Devin Review

Comment thread src/main/java/io/spring/api/security/WebSecurityConfig.java
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants