fix: Liquibase coexistence + NCDF on Spring Boot 3.5.x without liquibase-core (KOJAK-80)#42
Open
endrju19 wants to merge 1 commit into
Open
fix: Liquibase coexistence + NCDF on Spring Boot 3.5.x without liquibase-core (KOJAK-80)#42endrju19 wants to merge 1 commit into
endrju19 wants to merge 1 commit into
Conversation
…Spring Boot 3.5.x without liquibase-core Closes #38. Fixes a related undocumented NoClassDefFoundError that prevented startup on Spring Boot 3.5.x consumers without `liquibase-core` on the classpath (e.g. Flyway-only users). ## What changed ### Production code - Extract `okapi*Liquibase` factory methods into dedicated `PostgresLiquibaseConfiguration` / `MysqlLiquibaseConfiguration` inner classes with class-level `@ConditionalOnClass(SpringLiquibase)`. Spring evaluates the class-level condition via string-name lookup before any method introspection, so `Class.getDeclaredMethods()` is never called on a class whose `SpringLiquibase` return type would trigger `NoClassDefFoundError`. - Add `@AutoConfigureAfter(name = [3.x path, 4.x path])` on `OutboxAutoConfiguration` so Spring Boot's `LiquibaseAutoConfiguration` registers its own `liquibase` bean first. Without this, okapi's `okapiPostgresLiquibase` (typed `SpringLiquibase`) would shadow Spring Boot's type-based `@ConditionalOnMissingBean(SpringLiquibase)` guard, silently suppressing the host application's own changelog (issue #38 Mode 1). - Keep `@ConditionalOnMissingBean(name = "okapiPostgresLiquibase")` (name-based, not type-based) so a user-supplied `@Bean SpringLiquibase liquibase()` coexists with okapi's bean instead of being skipped, while `@Bean("okapiPostgresLiquibase")` still cleanly overrides okapi's default. - New `okapi.liquibase.enabled` property (default `true`) for explicit opt-out when the host app includes okapi's changelog from its own master changelog. - New `LiquibaseDisabledNotice` inner config logs a WARN-level breadcrumb on `enabled=false`, linking a future "relation okapi_outbox does not exist" runtime error back to the startup decision. ### Test coverage Adopts the testing pattern PR #41 introduced for the analogous Micrometer ordering bug (uses `spring-boot-starter-actuator` + reflection meta-test that fails when the declared class names don't resolve). - `LiquibaseE2ETest`: new `coexistence with the host application's own SpringLiquibase via Spring Boot autoconfig` test on both Postgres and MySQL — pulls in Spring Boot's real `LiquibaseAutoConfiguration` via `resolveSpringBootClass(...)`, sets `spring.liquibase.change-log`, and verifies both beans register and both changelogs run. Plus a `multi-datasource` test exercising `okapi.datasource-qualifier`. - `LiquibaseAutoConfigurationTest`: structural assertions pin the architectural decisions (no SpringLiquibase return type on unguarded classes; class-level `@ConditionalOnClass(SpringLiquibase)` on both new config classes; name-based `@ConditionalOnMissingBean`) plus a reflection meta-test for the `@AutoConfigureAfter` contract. Captures the `LiquibaseDisabledNotice` WARN via logback's `ListAppender` so removing the log body breaks the test, not just removing the class. User name-based override is exercised end-to-end. - Slice tests (`DataSourceQualifierAutoConfigurationTest`, `OutboxProcessorAutoConfigurationTest`, `OutboxPurgerAutoConfigurationTest`): add `okapi.liquibase.enabled=false` so they don't run Liquibase against the fake `SimpleDriverDataSource`. - Empirically verified on Spring Boot 3.5.12 (CI matrix) and 4.0.6 (default). ## Test plan - [x] `./gradlew :okapi-spring-boot:test` on Spring Boot 4.0.6 (default) - [x] `./gradlew :okapi-spring-boot:test -PspringBootVersion=3.5.12 -PspringVersion=6.2.17` - [x] `./gradlew :okapi-spring-boot:ktlintCheck` - [x] Sanity-checked four likely regressions: removing the 3.x `@AutoConfigureAfter` path, removing the annotation entirely, switching to type-based `@ConditionalOnMissingBean`, and moving the Liquibase bean back into `PostgresStoreConfiguration` — each one fails a distinct test in the new suite.
5647152 to
d4720f5
Compare
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
Closes GitHub issue #38 and JIRA KOJAK-80. Also fixes a related undocumented
NoClassDefFoundErrorthat prevented startup on Spring Boot 3.5.x consumers withoutliquibase-coreon the classpath (e.g. Flyway-only users) — discovered during the work.NCDF guard — fix for the related startup bug
okapi*Liquibasefactory methods into dedicatedPostgresLiquibaseConfiguration/MysqlLiquibaseConfigurationinner classes with class-level@ConditionalOnClass(SpringLiquibase). Spring evaluates the class-level condition via string-name lookup before any method introspection, soClass.getDeclaredMethods()is never called on a class whoseSpringLiquibasereturn type would triggerNoClassDefFoundError.Issue #38 Mode 1 / KOJAK-80 §1 — host's
liquibasebean shadowed@AutoConfigureAfter(name = [3.x path, 4.x path])onOutboxAutoConfigurationso Spring Boot'sLiquibaseAutoConfigurationregisters its ownliquibasebean first. Without this, okapi'sokapiPostgresLiquibase(typedSpringLiquibase) shadows Spring Boot's type-based@ConditionalOnMissingBean(SpringLiquibase)guard, silently suppressing the host application's own changelog.@AutoConfigureBeforeas the leading candidate. The PR uses@AutoConfigureAfter—Beforewould put okapi's bean first and trigger the exact shadowing it's meant to prevent (Spring Boot's@ConditionalOnMissingBean(SpringLiquibase)is type-based and would skip its own bean if okapi's is registered first). Rationale documented in the KDoc and the JIRA comment on KOJAK-80.@DependsOnKDoc: KOJAK-80 also asked for a note about host beans declaring@DependsOn("okapiPostgresLiquibase")to handle FKs intookapi_outbox. That motivating case is an anti-pattern (the purger needs to deleteokapi_outboxrows; a host FK would block it and stall the queue) — documenting it would encourage misuse. The 99.999% of users whose changelogs don't reference okapi's tables get a working setup with no extra ceremony. Decision logged on KOJAK-80.@ConditionalOnMissingBean(name = ...)(not type-based) so user-supplied@Bean SpringLiquibase liquibase()coexists with okapi's bean, while@Bean("okapiPostgresLiquibase")still cleanly overrides okapi's default.Opt-out
okapi.liquibase.enabledproperty (defaulttrue) for explicit opt-out when the host app includes okapi's changelog from its own master changelog.LiquibaseDisabledNoticeinner config logs a WARN-level breadcrumb onenabled=false, linking a future "relation okapi_outbox does not exist" runtime error back to the startup decision.Test plan
Adopts the testing pattern PR #41 introduced for the analogous Micrometer ordering bug (real Spring Boot autoconfig + reflection meta-test).
LiquibaseAutoConfigurationviaresolveSpringBootClass(...), setsspring.liquibase.change-log, asserts both beans register and both changelogs run.okapi.datasource-qualifier=secondaryDs, assertsokapi_outboxlands on the secondary and not the primary.LiquibaseAutoConfigurationTest):SpringLiquibasereturn type onOutboxAutoConfiguration/PostgresStoreConfiguration/MysqlStoreConfiguration(NCDF guard would otherwise return).@ConditionalOnClass(SpringLiquibase)on both new Liquibase configs.@ConditionalOnMissingBean(type-based would re-introduce Mode 1).@AutoConfigureAfterannotation is structurally sound; declared names resolve when Spring Boot Liquibase autoconfig is on classpath.LiquibaseDisabledNoticeWARN message captured via logbackListAppender— deletion of the warn body breaks the test, not just deletion of the class.@Bean("okapiPostgresLiquibase")taking precedence over okapi's default.Verified
./gradlew :okapi-spring-boot:test— Spring Boot 4.0.6 (default)./gradlew :okapi-spring-boot:test -PspringBootVersion=3.5.12 -PspringVersion=6.2.17./gradlew :okapi-spring-boot:ktlintCheck@AutoConfigureAfterpath → meta-test failure on 3.5.12@AutoConfigureAfterentirely → unconditional structural check failure@ConditionalOnMissingBean→ name-pin failurePostgresStoreConfiguration→ NCDF structural guard failureokapi-autocomplete-test(Spring Boot 3.5.7, noliquibase-coreon classpath) — app starts cleanly; the originalNoClassDefFoundErroris gone.