Skip to content

Upgrade to Java 21 and Spring Boot 3.5.16, with tests, CI and Java 21 modernization - #69

Open
devin-ai-integration[bot] wants to merge 12 commits into
masterfrom
devin/1786033413-java21-boot3-base
Open

devin-ai-integration[bot] wants to merge 12 commits into
masterfrom
devin/1786033413-java21-boot3-base

Conversation

@devin-ai-integration

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

Copy link
Copy Markdown

Summary

Brings this app from Spring Boot 2.0.2 / Java 8 to Spring Boot 3.5.16 / Java 21, and — because the repo exists to demonstrate language features — updates what it demonstrates to match the runtime it now runs on.

The upgrade itself was nearly free: the codebase contains no javax.* imports anywhere (no JPA, no Bean Validation, no servlet API, no Security), so the jakarta namespace migration that makes most Boot 2→3 upgrades painful simply doesn't apply here. Changing four lines of pom.xml compiles clean. Everything else in this PR is the work that the upgrade exposed.

Boot 3.5.16, not 4.x, deliberately: Boot 4 is a separate migration (Spring Framework 7, module restructuring, RestTemplate removal) and bundling it with a JDK jump would make any breakage ambiguous.

The app did not start before this PR — twice over

  1. A CommandLineRunner fetched a quote from gturnquist-quoters.cfapps.io, a demo service that no longer exists, and let the ResourceAccessException escape, so the process exited after the web server came up. The fetch was also duplicated across main() and a second runner bean. Now: one call, log-and-continue.
  2. Application declared the RestTemplate @Bean and @Autowired it into itself. Boot 2 tolerated this; Boot 3 rejects it with UnsatisfiedDependencyException: Requested bean is currently in creation. The bean moved out to its own @Configuration class.

Neither is caused by the upgrade, but both had to be fixed for "it runs" to mean anything.

