AI Kit Guides
Spring Boot 4 Migration

Spring Boot 3.5 to 4: A Runnable Verification Lab

Run a Java 17 project on Boot 3.5.7 and 4.0.0. Inspect CSRF, real HTTP error dispatch, Flyway history, and two deliberately failing checks.

Peng Zhou Peng Zhou · · Updated October 23, 2026 · 1154 words
6 min read · 1154 words

This case study supplies one complete Java project, two pinned Spring Boot profiles, and the run records needed to check its results. It was created with AI assistance and executed on October 23, 2026. All inputs are synthetic; the project does not reconstruct an earlier production incident or benchmark a coding agent.

Start with the source, results and download page. The migration overview helps choose which application contracts to protect; this article shows a bounded implementation of that process.

What changes between the profiles

Both profiles use Java 17, the same source and the same assertions. boot3 selects Spring Boot 3.5.7, the web starter and Flyway core. boot4 selects 4.0.0, the web MVC starter and Boot’s Flyway starter. The BOM manages library versions; compiler and test-runner plugins are explicit in the POM.

These are historical comparison releases, not a recommendation about the latest production patch. Consult the official migration guide for changes relevant to your target release. The fixture deliberately uses shared APIs and does not exercise every removed API or integration.

Run the project and inspect the evidence

Clone or download the Boot verification project on GitHub, enter its directory, and check that Maven uses JDK 17 or later:

java -version
mvn -version
python3 verify.py

The first run needs Maven repository access. The lab uses H2 and a loopback HTTP server, so no Docker or external database is required. verify.py saves raw logs locally and checks the names and results of the expected suites. It also verifies two deliberate failures rather than treating any nonzero exit as success.

The recorded environment was macOS arm64, Corretto 17.0.19 and Maven 3.9.11. The published snapshot contains these outcomes:

Run Tests Failures Errors Skipped Exit
-Pboot3 clean test 11 0 0 0 0
-Pboot4 clean test 11 0 0 0 0
Explicit discovery sentinel 1 1, intentional 0 0 1
Error-dispatch fault enabled 1 1, intentional 0 0 1

The two normal runs execute the same eleven cases, not twenty-two different requirements. Read the result manifest and Boot 4 log to inspect versions and observations. Source digests bind the record to the included POM and source files. Machine paths were redacted from the published logs and XML; the README explains what was removed.

Check HTTP security at two layers

SecurityContractTest creates MockMvc from the application context and applies springSecurity(). Its six cases cover an authenticated GET, anonymous GET, missing/invalid CSRF tokens, a valid CSRF request, and an intentionally unsecured standalone harness.

With the configured chain, an authenticated POST without a token returns 403; adding a valid test token permits the fixture’s 200 response. A standalone controller without security filters also returns 200, even without the protections. That is evidence about a different harness, not evidence that the application’s endpoint is public.

NetworkContractTest starts the embedded server on a random loopback port and sends actual Java HTTP client requests. It checks anonymous GET 401, valid Basic GET 200, and valid Basic POST without a CSRF token 403. It also checks that a direct request to /error remains protected. Test-user injection and a real Basic exchange are separate checks.

See Spring’s MockMvc setup and the CSRF diagnosis guide for the relevant wiring.

Reproduce the error dispatch that masks a 403

During development of this fixture, a catch-all authenticated rule produced a surprising real HTTP 401 for the missing-token POST. The trace showed an initial CSRF rejection with 403, followed by an error dispatch to /error that reached the authentication entry point.

The normal configuration permits the servlet’s ERROR dispatcher type before applying the authenticated rule to ordinary requests. It keeps CSRF protection enabled. This follows Spring’s documented dispatcher-type matching. It does not globally permit the /error URL.

The teaching switch below removes that dispatcher permission. The existing network assertion still expects 403, so the command must fail when it observes 401:

mvn -B -Pboot4 test -Dtest=NetworkContractTest \
  -Dlab.allow-error-dispatch=false \
  -Dlogging.level.org.springframework.security=TRACE

Inspect the fault trace for Invalid CSRF token, Responding with 403, and the subsequent /error authorization. This is a configuration-specific result in the fixture. It does not establish that Basic authentication universally changes CSRF failures into 401, or that Boot 4 introduced the behavior.

Prove migration activation and history separately

DatabaseActivationTest checks a real Flyway bean and a marker row inserted by the application’s classpath migration. That distinguishes a resolving library from a migration actually run by Boot.

FlywayHistoryTest then creates independent disposable databases for three cases:

  1. Apply V1 and original V2, edit V2, and assert that validation reports a checksum mismatch.
  2. Run repair against the edited history. Validation succeeds, but the existing database’s tag remains nullable while a fresh database’s column is not nullable. The old row has null in one database and new in the other.
  3. Restore original V2 and add V3 to backfill data and apply the constraint. Migrate and validate both upgrade and fresh-install paths; the checked nullability, data value and applied migration count now agree.

A standalone validation after creating an unapplied V3 may report that pending migration. The fixture validates restored history before introducing V3, then migrates and validates again. Read the source for the precise ordering rather than treating a validation error as a reason to run repair.

The Flyway guide explains the operational decisions this example illustrates. The tests use H2 SQL; they do not establish production locking, PostgreSQL syntax, or complete schema equivalence.

Expose a test that the normal suite omits

UndiscoveredShould contains a deliberate failing assertion but is outside Surefire’s default class-name patterns. Both normal runs pass eleven tests without discovering it. Explicit selection runs it and fails with DISCOVERY_SENTINEL:

mvn -B -Pboot4 test -Dtest=UndiscoveredShould

The verification script requires that exact test failure. A compilation error or unavailable dependency is not equivalent. The discovery guide describes how to apply this check to an existing repository.

Apply the pattern to your application

Choose the contracts your application needs, preserve a baseline, and run the same meaningful assertions on the target version. These profile runs use independent JVMs and disposable databases; they do not carry one persistent database through a cross-version engine upgrade. Add that path, real database fixtures, JSON contracts, and other integrations when your application requires them.

The Java guide organizes the next steps. This lab’s value is its inspectable code, observed failures, and bounded claims; passing it does not establish that an arbitrary Boot migration is complete.

Revision note: The earlier article described an AI-built migration without a complete retained project. This revision replaces that account with a newly created source package and run evidence. The new record does not validate historical model scores.

Peng Zhou

Peng Zhou

AI Kit Guides is dedicated to helping you navigate the world of AI tools with honest, practical, and beginner-friendly content.

Tags:

#spring boot 4 #spring boot migration #claude code #ai coding agents #java-ai-cluster
Share:

Related Articles

Stay Updated on AI Tools

Get the latest AI tool reviews, tutorials, and comparison guides delivered to your RSS reader.

Subscribe via RSS