Skip to content

Upgrade build to Java 17 (Spring Boot 2.7.18, Gradle 7.6.4) - #62

Open
tobydrinkall wants to merge 1 commit into
masterfrom
devin/1785431232-java17-upgrade
Open

tobydrinkall wants to merge 1 commit into
masterfrom
devin/1785431232-java17-upgrade

Conversation

@tobydrinkall

@tobydrinkall tobydrinkall commented Jul 30, 2026

Copy link
Copy Markdown

Summary

Moves both build systems from Java 8 to Java 17. No source changes were needed — the Java 8 idioms in this repo (Streams, Files.walk/Files.find, java.time, AtomicLong) all compile unchanged on 17.

Maven (pom.xml)

  • spring-boot-starter-parent 2.0.2.RELEASE → 2.7.18 (last 2.x line; Java 17 compatible and still javax.*, so no Jakarta migration).
  • <java.version>1.8</java.version>17, plus an explicit <maven.compiler.release>17</maven.compiler.release> so the bootclasspath is pinned rather than just source/target. Verified: mvn help:evaluate -Dexpression=maven.compiler.release17, and emitted classes are class-file major version 61.
  • <packaging>pom</packaging>jar. With pom packaging Maven never ran compiler:compile on the 13 sources here, so mvn verify was green while validating nothing about the Java level (and produced no artifact despite spring-boot-maven-plugin being configured). Flagging this since it goes slightly beyond a pure version bump — happy to revert if pom packaging was deliberate.
  • spring-boot-properties-migrator kept: it resolves on 2.7.18 and the app starts cleanly with it (it reports no deprecated properties; the only property file in the repo is a fully commented-out server.port).

Gradle (build.gradle)

  • spring-boot-gradle-plugin 2.0.2.RELEASE → 2.7.18.
  • sourceCompatibility/targetCompatibility = 1.8 → a toolchain:
    java { toolchain { languageVersion = JavaLanguageVersion.of(17) } }
  • Removed configurations migrated: compileimplementation, testCompiletestImplementation.
  • bootJar { baseName / version }archiveBaseName / archiveVersion (the old properties were removed in the Boot 2.x plugin).
  • Added spring-boot-starter-jdbc + com.h2database:h2, which the Gradle build was missing entirely. Without them compileJava fails on JdbcTemplate in Application.java — the Gradle build was broken before this change, independent of the Java level. This just brings it in line with pom.xml.

Wrappers

  • Gradle wrapper 4.6 → 7.6.4 (4.6 cannot run on JDK 17); wrapper jar/scripts regenerated via ./gradlew wrapper.
  • Maven wrapper distributionUrl 3.3.9 → 3.9.6 for the same reason. mvnw/gradlew were also missing the executable bit.

Build / test evidence

Both run on openjdk 17.0.13 (Maven 3.6.3, Java version: 17.0.13):

$ mvn clean verify
[INFO] Compiling 13 source files to /home/ubuntu/repos/springboot-java8/target/classes
[INFO] Building jar: target/gs-spring-boot-0.1.0.jar
[INFO] --- spring-boot-maven-plugin:2.7.18:repackage (repackage) @ gs-spring-boot ---
[INFO] BUILD SUCCESS

$ ./gradlew clean build
BUILD SUCCESSFUL in 3s   ->  build/libs/gs-spring-boot-0.1.0.jar

The repo contains no tests, so there is nothing for surefire / :test to run (Task :test NO-SOURCE). No tests were added, weakened or removed.

App boot + endpoints on 17 (java -jar target/gs-spring-boot-0.1.0.jar):

Starting Application using Java 17.0.13 ... Spring Boot v2.7.18, Tomcat/9.0.83
Started Application in 1.037 seconds

GET /                        -> {"id":1,"content":"Hello, World!"}
GET /topic                   -> [{"id":"spring",...},{"id":"java",...},{"id":"javascript",...}]
GET /topic/sort              -> sorted by id
POST /topic (java17)         -> 200, then GET /topic/java17 -> {"id":"java17",...}