Build

  • pom.xml packaging pomjar. It was pom, so the build never produced a runnable artifact.
  • Dropped spring-boot-properties-migrator (a 2.x migration aid).
  • Maven wrapper 3.3.9 → 3.9.9 (3.3.9 does not run on JDK 21).
  • Deleted the Gradle build. It was triply dead: Gradle 4.6 can't run on JDK 21, compile/testCompile were removed in Gradle 7, and bootJar { baseName } was removed in Boot 3. A build file that cannot build is worse than no build file.
  • Added a GitHub Actions job running ./mvnw clean verify on JDK 21. The repo has ~70 abandoned devin/* upgrade branches and no CI; nothing ever forced a branch to be provably green.
  • target/ and .idea/ were committed despite a .gitignore; they're now untracked (that's the ~2.6k deleted lines).

Tests — the repo had none

41 tests, from zero: full-context startup, MockMvc coverage of every endpoint, TopicService stream/regex/IntStream units, and TimeClient default/static interface methods. TopicService keeps mutable state on a singleton bean, so tests get a fresh instance rather than sharing one, and the file-walking endpoints assert loosely because they depend on the process working directory.

Java 21 modernization

  • All five models are records. JSON field names are unchanged — verified over the wire, not by inspection. Quote/Value are Jackson-deserialized and previously relied on setters; Jackson handles records natively.
  • GET /topic/{unknown-id} returns 404 instead of 500. It was .findFirst().get()NoSuchElementException. Service returns Optional, controller maps the miss:
    return topicService.getTopicWithId(id)
            .map(ResponseEntity::ok)
            .orElseGet(() -> ResponseEntity.notFound().build());
  • Virtual threads enabled (spring.threads.virtual.enabled=true), verified as actually taking effect rather than assumed.
  • RestTemplateRestClient (RestTemplate is in maintenance mode), keeping the log-and-continue behaviour.
  • Text blocks replace the +-concatenated response building in HelloController, with \ continuations so the single-line response bodies stay byte-identical — diffed against live pre-change curl output.
  • Collectors.toList().toList() only where the result isn't mutatedTopicService.topics is added to and removed from, and .toList() is unmodifiable, so a blanket conversion fails at runtime rather than at compile time.
  • Deleted a dead application.properties at the repo root containing the literal line public; it was never on the classpath.
  • README rewritten: it advertised Java 1.8, documented no build commands, pointed at the upstream repo, and omitted three endpoints.

Verification

  • ./mvnw -B clean verify — BUILD SUCCESS, 41 tests, 0 failures, 0 skipped, on JDK 21.
  • App run from the jar and every endpoint exercised over HTTP: /, /topic (GET/POST/PUT/DELETE), /topic/{id}, /topic/sort, /topic/minimum/length/{n}, /datetime, /topic/string/operation, /topic/file/operation.
  • Startup log shows the quote WARN, then the H2 table creation, 4 inserts, and a query returning exactly Josh Bloch + Josh Long — which is the real check on the query(sql, Object[], RowMapper)query(sql, RowMapper, Object...) migration, since a mis-bound ? returns 0 or 4 rows.
  • A separate adversarial pass (see the PR comment) covered malformed bodies, wrong content types, ghost ids, and repeat-request state corruption.

Known deltas, called out rather than buried

  • /topic/file/operation's two grad* file searches now return empty strings. TopicService scans the working directory for files starting with grad, and this PR deletes the Gradle build. The endpoint still returns 200; two of its four demo outputs are permanently blank. Intended consequence of going Maven-only.
  • Customer.toString() output changes format (hand-written → record-generated). It only appears in startup logs.
  • PUT/DELETE on an unknown topic id are still silent 200 no-ops. Out of scope here; the 404 work was scoped to GET.

Composition

Built as five branches merged into this one: the base upgrade, then tests+CI, models→records+404, presentation+README, and runtime config, developed in parallel and reconciled here — the test suite was written against the pre-modernization API, so its Topic accessors, 404 placeholders and RestTemplate bean assertion were updated in the final commit.

Link to Devin session: https://app.devin.ai/sessions/779507d78c0b4947b173a22b8b534c1b


Devin Review

Status Commit
⚪ Not started

Run Devin Review

Open in Devin Review (Staging)

- pom: spring-boot-starter-parent 2.0.2.RELEASE -> 3.5.16, java.version 1.8 -> 21
- pom: packaging pom -> jar so an executable jar is actually produced
- pom: drop spring-boot-properties-migrator (a 1.x -> 2.0 aid), add spring-boot-starter-test
- maven wrapper 3.3.9 -> 3.9.9
- remove the unmaintained Gradle build (Gradle 4.6 cannot run on JDK 21)
- bug: tolerate the unreachable demo quote service instead of failing startup
- move the RestTemplate bean out of Application to avoid a self-injection cycle
- replace the deprecated JdbcTemplate.query(String, Object[], RowMapper) overload
- stop tracking target/ and .idea/ (both already declared in .gitignore)

Co-Authored-By: alex.vyshetsky <alex.vyshetsky@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 Bot and others added 2 commits August 6, 2026 16:26
Co-Authored-By: alex.vyshetsky <alex.vyshetsky@cognition.ai>
…drop stray root properties file

Co-Authored-By: alex.vyshetsky <alex.vyshetsky@cognition.ai>
devin-ai-integration Bot and others added 2 commits August 6, 2026 16:31
…ontroller

- convert Topic, Customer, Greeting, Quote, Value to records (component names
  preserve the existing JSON field names)
- add @FunctionalInterface to CustomPredicate
- use .toList() where the result is only read; leave the mutable topics list alone
- bug: getTopicWithId returns Optional so an unknown id yields 404 instead of 500

Co-Authored-By: alex.vyshetsky <alex.vyshetsky@cognition.ai>
…r Java 21

Co-Authored-By: alex.vyshetsky <alex.vyshetsky@cognition.ai>
… TopicService and TimeClient

Co-Authored-By: alex.vyshetsky <alex.vyshetsky@cognition.ai>
@devin-ai-integration

Copy link
Copy Markdown
Author

Runtime verification — Java 21 / Spring Boot 3.5.16 upgrade

Black-box tested the built jar on localhost:8080 (browser for GETs, curl for mutations + adversarial cases). All assertions passed. Devin session

The crux — the app starts and stays up. ./mvnw -B clean verify → BUILD SUCCESS; app started in 1.116s and was still serving 200 after 3m28s. Zero ERROR lines at startup, zero UnsatisfiedDependencyException, zero APPLICATION FAILED TO START.

TomcatWebServer  : Tomcat started on port 8080 (http) with context path '/'
hello.Application: Started Application in 1.116 seconds
hello.Application: Could not fetch a quote from http://gturnquist-quoters.cfapps.io/api/random ...   <-- WARN, survived
hello.Application: Creating tables
hello.Application: Inserting customer record for John Woo / Jeff Dean / Josh Bloch / Josh Long
hello.Application: Customer{id=3, firstName='Josh', lastName='Bloch'}
hello.Application: Customer{id=4, firstName='Josh', lastName='Long'}

The query returning exactly Bloch + Long confirms the query(sql, Object[], RowMapper)query(sql, RowMapper, Object...) migration binds ? correctly (a bad migration returns 0 or 4 rows).

Endpoints verified (click to collapse)
GET / GET /?name=Devin

Full CRUD lifecycle — POST added a 4th topic, PUT updated in place (count stayed 4), DELETE restored the original 3:

After POST (4 topics) After DELETE (back to 3)

Also passed: /topic (3 topics, all fields), /topic/java, /topic/sort (java→javascript→spring), /topic/minimum/length/4 (excludes java correctly), /datetime, /topic/string/operation, /topic/file/operation.

Adversarial cases
GET  /topic/does-not-exist      -> 500   (known pre-existing defect, see below)
GET  /topic  immediately after  -> 200   <-- app survived
POST /topic  '{"id":'           -> 400, topic count unchanged
POST /topic  text/plain         -> 415
PUT/DELETE /topic/ghost-id      -> 200, silent no-op, count unchanged
GET  /topic x20                 -> 1 distinct payload (no state corruption)
GET  /   x5                     -> ids 3,4,5,6,7 (monotonic)
Two expected deltas (neither is a bug in this PR)
  1. /topic/file/operation's two grad* searches now return emptyTopicService.java:189,211 scan for files starting with grad, and this PR deleted the Gradle build. Endpoint still returns 200; two of its four demo outputs are now permanently blank.
  2. GET /topic/{unknown-id} → HTTP 500 from .findFirst().get() (TopicService.java:39). Pre-existing, fixed separately in feature: convert models to Java records and modernize the topic service layer #71. App recovers fully.

Verdict: behaviourally equivalent to the Java 8 app on every endpoint, with the intended change being the quote fetch going from fatal to log-and-continue.

Caveat worth stating plainly: ./mvnw clean verify reports No tests to run. on this branch, so the above is 100% black-box — the test suite lands in #73. master was not tested side-by-side, so "this fixes a previously-fatal startup" rests on this branch starting cleanly rather than on a before/after comparison.

devin-ai-integration Bot and others added 5 commits August 6, 2026 16:34
The test suite was written in parallel against the pre-modernization API:
- Topic accessors are record components now (getId -> id)
- getTopicWithId returns Optional and an unknown id is a 404, not a 500,
  so the placeholder assertions and their @disabled twins collapse into one
- the RestTemplate bean is a RestClient bean

Co-Authored-By: alex.vyshetsky <alex.vyshetsky@cognition.ai>
@devin-ai-integration devin-ai-integration Bot changed the title Upgrade to Java 21 and Spring Boot 3.5.16 Upgrade to Java 21 and Spring Boot 3.5.16, with tests, CI and Java 21 modernization Aug 6, 2026
The runner cannot resolve actions/checkout or actions/setup-java for this
repository (Service Unavailable at 'Getting action download info'), so the
job failed before running anything. Checkout via git and use the JDK 21
already present on the runner image.

Co-Authored-By: alex.vyshetsky <alex.vyshetsky@cognition.ai>
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