chore: migrate to Spring Boot 4#3804
Draft
bdshadow wants to merge 61 commits into
Draft
Conversation
Contributor
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
bdshadow
force-pushed
the
bdshadow/spring-boot-4
branch
7 times, most recently
from
July 22, 2026 09:55
fa9dd06 to
fad86f7
Compare
bdshadow
force-pushed
the
bdshadow/spring-boot-4
branch
from
July 22, 2026 12:10
41d08cc to
df298af
Compare
…etamodel Migrate Jackson 2 -> 3 across the backend: - version catalog + deps use tools.jackson coordinates; drop the duplicate jackson-module-kotlin catalog alias - rewrite imports (databind/core/module/dataformat -> tools.jackson; annotations stay on com.fasterxml) - API changes: JsonDeserializer -> ValueDeserializer, JsonProcessingException -> JacksonException, immutable-mapper builders (jacksonMapperBuilder / JsonMapper.builder), JsonNode.fields() -> properties(), elements() -> values(), TextNode -> StringNode, ObjectNode.set<T>() -> set(), writer(pp) -> writer().with(pp), YAMLFactory.builder() / YAMLWriteFeature - align Jackson to Spring Boot 4's managed version (3.1.4), no ahead-of-BOM override Unblock Hibernate 7 static-metamodel generation under kapt: - prepend the antlr runtime hibernate-jpamodelgen needs to the kapt worker boot classpath (Gradle bundles antlr 4.7.2 via tomlj, which shadows it) - bump hypersistence-utils to the Hibernate 7 variant (hibernate-73)
- core package moves: Job -> core.job, Step -> core.step, JobExecution -> core.job, JobParameters/JobParameter -> core.job.parameters - item API moved to the new infrastructure namespace: org.springframework.batch.item.* -> org.springframework.batch.infrastructure.item.* - RepositoryItemReader now takes (repository, sorts) via constructor (no-arg gone); setSort -> setSorts - JobParameters(Map) -> JobParameters(Set); JobParameter carries its own name - JobLauncher (deprecated) -> JobOperator; run() -> start()
…g Boot 4 Both stem from Spring Framework 7 / Boot 4 adopting JSpecify null-safety: - HibernatePropertiesCustomizer moved org.springframework.boot.autoconfigure.orm.jpa -> org.springframework.boot.hibernate.autoconfigure (autoconfigure split into per-module packages); customize() value type Any? -> Any (non-null) - JpaRepository<X?, Long?> -> JpaRepository<X, Long> (repository T/ID type params are now non-null)
Spring 7's JSpecify null-safety makes HttpEntity<T> require T : Any, so the bodyless-GET pattern HttpEntity<String?>(null, headers) no longer compiles. Use HttpEntity<String>(null, headers) (the constructor body param is @nullable).
Clears the last data-module compile errors so it builds clean: - Hibernate 7: ActionQueue.registerProcess(BeforeTransactionCompletionProcess) -> registerCallback(TransactionCompletionCallbacks.BeforeCompletionCallback) - Hibernate 7 / JPA 3.2: org.hibernate.query.NullPrecedence -> jakarta.persistence.criteria.Nulls - Spring Framework 7: Cache.get<T> now requires T : Any (bound the accessor's T) - Spring Security 7: PasswordEncoder.encode returns @nullable String (assert non-null)
Boot 4 dependencies ship Kotlin metadata the old 2.1.20 compiler cannot read (spring-hateoas 3.1.1 -> metadata 2.4.0, opentelemetry-extension-kotlin 1.63.0 -> 2.3.0). Kotlin 2.3.x reads metadata up to 2.4.0, clearing the resulting unresolved-reference errors and letting extension-kotlin realign with the rest of OpenTelemetry at 1.63.0.
api: - Boot 4 Redis autoconfigure move+rename: RedisAutoConfiguration/RedisProperties -> org.springframework.boot.data.redis.autoconfigure.DataRedis* - JSpecify bounds: ProjectLastModifiedManager<T : Any>, PagedModelWithNextCursor<T : Any> testing: - AutoConfigureMockMvc moved to the new spring-boot-webmvc-test module - Spring 7 MockMvc: MockMultipartHttpServletRequestBuilder no longer extends MockHttpServletRequestBuilder -> addToken generic over AbstractMockHttpServletRequestBuilder<B> - Jackson 3: TypeFactory.defaultInstance() -> createDefaultInstance() - JSpecify: getBean<reified T : Any>, non-null header arg
- RestTemplateBuilder moved org.springframework.boot.web.client -> org.springframework.boot.restclient (added spring-boot-restclient dependency; starter-web no longer pulls it transitively) - EntityScan moved org.springframework.boot.autoconfigure.domain -> org.springframework.boot.persistence.autoconfigure - Spring 7 Kotlin RestTemplate extensions require a non-null reified type (postForObject/getForObject<T>, not <T?>) - Spring Data 4: non-null Page.map element (GlossaryTermService) With this the whole tolgee-platform backend (data, api, testing, app, ee) compiles.
Completes the backend main-source migration (whole platform + billing compiles): - Boot 4 autoconfigure package moves: SecurityProperties -> security.autoconfigure, MultipartConfigFactory -> boot.servlet, actuator Health* -> boot.health.contributor - Ldap/Thymeleaf autoconfigure are no longer on the classpath -> removed the now-dead @SpringBootApplication excludes - Boot 4 removed SecurityProperties.DEFAULT_FILTER_ORDER (inlined -100) - Boot 4 HealthContributor is sealed -> implement only HealthIndicator - Spring 7: WebMvcConfigurer.configureMessageConverters non-null element list; Jackson 3 converter base (AbstractJackson2 -> AbstractJacksonHttpMessageConverter); FilterRegistrationBean filter set via constructor - JSpecify: pagedResponse<T : Any>
- AutoConfigureMockMvc moved to org.springframework.boot.webmvc.test.autoconfigure across all test files; expose spring-boot-webmvc-test as api from :testing so it propagates to consumers' test classpaths - Jackson 3: ObjectMapper().registerKotlinModule() -> jacksonObjectMapper(); JsonNode.map(Function) now shadows Iterable.map -> iterate via values() - Spring 7: MockMvcBuilders.standaloneSetup non-null vararg; HttpHeaders.set takes a single String? (dropped single-element listOf wrappers)
- ee-test needs spring-boot-restclient and jakarta.mail on its own classpath - Spring 7: ResponseEntity(body, headers, status) is ambiguous with a null headers arg -> cast to HttpHeaders?; ClientHttpResponse.getRawStatusCode() removed -> drop the override (getStatusCode covers it)
spring-cloud-azure 5.23.0 registers a KeyVaultEnvironmentPostProcessor that references org.springframework.boot.ConfigurableBootstrapContext, which Boot 4 moved to org.springframework.boot.bootstrap.* -> NoClassDefFoundError on startup. 7.x is the Boot-4-compatible line (references the new package).
…x JPA startup ordering Hibernate 7's model introspection (hibernate-models MapValueSwitch) crashes with IndexOutOfBoundsException on @type JSONB entity fields whose declared type is a custom Map subclass reaching Map<String,V> through a 1-type-arg generic intermediate (CompactSharedMap<V>): it treats CompactSharedMap<V> as the Map and reads type-argument index 1. Fix by removing Map<String,V> from CompactSharedMap and having DescribingDataMap/DescribingRelationsMap implement Map<String,X> directly (inheriting the storage impl), so Hibernate resolves a clean 2-arg Map. Reported upstream: hibernate/hibernate-models#263 Also defer JPA beans that Boot 4 now forces during early web-server startup: - @lazy CurrentDateProvider.entityManager (EntityManager not resolvable yet) - @lazy TestClockHeaderFilter.currentDateProvider (breaks the auditing DateTimeProvider cycle)
Declare the app's primary mapper as JsonMapper so Boot 4's auto-configured @primary jacksonJsonMapper (@ConditionalOnMissingBean(JsonMapper)) backs off, leaving a single primary ObjectMapper for injection.
…ced date Boot 4 instantiates servlet-filter beans during web-server startup, which reaches CurrentDateProvider before Liquibase has migrated. Wire the DateTimeProvider declaratively (dropping the constructor dependency on AuditingHandler that formed a startup cycle) and load the persisted forced date lazily on first read instead of in the constructor.
Hibernate 7 rejects a query with a non-fetch join and no select clause when no result type is supplied. Add an explicit 'select <root>' to the affected repository queries; fetch-join-only queries are unaffected.
MappingJackson2HttpMessageConverter is no longer an injectable bean under Boot 4; add the octet-stream media type via WebMvcConfigurer.extendMessageConverters on the JacksonJsonHttpMessageConverter instead.
Boot 4 split Liquibase auto-configuration into its own module. Without it the EntityManagerFactory depends-on wiring that runs the SpringLiquibase beans before the EMF is never registered, so migrations never execute.
hypersistence-utils 3.15.x clones JSON attributes only via Java serialization and throws NonSerializableObjectException for attributes whose element/value types are not Serializable (e.g. batch jobs' List<BatchTranslationTargetItem>). Register a JsonSerializer that restores the pre-3.15 JSON round-trip clone, via hypersistence-utils.properties.
Hibernate 7's CamelCaseToUnderscoresNamingStrategy inserts an underscore at digit->uppercase boundaries, so key1Id/key2Id now derive key1_id/key2_id instead of key1id/key2id (the actual Liquibase-created columns), making KeysDistance queries fail with 'column ... does not exist'. Pin the columns with @column and align the @table index column lists.
Redisson 4.6.1 registers a new unconditional RedissonAutoConfigurationV4 that creates a StringRedisTemplate and connects to localhost:6379 at startup. The ee test config only excluded V2, so every ee backend test failed to load its context (Connection refused). Exclude V4 too, matching the platform and billing test configs.
Jackson 3 (jackson-module-kotlin) no longer populates val properties declared in a class body (no primary constructor parameter), so such @RequestBody DTOs kept their defaults -- e.g. GlossaryHighlightsRequest.languageTag stayed blank and failed @notblank with 400 (breaking the glossary highlights panel). Move the properties into primary constructors for GlossaryHighlightsRequest, LabelRequest and the QA settings requests.
Hibernate 7 inserts an underscore at the digit->uppercase boundary, deriving table s3_content_storage_config from S3ContentStorageConfig instead of the actual Liquibase-created s3content_storage_config, so any query touching content storage failed with 'relation ... does not exist'. Pin it with @table.
…ate 7 Hibernate 7 rejects referencing a join-fetch alias in WHERE and on-restricted fetch joins, so the latest-merge filter can no longer live in the repository query. Move it onto the Branch.merges collection via @SQLRestriction: it is emitted in the LEFT JOIN's ON clause, so `left join fetch b.merges` loads only the latest merge per branch while branches without merges are still returned.
Jackson 3 requires non-null constructor params without defaults; requests omitting 'force' failed with 'Missing required creator property'. Default enabled/force to false.
Jackson 3 flipped WRITE_DATES_AS_TIMESTAMPS to disabled by default, so dates serialized as ISO strings instead of epoch millis - breaking the API's `number` timestamp contract and activity-log JSONB values. Re-enable it on the primary API JsonMapper and, via a supplier, on hypersistence's JSONB mapper.
Jackson 3's kotlin module does not populate class-body-declared `val`s (no setter) and requires non-defaulted creator properties. Make the S3 storage config's signingRegion/path writable and default SelfHostedEePlanModel's isPayAsYouGo so requests/responses deserialize as before.
…ing Boot 4
Two Hibernate 7 changes and one Jackson 3 change broke the translation list:
- The screenshot-count subquery exposed its inner count() selection to the outer
query ("references a root from a different tree"); use the subquery itself.
- Cursor sort columns now report java.util.Date (not Timestamp) as their type, so
the cursor value parser rejected date sorts with a 500.
- Jackson 3 no longer interns field names, so the reference-equality check for the
unique cursor column always failed, dropping the strict boundary and returning
duplicate keys across pages; compare by value.
Hibernate 7 maps native-query TIMESTAMP columns to java.time.LocalDateTime instead of java.util.Date, so casting the translation-memory suggestion row's updatedAt to Date threw a ClassCastException (500). Read it as LocalDateTime and convert to Date via the system zone, matching how the entity's own Date field round-trips.
…lback Batch chunk processing rolls back to a JDBC savepoint on failure, then commits the FAILED execution record in the same transaction - which requires clearing Hibernate's rollback-only mark. Hibernate 7 moved that mark from a boolean on TransactionDriverControlImpl to TransactionStatus.MARKED_ROLLBACK on the logical connection, and exposes no public API to clear it. Reach the logical connection via the JdbcCoordinator SPI and reset its status to ACTIVE, replacing the old internal-class reflection chain; also unwrap the session to the SharedSessionContractImplementor SPI instead of the internal SessionImpl.
Rebase on main brought in new tests using Boot-3 APIs. Move the AutoConfigureMockMvc import to the Boot 4 package, and convert OrganizationFloorAccessTest to Jackson 3 (tools.jackson kotlin module; iterate JsonNode via values() since Jackson 3 added a member map(Function) that shadows Kotlin's Iterable.map).
liquibase-gradle 2.2.2 uses Project.exec() (removed in Gradle 9) and
liquibase-hibernate6 breaks on Hibernate 7. Bump the plugin to 3.1.0 (first
release with Gradle 9 support) and apply it via the buildscript classpath so
liquibase.Scope resolves at configuration time - the plugins{} block isolates
plugin classloaders. Move liquibase-core to 5.0.3 (already managed by Boot 4)
and the diff ext to liquibase-hibernate7 5.0.3 for Hibernate 7 model
introspection. Adjust the diff DSL for Liquibase 5: changelogFile (kebab-cased
to --changelog-file) and the Boot 4 package for SpringImplicitNamingStrategy.
…efault bug) liquibase-hibernate7 5.0.3 emits a duplicate addDefaultValue(nextval) changeset for every sequence-generated @id column, so diffChangelog never matches the committed schema. Exclude id columns from the migration diff until the upstream bug is fixed. See liquibase/liquibase-hibernate#919
… 7 diff Hibernate 7's model introspection is stricter than 6's: it reads column defaults only from @ColumnDefault (not Kotlin field initializers), and takes column nullability from @column(nullable)/optional rather than the Kotlin type. That surfaced pre-existing gaps between the entities and schema.xml in the migration diff. Declare them explicitly so the model matches the committed schema: - @ColumnDefault on WebhookConfig.enabled/autoDisableNotified/autoDisabled, TranslationMemory.defaultPenalty/writeOnlyReviewed, and KeysDistance.distance - @column(nullable = false) on SsoTenant.domain and Project.is_public - optional = false on TaskKey.task and TaskKey.key
The Hibernate 7 migration diff caught two real gaps between the entities and the schema: - translation_suggestion's author/key/language/project FKs are declared @joincolumn(nullable = false) but the table was created with nullable columns; add the NOT NULL constraints. - the api_key "scopes" and mt_service_config "enabled services" @ElementCollection tables lacked the (owner, element) unique constraint Hibernate maps for a Set; add it (using Hibernate's generated constraint names so the model matches).
Redisson 4.6.1's Kryo5Codec.createKryo takes (ClassLoader, boolean useReferences) instead of (ClassLoader). main added this codec (#3807) against the Boot 3 Redisson, so its override no longer matches once merged with the Boot 4 bump. Update the override to pass the useReferences flag through.
The ee-module Migration Check (not covered by the earlier :data fixes) flagged the ee_subscription table: - includedKeys/includedSeats/keysLimit/seatsLimit/isPayAsYouGo lacked @ColumnDefault, so Hibernate 7 (which reads defaults only from @ColumnDefault, not Kotlin field initializers) saw no default while the schema declares one; declare them to match the schema (-1 limits, true pay-as-you-go). - license_key is @notblank in the entity but the column was created nullable; add the NOT NULL constraint.
springdoc 3.0.3 (the newest Boot 4 line; no newer exists) doesn't fully work with Boot 4 + Jackson 3, and its SpringDocHateoasConfiguration silently backs off, so the generated schema degraded badly: Kotlin property types became `unknown` and lost required flags (~395 properties), and paged responses lost their HAL `_embedded` shape (dropping to a raw `content` field the frontend can't consume). Rebuild what springdoc leaves out, on the swagger ModelResolver's (Jackson 2) mapper: - register the Jackson 2 Kotlin module so swagger can read Kotlin types/nullability; - register SpringDocJackson2HalModule so RepresentationModel/PagedModel render as HAL (`_embedded`/`_links`); - register springdoc's CollectionModelContentConverter to type the `_embedded` items. Also hide the derived StorageConfig.enabled/contentStorageType from the schema (they are server-side computed, not request inputs). Regenerated apiSchema.generated.ts: `unknown` properties 395 -> 0 and `_embedded` restored, verified against a running Boot 4 backend. The app's own runtime serialization uses Jackson 3 and is unaffected.
…oot 4
Spring Boot 4 / Spring Data JPA 4 no longer expose a spy-able EntityManager bean,
so @MockitoSpyBean EntityManager (used to count queries) fails to load the context
("no beans of type EntityManager"). Verify the cache instead through Hibernate's
statement counter (generate_statistics): a cache hit runs no additional statements.
This drops the EntityManager spy entirely and is robust across Spring versions.
Spring Batch 6 (Spring Boot 4) defaults to a non-persistent ResourcelessJobRepository and drops the spring.batch.jdbc schema auto-init, which breaks the migration jobs that rely on persisted job instances to avoid re-running. Switch the batch starter to spring-boot-starter-batch-jdbc, the sanctioned way to keep storing batch metadata in the database (restores both the JDBC repository and schema initialization). See https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-4.0-Migration-Guide#spring-batch Also exclude the Spring Batch 6 batch_job_instance_seq (renamed from batch_job_seq) from the migration diff so it isn't reported as drift.
…ections Importing a screenshot onto a not-yet-persisted key returned 500 under Hibernate 7. KeyScreenshotReference has a @IdClass derived identity (@manytoone @id key/screenshot), and Key.keyScreenshotReferences cascades PERSIST. Because the reference was added to that collection before being persisted, persisting it made Hibernate persist the transient key while generating the reference's composite id, which cascaded back onto the reference mid-generation. Hibernate 7 then injects the deprecated SHORT_CIRCUIT_INDICATOR into the Long id field and fails with PropertyAccessException. Persisting the reference before adding it to the collections generates the derived id with no circular cascade. https://docs.hibernate.org/orm/7.0/migration-guide/
In Redis mode the batch job queue is populated only via the pub/sub round-trip, so the RedisMessageListenerContainer must actually deliver messages. redisJobPubsubContainer uses a single-threaded taskExecutor to keep REMOVE/ADD events in order (#3761). Spring Data Redis 4.1.0 (Boot 4) defaults subscriptionExecutor to the taskExecutor when it is unset, so the blocking subscription task claimed the only thread and no thread was left to dispatch messages — items were delivered only at container shutdown and the queue stayed empty. Set a dedicated SimpleAsyncTaskExecutor as the subscription executor so the single-threaded taskExecutor is used solely for ordered dispatch.
The runE2e task hardcoded Cypress's target to localhost:8201, so running e2e in a git worktree (which publishes the app on a per-worktree TOLGEE_E2E_PORT) hit ECONNREFUSED. Derive both CYPRESS_HOST and CYPRESS_API_URL from TOLGEE_E2E_PORT, defaulting to 8201 so CI is unchanged.
…ngTest AutomationCachingWithRedisTest is annotated @redistest, which is meta- annotated with its own @SpringBootTest. That replaces the parent's @SpringBootTest, dropping the spring.jpa.properties.hibernate.generate_statistics property the test relied on to count executed statements, so the counter stayed 0 and the cache assertion failed. Enable statistics in @beforeeach instead, so it survives the annotation override and works for both the Redis and non-Redis variants.
bdshadow
force-pushed
the
bdshadow/spring-boot-4
branch
from
July 23, 2026 22:00
26d4a9d to
248cf63
Compare
hardDeleteProject never removed task rows, so deleting a project that had tasks aborted on the task.project_id and task.language_id foreign key constraints, leaving the project and its organization half-deleted. Delete tasks (and the rows referencing them: notifications, task_assignees, task_key) before languages, keys, branches and the project.
canUserViewOrPublic short-circuited to true for admins and supporters without checking the organization exists. When a user's preferred organization was soft-deleted, preferred-organization resolution therefore skipped the refresh path for admins and then failed in getPrivateModel (which filters deleted organizations), returning no preferred organization and a 403. Require the organization to exist for admins and supporters too.
Commit 8bc9cb7 aligned the ee_subscription defaults to the schema's -1 / true to satisfy the Hibernate 7 migration check, changing the runtime defaults. Restore the values that main uses (0 limits, isPayAsYouGo=false) on both the field initializer and @ColumnDefault, and migrate the column defaults so the schema matches.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Migrates the Tolgee platform backend from Spring Boot 3 to Spring Boot 4 (Spring Framework 7). This is a large, cross-cutting framework upgrade; opening as a draft while the test suite is brought fully green.
Core version bumps:
com.fasterxml) → 3 (tools.jackson)Notable migration work
tools.jacksoncoordinates and APIs; annotations remain oncom.fasterxml.jackson.annotation.spring-boot-liquibasemodule; added it so the customSpringLiquibasebeans run before theEntityManagerFactory.RedissonAutoConfigurationV4, exclude the new unconditional V4 autoconfig, addredisson-spring-cache(split out of core), and declaremicrometer-coreexplicitly (Redisson 4 marks it optional).selectclauses to join queries that no longer infer a result type.Serializablevalues (hypersistence-utils 3.15 dropped that fallback).dateTimeProviderRefand defer DB access out of bean construction, since Boot 4 creates servlet-filter beans before Liquibase migrates.Upstream bug found
Hibernate 7's
hibernate-modelscrashes withIndexOutOfBoundsExceptionon a JSONB entity attribute whose type is aMapsubclass reachingMap<K,V>through a one-type-parameter generic intermediate. Reported upstream: hibernate/hibernate-models#263 (worked around inCompactSharedMap).Status
BatchJobsGeneralWithoutRedisTest)BatchJobsGeneralWithRedisTest,BatchJobRecoveryTest, …) are pre-existing flaky/timing-sensitive tests still being verifiedRelated
The matching
billingbranch (bdshadow/spring-boot-4) carries the corresponding billing changes.