Not fully verified / notes

  • Application.main and a CommandLineRunner both call http://gturnquist-quoters.cfapps.io/api/random; that host no longer exists, so the app dies with UnknownHostException right after Started Application. This is pre-existing and unrelated to the Java level — to capture the endpoint evidence above I pointed that hostname at a throwaway local stub via /etc/hosts (nothing in the repo was changed for it). Worth fixing separately.
  • Maven Central was returning HTTP 429 on this machine, so dependencies were fetched through a local-only mirror in ~/.m2/settings.xml / a Gradle init script. Neither is part of this diff; the build files still point at mavenCentral().
  • Gradle 7.6.4 (not 8.x) was chosen for headroom against the Boot 2.7 plugin; the build logs the usual "incompatible with Gradle 8.0" deprecation notice from the plugin.
  • No CI workflows exist in this repo, so nothing CI-side was touched.

Link to Devin session: https://app.devin.ai/sessions/06b09547bfc84eeeab26864fb8daa79e
Requested by: @tobydrinkall


Devin Review

Status Commit
⚪ Not started

Run Devin Review

Open in Devin Review (Staging)
Open in Devin Review

Co-Authored-By: Toby Drinkall <toby.drinkall@cognition.ai>
@tobydrinkall tobydrinkall self-assigned this Jul 30, 2026
@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

@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 4 potential issues.

Open in Devin Review

Comment thread pom.xml
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
<version>2.7.18</version>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔍 Boot BOM bump pulls H2 2.x, whose SQL grammar may reject the legacy DDL in Application

The parent bump from 2.0.2.RELEASE to 2.7.18 also moves the managed com.h2database:h2 version from 1.4.19x to 2.1.x. hello/Application.java:66-67 issues legacy DDL (DROP TABLE customers IF EXISTS with trailing IF EXISTS, and the SERIAL column type) that H2 2.x tightened/removed in its grammar cleanup. Because these run from a CommandLineRunner, a syntax error would abort the application right after startup. The PR's verification narrative reports the app dying in the other runner (UnknownHostException from the gturnquist-quoters.cfapps.io call at hello/Application.java:39 and hello/Application.java:52-53), so the JDBC path may never have been exercised on the new stack. Worth explicitly running the app with a reachable/stubbed quote endpoint (or pinning <h2.version> to 1.4.200) to confirm the DDL still parses.

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.

The JDBC path was exercised — H2 resolves to 2.1.214 on the new BOM (mvn dependency:treecom.h2database:h2:jar:2.1.214:compile) and both the legacy DROP TABLE customers IF EXISTS and the SERIAL column still parse. Runtime log from the boot run described in the description (the quote endpoint was stubbed locally, so Application.run completed before the other runner failed):

Creating tables
Inserting customer record for John Woo / Jeff Dean / Josh Bloch / Josh Long
Querying for customer records where first_name = 'Josh':
Customer{id=3, firstName='Josh', lastName='Bloch'}
Customer{id=4, firstName='Josh', lastName='Long'}

So no <h2.version> pin is needed.

Comment thread build.gradle
testCompile("junit:junit")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-jdbc")
runtimeOnly("com.h2database:h2")

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: Gradle declares H2 runtimeOnly while Maven keeps it on the compile classpath

pom.xml:31-34 declares com.h2database:h2 with default (compile) scope, whereas the Gradle build uses runtimeOnly. No source references H2 classes directly, so both builds work, but the two build definitions are no longer symmetric; making the Maven dependency <scope>runtime</scope> would keep them aligned and prevent accidental compile-time coupling to H2.

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.

True. H2's compile scope in pom.xml is pre-existing, so I left it alone to keep the diff to the upgrade; nothing imports H2 classes, so it's inert either way. Say the word and I'll add <scope>runtime</scope> to align the two builds.

Comment thread pom.xml
<groupId>org.springframework</groupId>
<artifactId>gs-spring-boot</artifactId>
<packaging>pom</packaging>
<packaging>jar</packaging>

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: Switching to jar packaging now ships spring-boot-properties-migrator inside the executable jar

With pom packaging no artifact was produced, so spring-boot-properties-migrator was effectively inert. Now that a repackaged jar is built, the migrator is bundled as a runtime dependency of the shipped application. Spring's guidance is to remove it once the migration is complete, since it scans and rewrites properties on every startup; it is also absent from the Gradle build, so the two builds produce different runtime classpaths.

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.

Correct, and worth deciding on. It resolves on 2.7.18 and reports nothing at startup (the only property file here is a fully commented-out server.port), so per the upgrade brief I kept it rather than silently dropping a declared dependency. I'm happy to remove it in a follow-up commit — it's a migration aid with nothing left to migrate — just want the owner's call since deleting a dependency is a scope change.

Comment thread build.gradle
Comment on lines +25 to +29
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}

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: Toolchain replaces source/targetCompatibility, so builds now hard-fail without a local JDK 17

