Skip to content

Upgrade to Java 17 and Spring Boot 3.3.5 - #57

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

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

Conversation

@tobydrinkall

@tobydrinkall tobydrinkall commented Jul 30, 2026

Copy link
Copy Markdown

Summary

Moves this repo off Java 8 / Spring Boot 2.0.2 onto Java 17 (LTS) + Spring Boot 3.3.5, for both build systems.

  • pom.xml: parent spring-boot-starter-parent 2.0.2.RELEASE3.3.5, java.version 1.817, and <packaging>pom</packaging>jar. The pom packaging meant Maven never compiled src/ or produced an artifact at all — with jar the compile + bootJar repackage actually run, which is what makes the Java 17 target verifiable. Also dropped spring-boot-properties-migrator (a temporary Boot-2→3 aid with nothing to migrate here) and brought scopes in line with the Gradle build (H2 runtime, added spring-boot-starter-test).
  • build.gradle: rewritten from the legacy buildscript{}/apply plugin form to the plugins {} block (Boot 3.3.5, dependency-management 1.1.6), sourceCompatibility/targetCompatibility 1.817, and removed configurations Gradle 8 no longer has (compile/testCompileimplementation/runtimeOnly/testImplementation, bootJar.baseName/versionarchiveBaseName/archiveVersion).
  • Wrappers: Maven 3.3.93.9.9 and Gradle 4.68.10.2, both fully regenerated (wrapper:wrapper / gradlew wrapper), so the launcher scripts and wrapper jars match their distributions. Both old versions fail outright on a JDK 17 runtime, so this is required, not cosmetic. mvnw/gradlew were also committed non-executable (100644); fixed.
  • Application.java: JdbcTemplate.query(sql, Object[], RowMapper) is deprecated in Spring 6 and was the sole deprecation warning; switched to the varargs overload. Separately, the app fetched a random quote from the long-decommissioned gturnquist-quoters.cfapps.io in two places and died with UnknownHostException; since jar packaging now makes that reachable, both call sites route through one guarded helper:
private static void logRandomQuote(RestTemplate restTemplate) {
    try {
        log.info(String.valueOf(restTemplate.getForObject(QUOTE_URL, Quote.class)));
    } catch (RestClientException e) {
        log.warn("Could not fetch a quote from {}: {}", QUOTE_URL, e.getMessage());
    }
}

No javax.*jakarta.* migration was needed: the app has no servlet/JPA/validation imports, so Boot 3 was a drop-in.

Verification

  • mvn clean package and gradlew build both succeed on JDK 17.0.13 with zero warnings.
  • Ran the boot jar: Tomcat starts on 8080, the quote failure is now a WARN instead of a fatal error, the JDBC CommandLineRunner creates/queries the H2 customers table, and GET /{"id":1,"content":"Hello, World!"} plus GET /topic / GET /topic/java return the expected JSON.

Behaviour change to be aware of: Spring 6 removed trailing-slash URL matching, so GET /topic/ now 404s where it worked on Boot 2. The README documents all endpoints without trailing slashes.

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


Devin Review

Status Commit
⚪ Not started

Run Devin Review

Open in Devin Review (Staging)
Open in Devin Review

Bump Maven/Gradle compiler targets to 17, upgrade Spring Boot 2.0.2 -> 3.3.5, refresh Maven (3.9.9) and Gradle (8.10.2) wrappers, and modernise the deprecated JdbcTemplate.query args form.

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
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
<version>3.3.5</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 3 removes trailing-slash URL matching for existing endpoints

Spring Framework 6 / Boot 3 changed PathPatternParser to be the default and removed trailing-slash matching (setUseTrailingSlashMatch is deprecated/false). Endpoints declared in src/main/java/hello/controller/TopicController.java:21-81 and src/main/java/hello/controller/HelloController.java:46-87 will therefore no longer answer requests with a trailing slash (e.g. GET /topic/ now returns 404 whereas it worked under Boot 2.0). No code change is needed for the happy path, but any client or doc relying on trailing slashes will break silently after this upgrade.

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.

