Skip to content

Upgrade Java 8 -> 17 (Spring Boot 2.7.18) for Maven and Gradle builds - #56

Open
tobydrinkall wants to merge 2 commits into
masterfrom
devin/java17-upgrade
Open

tobydrinkall wants to merge 2 commits into
masterfrom
devin/java17-upgrade

Conversation

@tobydrinkall

@tobydrinkall tobydrinkall commented Jul 30, 2026

Copy link
Copy Markdown

Summary

Upgrades this Spring Boot demo from Java 8 → Java 17 (LTS) and Spring Boot 2.0.2.RELEASE → 2.7.18 (last Java 17-compatible 2.x line, still javax.*). Both the Maven and Gradle builds compile, and the app boots and serves all documented REST endpoints on JDK 17.

Deliberately not moving to Spring Boot 3.x — the javax.*jakarta.* namespace migration is out of scope here and is a recommended follow-up (see Residual risks).

pom.xml

  • spring-boot-starter-parent 2.0.2.RELEASE2.7.18.
  • <java.version>1.8</java.version><java.version>17</java.version>. The 2.7 parent maps this to maven.compiler.release=17, so the compiler validates against the Java 17 API (bytecode confirmed as class-file major version 61).
  • <packaging>pom</packaging>jar. This was a real bug: with pom packaging the Maven build never compiled src/main/java or produced a runnable artifact. jar is clearly correct (there is a src/main/java + spring-boot-maven-plugin), and the build/run/endpoints all pass with it.
  • H2 dependency given <scope>runtime</scope> to match Gradle's runtimeOnly.
  • No plugin version pins needed — the 2.7 parent already manages maven-compiler-plugin (3.10.1) and maven-surefire-plugin (2.22.2), both Java 17-safe.
  • spring-boot-properties-migrator (runtime) kept as a temporary migration aid for surfacing renamed 2.x properties; it emitted no warnings for this app and should be removed after the bump.

build.gradle

  • spring-boot-gradle-plugin 2.0.2.RELEASE2.7.18 (matches Maven).
  • sourceCompatibility/targetCompatibility = 1.8 → Java toolchain targeting 17:
    java { toolchain { languageVersion = JavaLanguageVersion.of(17) } }
  • Removed/deprecated configurations updated: compileimplementation, testCompiletestImplementation.
  • bootJar { baseName / version }archiveBaseName / archiveVersion (baseName/version are removed in modern Gradle).
  • Dependencies aligned with Maven: the Gradle build previously declared only spring-boot-starter-web, but Application.java uses JdbcTemplate + H2, so the Gradle build could not have compiled the sources. Added spring-boot-starter-jdbc, com.h2database:h2 (runtime) and spring-boot-properties-migrator (runtime) so both builds are consistent (same Spring Boot version, Java target, and dependencies).

Gradle wrapper

  • gradle/wrapper/gradle-wrapper.properties 4.67.6.4 (Gradle < 7.3 cannot run on JDK 17; 7.6.4 stays inside the Spring Boot 2.7 supported matrix — Gradle 8.x is not officially supported by the 2.7 plugin). Regenerated via ./gradlew wrapper --gradle-version 7.6.4, which also updated gradlew, gradlew.bat and gradle-wrapper.jar.

Maven wrapper

  • .mvn/wrapper/maven-wrapper.properties 3.3.93.9.9 for consistency with the Gradle wrapper bump and to run on a modern-JDK-supported Maven (3.3.9 predates JDK 9 support; it happened to build green here on 17 but is an unsupported pairing).

Application code

  • Spring Boot 2.0 → 2.7 was source-compatible for this app — no WebMvcConfigurerAdapter, no removed Spring MVC/JDBC APIs, no application.properties renames, and H2 2.x (pulled in by the 2.7 parent) accepted the existing DDL (DROP TABLE ... IF EXISTS, CREATE TABLE ... id SERIAL).
  • One pre-existing runtime bug fixed (not caused by Java 17): startup called a now-dead host http://gturnquist-quoters.cfapps.io/api/random (Pivotal's shut-down cfapps.io) inside a CommandLineRunner, throwing UnknownHostException and aborting boot. Wrapped both quote fetches in try/catch that logs a warning, so the demo feature is preserved but a dead external endpoint no longer prevents the app from starting. Minimal diff:
    try {
        Quote quote = restTemplate.getForObject(".../api/random", Quote.class);
        log.info(String.valueOf(quote));
    } catch (Exception e) {
        log.warn("Could not fetch random quote: {}", e.getMessage());
    }

Note: target/ (stale committed .class files) is tracked in the repo; this PR does not touch those build artifacts.

Validation (JDK 17, openjdk 17.0.13)

