Skip to content

feature: upgrade to Java 17 and Spring Boot 3.2.5 - #985

Open
amitmanchella-cog wants to merge 3 commits into
base/ankehao-masterfrom
devin/1786386917-java17-spring-boot3
Open

amitmanchella-cog wants to merge 3 commits into
base/ankehao-masterfrom
devin/1786386917-java17-spring-boot3

Conversation

@amitmanchella-cog

@amitmanchella-cog amitmanchella-cog commented Aug 10, 2026

Copy link
Copy Markdown

Summary

Modernizes the stack from Java 11 / Spring Boot 2.6.3 to Java 17 / Spring Boot 3.2.5.

Note on target repo: the source of these changes is ankehao-demo/spring-boot-realworld-[REDACTED SECRET]-app@master, but that repo is not writable from this session (403 on push, and PR creation is rejected). The branch is pushed here instead, with base/ankehao-master (a copy of ankehao-demo master) as the base so the diff is exactly the upgrade.

Non-obvious pieces beyond the version bumps:

  1. DGS codegen would have broken Relay pagination. Codegen 6.x generates its own io.spring.graphql.types.PageInfo, incompatible with the graphql.relay.PageInfo the datafetchers build. Instead of rewriting every datafetcher, the schema type is mapped back in build.gradle:
    tasks.named('generateJava') {
        typeMapping = ["PageInfo": "graphql.relay.PageInfo"]
    }
  2. DataFetcherExceptionHandler is now async: onException(params) -> DataFetcherExceptionHandlerResult became handleException(params) -> CompletableFuture<...>, so GraphQLCustomizeExceptionHandler wraps results in CompletableFuture.completedFuture(...) and delegates to defaultHandler.handleException(...).
  3. Spring Security 6: WebSecurityConfig no longer extends WebSecurityConfigurerAdapter; configure(HttpSecurity) is replaced by a SecurityFilterChain bean using the lambda DSL, antMatchersrequestMatchers, authorizeRequestsauthorizeHttpRequests. GraphiQL now serves sub-resources, so the permit rule covers /graphiql/** too (without it GraphiQL 401s on its assets).

Other changes:

  • javax.validation.* / javax.servlet.*jakarta.* across 20 files (javax.crypto in DefaultJwtService stays — JDK, not Jakarta EE).
  • CustomizeExceptionHandler.handleMethodArgumentNotValid now takes HttpStatusCode instead of HttpStatus.
  • Dependencies required for Spring Boot 3: mybatis-spring-boot-starter 2.2.2 → 3.0.3, DGS starter 4.9.21 → 8.5.8, DGS codegen plugin 5.0.6 → 6.0.3, rest-assured 4.5.1 → 5.4.0, spotless 6.2.1 → 6.25.0, dependency-management 1.0.11 → 1.1.4, plus jjwt/joda-time/sqlite-jdbc patch bumps.
  • Gradle wrapper 7.4 → 8.7. Under Gradle 8 the Spotless target (fileTree(rootDir)) tripped the implicit-dependency check by scanning build/, so it now excludes build/** and frontend/**.
  • CI workflow and README updated to JDK 17.

Verification

./gradlew clean build passes on JDK 17 (68 tests, 0 failures). Smoke-tested bootRun: GET /tags, user registration, authenticated article creation, GET /articles/feed → 401 unauthenticated, validation errors → 422, POST /graphql {tags}, /graphiql → 200.

Link to Devin session: https://app.devin.ai/sessions/eab1470696714c82a659d661972bb89d
Requested by: @amitmanchella-cog


Devin Review

Status Commit
⚪ Not started

Run Devin Review

💡 Connect your GitHub account to enable automatic code reviews.

Open in Devin Review (Staging)
Open in Devin Review

Co-Authored-By: Amit Manchella <amit.manchella@cognition.ai>
@devin-ai-integration

Copy link
Copy Markdown

🤖 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

Co-Authored-By: Amit Manchella <amit.manchella@cognition.ai>

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Devin Review found 5 potential issues.

Open in Devin Review

Comment thread src/main/java/io/spring/api/security/WebSecurityConfig.java
Comment on lines 26 to 27
@Bean
public JwtTokenFilter jwtTokenFilter() {

@devin-ai-integration devin-ai-integration Bot Aug 10, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📝 Info: JwtTokenFilter bean is still auto-registered in the servlet chain

jwtTokenFilter() remains a @Bean of type Filter, so Spring Boot's servlet auto-registration adds it to the container filter chain in addition to the security chain (addFilterBefore). This was pre-existing, but under Spring Security 6 the chain is now built exclusively from this bean, making the double registration more visible: the filter will populate the SecurityContext for every request (including static/error dispatches) outside the security chain. Consider returning it as a plain new JwtTokenFilter() inside the chain or registering a FilterRegistrationBean with setEnabled(false).

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pre-existing behavior that this upgrade doesn't change, and disabling the auto-registration would alter runtime behavior beyond the scope of the upgrade, so leaving it as-is. Worth a follow-up if the maintainers want the filter confined to the security chain.

Comment on lines +49 to +60
.requestMatchers(HttpMethod.OPTIONS, "/**")
.permitAll()
.requestMatchers("/graphiql", "/graphiql/**")
.permitAll()
.requestMatchers("/graphql")
.permitAll()
.requestMatchers(HttpMethod.GET, "/articles/feed")
.authenticated()
.requestMatchers(HttpMethod.POST, "/users", "/users/login")
.permitAll()
.requestMatchers(HttpMethod.GET, "/articles/**", "/profiles/**", "/tags")
.permitAll()

@devin-ai-integration devin-ai-integration Bot Aug 10, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📝 Info: requestMatchers now resolve as MvcRequestMatcher instead of Ant matchers

Replacing antMatchers with requestMatchers changes matcher resolution: when Spring MVC is on the classpath the rules become MvcRequestMatchers evaluated against the path within the DispatcherServlet mapping. For paths served by static resource handling or by DGS/spring-graphql outside DispatcherServlet mappings (e.g. /graphiql assets), matching semantics can differ subtly from Ant matching. The added /graphiql/** rule covers the documented case, but if additional non-DispatcherServlet endpoints (actuator, static files) are added later they may not match as expected.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines 63 to +69
.path(handlerParameters.getPath())
.extensions(errorsToMap(errors))
.build();
return DataFetcherExceptionHandlerResult.newResult().error(graphqlError).build();
return CompletableFuture.completedFuture(
DataFetcherExceptionHandlerResult.newResult().error(graphqlError).build());
} else {
return defaultHandler.onException(handlerParameters);
return defaultHandler.handleException(handlerParameters);

@devin-ai-integration devin-ai-integration Bot Aug 10, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📝 Info: Delegating to DefaultDataFetcherExceptionHandler.handleException keeps async semantics

The migration from onException to handleException is consistent: both custom branches wrap results with CompletableFuture.completedFuture, and the fallback delegates to defaultHandler.handleException(...) rather than the deprecated onException, which in graphql-java 20+ throws UnsupportedOperationException by default. No blocking .join() was introduced, so the handler stays non-blocking.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread build.gradle
tasks.named('generateJava') {
schemaPaths = ["${projectDir}/src/main/resources/schema"] // List of directories containing schema files
packageName = 'io.spring.graphql' // The package name to use to generate sources
typeMapping = ["PageInfo": "graphql.relay.PageInfo"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔍 PageInfo type mapping relies on graphql.relay.PageInfo field coercion

Mapping the schema PageInfo to graphql.relay.PageInfo keeps the datafetchers compiling, but the schema declares endCursor/startCursor as String while graphql.relay.PageInfo exposes them as ConnectionCursor. Serialization works only because graphql-java's String coercing falls back to String.valueOf(...). This was the pre-upgrade behavior too, but it is worth verifying against the newer graphql-java version bundled with DGS 8, since stricter scalar coercion would surface as runtime serialization errors on paginated GraphQL queries.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Verified against the running app on the upgraded stack — Relay coercion still works with the graphql-java bundled by DGS 8.5.8:

{ articles(first:5) { pageInfo { hasNextPage hasPreviousPage startCursor endCursor } edges { cursor node { slug } } } }
-> {"pageInfo":{"hasNextPage":false,"hasPreviousPage":false,"startCursor":"1786387329360","endCursor":"1786386909054"}, ...}

Co-Authored-By: Amit Manchella <amit.manchella@cognition.ai>

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Devin Review found 3 new potential issues.

Open in Devin Review

Comment on lines +50 to +51
.dispatcherTypeMatchers(DispatcherType.ERROR)
.permitAll()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📝 Info: Permitting ERROR dispatch bypasses authorization for error responses

dispatcherTypeMatchers(DispatcherType.ERROR).permitAll() is required under Spring Security 6 (the security filter is registered for REQUEST, ASYNC and ERROR dispatch, and authorizeHttpRequests would otherwise re-authorize the /error forward and turn 404/422 into 401). Worth confirming that no sensitive content is rendered through the error dispatch path, since any error-dispatched request now skips authorization entirely; with the default BasicErrorController body this is fine.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Confirmed: the app has no custom error controller or /error view, so the ERROR dispatch renders the default BasicErrorController body only.

Comment on lines +52 to +65
.requestMatchers(HttpMethod.OPTIONS, "/**")
.permitAll()
.requestMatchers("/graphiql", "/graphiql/**")
.permitAll()
.requestMatchers("/graphql")
.permitAll()
.requestMatchers(HttpMethod.GET, "/articles/feed")
.authenticated()
.requestMatchers(HttpMethod.POST, "/users", "/users/login")
.permitAll()
.requestMatchers(HttpMethod.GET, "/articles/**", "/profiles/**", "/tags")
.permitAll()
.anyRequest()
.authenticated())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📝 Info: Security rule ordering and matcher semantics preserved in the DSL migration

I verified the rewritten chain is semantically equivalent to the old one: antMatchers(HttpMethod.OPTIONS) (null pattern = any path) maps to requestMatchers(HttpMethod.OPTIONS, "/**"), and the ordering /articles/feed (authenticated) before /articles/** (permitAll) is preserved, so the feed endpoint still requires auth. Note the newly added /graphiql/** permit rule widens the public surface to every GraphiQL sub-path, which is intended per the description but does expose the whole prefix.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread build.gradle
target project.fileTree(project.rootDir) {
include '**/*.java'
exclude 'build/generated/**/*.*', 'build/generated-examples/**/*.*'
exclude 'build/**', 'frontend/**'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📝 Info: Spotless target now skips all Java under build/ and frontend/

The exclude was broadened from the two generated-source directories to build/** and frontend/**. frontend/ currently contains no Java files, so the only practical effect is that DGS-generated sources under build/generated* are no longer formatted (as before) plus any future build outputs — no behavioral risk, but the frontend/** exclusion is currently a no-op.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Right — frontend/** is defensive only. build/** is the load-bearing part: under Gradle 8 scanning build/ made spotlessJava consume processResources output and fail the implicit-dependency check.

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.

1 participant