Accurate, and inherent to Boot 3 — Spring 6 dropped trailing-slash matching and the opt-back-in (setUseTrailingSlashMatch) is deprecated, so I'm not restoring it. Verified the non-slash happy path against the built jar: GET / -> {"id":1,"content":"Hello, World!"}, GET /topic and GET /topic/java return the expected JSON. The README documents all endpoints without trailing slashes, so no documented usage regresses.

Comment thread build.gradle
Comment thread build.gradle Outdated
Comment thread .mvn/wrapper/maven-wrapper.properties Outdated
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: Maven packaging switch to jar now actually compiles and packages sources

Changing packaging from pom to jar means the Maven build now compiles src/main/java and runs spring-boot-maven-plugin:repackage for the first time. Note that application.properties lives at the repository root rather than src/main/resources, so it is not packaged into the jar — the runtime picks it up only when the process's working directory is the repo root. It currently contains just a commented-out server.port, so behavior is unaffected today, but the file will be silently ignored for anyone running the produced jar from elsewhere.

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 pre-existing — the root application.properties was already outside src/main/resources before this PR; the only difference is that a jar now actually gets produced. Its sole entry is a commented-out server.port, so nothing changes behaviourally. Leaving it as-is rather than widening this PR's scope; moving it into src/main/resources would be a sensible follow-up.

…enerate Maven wrapper

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 6 new potential issues.

Open in Devin Review

Comment thread gradle/wrapper/gradle-wrapper.properties Outdated
Comment thread build.gradle
Comment thread build.gradle
Comment thread mvnw
Comment on lines +259 to +288
# Find the actual extracted directory name (handles snapshots where filename != directory name)
actualDistributionDir=""

# For Cygwin, switch paths to Windows format before running java
if $cygwin; then
[ -n "$M2_HOME" ] &&
M2_HOME=`cygpath --path --windows "$M2_HOME"`
[ -n "$JAVA_HOME" ] &&
JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
[ -n "$CLASSPATH" ] &&
CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
# First try the expected directory name (for regular distributions)
if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then
if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then
actualDistributionDir="$distributionUrlNameMain"
fi
fi