Maven./mvnw clean verify (Apache Maven 3.9.9, Java version: 17.0.13):

[INFO] Building jar: target/gs-spring-boot-0.1.0.jar
[INFO] Replacing main artifact with repackaged archive
[INFO] BUILD SUCCESS

No unit tests exist in the repo (src/test absent), so there are no tests to run under Maven/Gradle; compilation + packaging + repackage succeed.

Gradle./gradlew clean build (Gradle 7.6.4, JVM: 17.0.13):

BUILD SUCCESSFUL
6 actionable tasks: 6 executed
:test NO-SOURCE

Runtimejava -jar target/gs-spring-boot-0.1.0.jar:

Tomcat started on port(s): 8080 (http)
Started Application in 1.035 seconds
Creating tables / Inserting customer record ... / Querying ...
Customer{id=3, firstName='Josh', lastName='Bloch'}

Endpoint checks with curl on JDK 17:

GET /                       -> {"id":1,"content":"Hello, World!"}
GET /topic                  -> [ {spring}, {java}, {javascript} ]
GET /topic/java             -> {"id":"java","subjectName":"Core Java",...}
POST /topic (go)            -> 200, subsequently listed in GET /topic
GET /topic/sort             -> sorted by id
GET /topic/minimum/length/4 -> [javascript, spring]
GET /topic/string/operation -> stream/string ops output
GET /topic/file/operation   -> NIO.2 Files.walk/find + BufferedReader output
GET /datetime               -> java.time output (LocalDateTime / ZonedDateTime)

Residual risks / follow-ups

  • Spring Boot 3.x / jakarta.* migration intentionally deferred — the recommended next step for a modern JDK 17 baseline.
  • Remove spring-boot-properties-migrator (from both builds) once the version bump is settled — it is only a temporary migration aid and adds runtime property-scanning to the fat jar.
  • Dead demo endpoint: the gturnquist-quoters.cfapps.io quote service is gone; the call now fails gracefully. Consider removing it or pointing at a live quote source.
  • Startup DDL uses legacy H2 forms (DROP TABLE customers IF EXISTS, SERIAL); H2 2.x still parses them but DROP TABLE IF EXISTS customers + id IDENTITY would be more future-proof.
  • target/ compiled classes are checked into the repo; consider gitignoring target/, build/, .gradle/ in a separate cleanup.
  • No CI exists in the repo; none was added, per the task's ground rules.

Link to Devin session: https://app.devin.ai/sessions/b941fb29ceab40319abef85352f738e6
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 5 potential issues.

Open in Devin Review

Comment thread pom.xml
Comment thread build.gradle
Comment on lines +39 to +44
try {
Quote quote = restTemplate.getForObject("http://gturnquist-quoters.cfapps.io/api/random", Quote.class);
log.info(String.valueOf(quote));
} catch (Exception e) {
log.warn("Could not fetch random quote: {}", e.getMessage());
}

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: Broad catch around the quote fetch hides stack traces and non-network failures

catch (Exception e) plus log.warn(..., e.getMessage()) swallows every failure mode of the call — including JSON deserialization errors and programming errors — and logs only the message with no stack trace, which makes real regressions in this code path invisible. Narrowing to RestClientException (or at least passing e to the logger) would keep the graceful-degradation goal while preserving diagnostics. Same pattern appears in both main() and the CommandLineRunner bean (src/main/java/hello/Application.java:56-62).

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 broad catch (Exception) is intentional here: the explicit goal of this change is that a dead/misbehaving demo endpoint must never prevent the app from booting on JDK 17, so I don't want to let e.g. a deserialization error propagate out of the CommandLineRunner and abort startup again. I take your point about diagnostics though — since this is a throwaway demo call, dropping the stack trace is acceptable, but I'm happy to switch to logging the full exception (log.warn("...", e)) if you'd prefer to keep the trace. Leaving the broad catch as-is unless the maintainer wants it narrowed.

Comment on lines +56 to +62
try {
Quote quote = restTemplate.getForObject(
"http://gturnquist-quoters.cfapps.io/api/random", Quote.class);
log.info(String.valueOf(quote));
} catch (Exception e) {
log.warn("Could not fetch random quote: {}", e.getMessage());
}

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: Dead external host retained in code after the failure is made non-fatal

Both call sites still point at http://gturnquist-quoters.cfapps.io/api/random, a host that no longer resolves. With the new try/catch the app boots, but every startup now performs a doomed DNS lookup/connection attempt (two of them: one in main() and one in the CommandLineRunner bean) and emits a warning, adding avoidable startup latency depending on resolver timeouts. Removing the demo call or pointing it at a live endpoint would be cleaner, as the PR description itself suggests.

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 this is the cleaner end state. I deliberately kept the demo call (just made it non-fatal) to keep the diff minimal and preserve the original demo behavior, and flagged removal as a follow-up in the PR description. The extra startup cost is one failed DNS lookup per call site; happy to remove both calls (and the Quote/RestTemplate demo wiring) if the maintainer prefers — let me know and I'll do it in this PR.