Gradle 7.6 has no built-in toolchain provisioning resolver plugin configured here, so if the machine running the build has no JDK 17 installed (and auto-download is not enabled), the build fails with "No matching toolchains found" rather than falling back to the current JVM — a behavioral difference from the previous sourceCompatibility/targetCompatibility settings. Fine for local development on 17, but worth noting if this build ever runs on an environment that only provides a different JDK.

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.

Agreed. Deliberate: a toolchain pins the compile target instead of silently compiling with whatever JVM Gradle runs on, which is what the upgrade brief asked for. There is no CI here, so the only consumer is local dev; toolchain auto-provisioning (foojay-resolver) can be added if this ever builds somewhere without a JDK 17.

@devin-ai-integration

Copy link
Copy Markdown

Runtime verification of the Java 17 upgrade ✅

Built and booted both jars on JDK 17.0.13 and exercised the whole REST API end-to-end. Devin session: https://app.devin.ai/sessions/06b09547bfc84eeeab26864fb8daa79e

REST API works on the upgraded runtime (Maven jar, Spring Boot 2.7.18 / Java 17)

GET /topic — 3 seeded topics:

GET /topic

POST /topic {"id":"kotlin",...} → 200, list now has 4 topics:

After POST

PUT /topic/kotlin → 200, values updated:

After PUT

DELETE /topic/kotlin → 200, back to exactly 3 topics:

After DELETE

Also verified: GET / + ?name= greeting counter, GET /topic/{id}, GET /topic/sort
(java, javascript, spring), GET /topic/minimum/length/4 (correctly excludes java — strict >),
GET /datetime, GET /topic/string/operation.

Bytecode + boot evidence
mvn clean verify        -> BUILD SUCCESS      -> target/gs-spring-boot-0.1.0.jar
./gradlew clean build   -> BUILD SUCCESSFUL   -> build/libs/gs-spring-boot-0.1.0.jar

javap -v target/classes/hello/controller/TopicController.class      -> major version: 61
javap -v build/classes/java/main/hello/service/TopicService.class   -> major version: 61
git show master:target/.../TopicController.class | javap -v         -> major version: 52   (baseline)
 :: Spring Boot ::               (v2.7.18)
Starting Application v0.1.0 using Java 17.0.13 ...
Tomcat started on port(s): 8080 (http)
Creating tables
Inserting customer record for John Woo / Jeff Dean / Josh Bloch / Josh Long
Customer{id=3, firstName='Josh', lastName='Bloch'}
Customer{id=4, firstName='Josh', lastName='Long'}

The H2 risk of this upgrade is clear: the legacy DROP TABLE customers IF EXISTS + SERIAL DDL
still runs fine on the H2 2.1.214 that Boot 2.7.18 pulls in.

Gradle jar parity

The Gradle jar boots with the same banner/JDK/H2 output and returns byte-identical JSON for
/topic, /topic/sort and /topic/minimum/length/4; the mutation and error-handling set was
re-run against it with identical statuses. spring-boot-properties-migrator (Maven-only) emitted
zero log lines, so there is no observable Maven-vs-Gradle runtime difference.

Gradle jar GET /topic

Adversarial inputs — and two pre-existing bugs (not regressions)
POST /topic '{"id":'                       -> 400
POST /topic (no Content-Type)              -> 415
GET  /topic/minimum/length/0  and  /-5     -> 200 (all topics; predicate is strictly >)
GET  /topic/minimum/length/99999999999999  -> 400 (int overflow, not 500)
GET  /topic/minimum/length/abc             -> 400
PUT/DELETE /topic/nope                     -> 200 no-op
GET  /topic/doesnotexist                   -> 500  <-- pre-existing
POST /topic '{}'                           -> 200, inserts an all-null Topic  <-- pre-existing

400 for huge minLength

No response leaked a stack trace. The two 500-ish issues exist on master too and are unrelated to
this PR: TopicService.getTopicWithId calls Optional.get() with no 404 mapping, and there is no
@Valid on POST /topic. Worth separate follow-ups.

Caveat: the Chrome automation crashed late in the run, so GET /datetime and the Gradle-jar
mutation flow were verified via curl only (outputs above) rather than with screenshots. Separately,
note that Application.main still calls the defunct gturnquist-quoters.cfapps.io, so the app exits
right after startup unless that host is stubbed — I stubbed it locally for testing; it is a
pre-existing bug outside this PR's scope but blocks running the app as-is.

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