# traverses directory structure from process work directory to filesystem root
# first directory with .mvn subdirectory is considered project base directory
find_maven_basedir() {
local basedir=$(pwd)
local wdir=$(pwd)
while [ "$wdir" != '/' ] ; do
if [ -d "$wdir"/.mvn ] ; then
basedir=$wdir
break
# If not found, search for any directory with the Maven executable (for snapshots)
if [ -z "$actualDistributionDir" ]; then
# enable globbing to iterate over items
set +f
for dir in "$TMP_DOWNLOAD_DIR"/*; do
if [ -d "$dir" ]; then
if [ -f "$dir/bin/$MVN_CMD" ]; then
actualDistributionDir="$(basename "$dir")"
break
fi
fi
wdir=$(cd "$wdir/.."; pwd)
done
echo "${basedir}"
}

# concatenates all lines of a file
concat_lines() {
if [ -f "$1" ]; then
echo "$(tr -s '\n' ' ' < "$1")"
fi
}

export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-$(find_maven_basedir)}
MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
set -f
fi

# Provide a "standardized" way to retrieve the CLI args that will
# work with both Windows and non-Windows executions.
MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@"
export MAVEN_CMD_LINE_ARGS
if [ -z "$actualDistributionDir" ]; then
verbose "Contents of $TMP_DOWNLOAD_DIR:"
verbose "$(ls -la "$TMP_DOWNLOAD_DIR")"
die "Could not find Maven distribution directory in extracted archive"
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔍 mvnw deviates from the upstream 3.3.4 script and skips an error check on the Java fallback download

The committed mvnw/mvnw.cmd include a non-upstream "find the actual extracted directory name" block, and the Java-based download fallback at the Downloader invocation has no || die ... guard (unlike the wget/curl branches), so a failed Java download surfaces later as a confusing "failed to unzip" message. Additionally the Unix script derives the cache directory name from a Java-style string hash while the PowerShell script uses a SHA-256 hex digest, so Windows and Unix users cache the same distribution under different directories. None of this breaks the build, but hand-modified wrapper scripts drift from what mvn wrapper:wrapper regenerates.

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.

mvnw/mvnw.cmd are not hand-modified — they're verbatim output of mvn wrapper:wrapper -Dmaven=3.9.9 (maven-wrapper-plugin 3.3.4, the version resolved by Maven 3.9.9), which is also where the distributionType=only-script switch and the deletion of maven-wrapper.jar come from. So the blocks you flagged (extracted-directory discovery, the un-guarded Java download fallback, the hash-vs-SHA256 cache-dir difference between the sh and cmd scripts) are upstream plugin behaviour; patching them here would be the actual drift, since the next wrapper:wrapper run would revert it. Better filed upstream against maven-wrapper-plugin if it's worth pursuing.

Comment thread pom.xml
…, regenerate Gradle wrapper

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

Comment on lines +88 to +90
"SELECT id, first_name, last_name FROM customers WHERE first_name = ?",
(rs, rowNum) -> new Customer(rs.getLong("id"), rs.getString("first_name"), rs.getString("last_name")),
"Josh"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔍 Legacy H2 DDL syntax is the main runtime risk of the H2 2.x jump

Boot 3.3.5 manages H2 2.x, and the startup runner still uses the legacy grammar DROP TABLE customers IF EXISTS (trailing IF EXISTS) plus the SERIAL column type. Both are legacy/compatibility forms that H2 2.x tightened up in places; the same pattern comes from the old Spring guide. The PR states this path was executed successfully on the new stack, so I did not flag it, but if the H2 version is later bumped or the compatibility mode changes, this DDL is the first thing that will break at startup (the runner failure would propagate out of SpringApplication.run).

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 working, not just asserted: with Boot 3.3.5's managed H2 (2.2.224) the runner logs Creating tables, the four inserts, then Querying for customer records where first_name = 'Josh' with no SQL error — H2 2.x still accepts both DROP TABLE ... IF EXISTS and SERIAL. Leaving the guide's DDL as-is so this PR stays an upgrade rather than a rewrite; modernising it to DROP TABLE IF EXISTS customers / BIGINT AUTO_INCREMENT would be a clean follow-up.

Comment on lines +44 to +49
private static void logRandomQuote(RestTemplate restTemplate) {
try {
log.info(String.valueOf(restTemplate.getForObject(QUOTE_URL, Quote.class)));
} catch (RestClientException e) {
log.warn("Could not fetch a quote from {}: {}", QUOTE_URL, 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: Quote fetch failures are now silently downgraded to a warning

logRandomQuote swallows any RestClientException (including ResourceAccessException for the decommissioned gturnquist-quoters.cfapps.io host) and logs a warning. This is what makes java -jar runs survive, but it also means the URL being permanently dead is now invisible in normal operation, and String.valueOf(...) will log the literal null if the endpoint ever returns an empty body instead of surfacing a problem. Consider removing the dead call entirely as the description 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.

Deliberate, and it's the lesser of two evils: before this the process died with UnknownHostException on every java -jar, which made the endpoints unreachable. The failure isn't silent — it's a WARN naming the URL and the cause on each of the two calls. I'd agree deleting the dead call is the better end state, but that removes a demo feature the repo advertises, so I've asked the author to confirm rather than deciding it inside an upgrade PR. String.valueOf is only there to avoid an NPE on a null body; getForObject returning null against a live quote service isn't a scenario reachable today.

Comment thread build.gradle
Comment on lines 26 to +29
dependencies {
compile("org.springframework.boot:spring-boot-starter-web")
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 and Maven builds now agree on dependencies, but h2 scoping differs subtly

Maven declares h2 with runtime scope and Gradle uses runtimeOnly, which are equivalent for this app since no source touches H2 APIs. Note the Maven side previously had h2 at compile scope; narrowing it is safe here only because there are no H2 imports in src/. Similarly, no explicit test { useJUnitPlatform() } is needed in build.gradle because the Spring Boot Gradle plugin configures it, and there are currently no tests under src/test.

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 on all three points. H2 narrowing is safe — rg 'org\.h2' src/ finds nothing, it's purely a JDBC-URL-driven runtime dependency. I'll leave test { useJUnitPlatform() } in place: it's redundant with the Boot plugin today but harmless, and it makes the intent explicit for whoever adds the first test.

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