Comment thread build.gradle
… align H2 scope

Co-Authored-By: Toby Drinkall <toby.drinkall@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

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: H2 2.x DDL compatibility relies on legacy syntax

The startup DDL uses DROP TABLE customers IF EXISTS (trailing IF EXISTS) and the non-standard SERIAL column type; both are legacy/compat forms that H2 2.x (pulled in by the 2.7 parent) still parses but which are not part of its documented grammar. The PR states boot was verified manually; if the H2 version ever moves again, this is the first place that will break. Rewriting as DROP TABLE IF EXISTS customers and id IDENTITY/BIGINT AUTO_INCREMENT would be more future-proof.

(Refers to lines 74-76)

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 existing DDL (DROP TABLE customers IF EXISTS, id SERIAL) parses and runs cleanly on the H2 2.x that the 2.7 parent pulls in (verified at startup: tables created, rows inserted and queried). I'm leaving it as-is to keep this PR a focused Java 17 / Spring Boot bump rather than rewriting demo SQL, but I've noted your future-proofing suggestion (DROP TABLE IF EXISTS customers + id IDENTITY) as a follow-up in the PR description.

@@ -1,6 +1,6 @@
#Thu Mar 01 09:01:15 CST 2018
distributionBase=GRADLE_USER_HOME

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: Wrapper version differs from what the PR description states

The PR description says the Gradle wrapper was regenerated to 8.5, but the committed gradle/wrapper/gradle-wrapper.properties targets 7.6.4 (a later commit, Address review: ... Gradle wrapper to 7.6.4, changed it). The gradlew script and wrapper jar do look consistent with the 7.6.x wrapper (networkTimeout present, no validateDistributionUrl), so the build should work; only the description is stale. 7.6.4 is also the safer choice for the Spring Boot 2.7.18 Gradle plugin, which does not officially support Gradle 8.x.

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.

Thanks — updated the PR description to reflect the wrapper being 7.6.4 (and the Maven wrapper bump to 3.9.9). And agreed, 7.6.4 is the safer choice inside the Boot 2.7 supported matrix, which is exactly why I switched off 8.5.

Comment thread build.gradle
dependencies {
compile("org.springframework.boot:spring-boot-starter-web")
testCompile("junit:junit")
runtimeOnly("org.springframework.boot:spring-boot-properties-migrator")

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: properties-migrator added to Gradle runtime as well

spring-boot-properties-migrator is now a runtime dependency in both builds. Per Spring's docs it is a temporary migration aid and should be removed once the upgrade is settled; leaving it in ships extra runtime property-scanning in the fat jar produced by bootJar. The description already lists removal as a follow-up.

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 — it's intentionally kept in both builds as a temporary migration aid for this bump (it surfaces renamed 2.x properties; it reported none for this app), and removal from both pom.xml and build.gradle is listed as a follow-up in the PR description.

@devin-ai-integration

Copy link
Copy Markdown

✅ Runtime verification — Java 17 / Spring Boot 2.7.18 (all endpoints pass)

Built with ./mvnw clean package (BUILD SUCCESS) on OpenJDK 17.0.13 and ran java -jar target/gs-spring-boot-0.1.0.jar. All REST endpoints verified via curl.

Startup on JDK 17 (the core of this upgrade)
Tomcat started on port(s): 8080 (http)
Started Application in 1.068 seconds
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, ... 'Josh', 'Bloch'}
Customer{id=4, ... 'Josh', 'Long'}
WARN  Could not fetch random quote: ... UnknownHostException: gturnquist-quoters.cfapps.io

The dead external quote host logs a WARN but startup completes — the non-fatal try/catch works as intended. H2 DDL/insert/query all succeed.

Endpoint checks (all HTTP 200)
  • GET /{"id":1,"content":"Hello, World!"}
  • GET /topic → spring/java/javascript; GET /topic/java → Core Java
  • CRUD: POST go → appears; PUT → Golang; DELETE → removed
  • /topic/sort → java, javascript, spring; /topic/minimum/length/4 → javascript, spring
  • /topic/string/operationjava:javascript:spring, [spring]
  • /topic/file/operation → file listing + temp.txt read, no IO errors
  • /datetime → java.time string with leap-year/zone info

Note (pre-existing, not a regression from this PR): GET /topic/{missing} returns 500 due to Optional.get() on an absent topic.

Verified by Devin's testing agent.

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