Repository navigation
Commit fa9afda
authored
fix: Liquibase coexistence + NCDF on Spring Boot 3.5.x without liquibase-core (KOJAK-80) (#42)
## Summary
Closes GitHub issue #38 and JIRA KOJAK-80. Also 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) — discovered during the work.
### NCDF guard — fix for the related startup bug
- Move `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`.
### Issue #38 Mode 1 / KOJAK-80 §1 — host's `liquibase` bean shadowed
- 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`) shadows Spring Boot's
type-based `@ConditionalOnMissingBean(SpringLiquibase)` guard, silently
suppressing the host application's own changelog.
- **Decision re. ordering direction**: KOJAK-80 suggested
`@AutoConfigureBefore` as the leading candidate. The PR uses
`@AutoConfigureAfter` — `Before` would 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.
- **No `@DependsOn` KDoc**: KOJAK-80 also asked for a note about host
beans declaring `@DependsOn("okapiPostgresLiquibase")` to handle FKs
into `okapi_outbox`. That motivating case is an anti-pattern (the purger
needs to delete `okapi_outbox` rows; 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.
- Keep `@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
- 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 plan
Adopts the testing pattern PR #41 introduced for the analogous
Micrometer ordering bug (real Spring Boot autoconfig + reflection
meta-test).
- **Real autoconfig coexistence E2E** (Postgres + MySQL): pulls Spring
Boot's `LiquibaseAutoConfiguration` via `resolveSpringBootClass(...)`,
sets `spring.liquibase.change-log`, asserts both beans register and both
changelogs run.
- **Multi-DataSource E2E** (KOJAK-80 §2): two real Postgres containers,
`okapi.datasource-qualifier=secondaryDs`, asserts `okapi_outbox` lands
on the secondary and not the primary.
- **Structural pins** (`LiquibaseAutoConfigurationTest`):
- No `SpringLiquibase` return type on `OutboxAutoConfiguration` /
`PostgresStoreConfiguration` / `MysqlStoreConfiguration` (NCDF guard
would otherwise return).
- Class-level `@ConditionalOnClass(SpringLiquibase)` on both new
Liquibase configs.
- Name-based `@ConditionalOnMissingBean` (type-based would re-introduce
Mode 1).
- `@AutoConfigureAfter` annotation is structurally sound; declared names
resolve when Spring Boot Liquibase autoconfig is on classpath.
- **Log capture**: `LiquibaseDisabledNotice` WARN message captured via
logback `ListAppender` — deletion of the warn body breaks the test, not
just deletion of the class.
- **User override**: end-to-end test for
`@Bean("okapiPostgresLiquibase")` taking precedence over okapi's
default.
### Verified
- [x] `./gradlew :okapi-spring-boot:test` — 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 — each fails a distinct
test in the new suite:
- Remove 3.x `@AutoConfigureAfter` path → meta-test failure on 3.5.12
- Remove `@AutoConfigureAfter` entirely → unconditional structural check
failure
- Switch to type-based `@ConditionalOnMissingBean` → name-pin failure
- Move Liquibase bean back into `PostgresStoreConfiguration` → NCDF
structural guard failure
- [x] Empirically verified on `okapi-autocomplete-test` (Spring Boot
3.5.7, no `liquibase-core` on classpath) — app starts cleanly; the
original `NoClassDefFoundError` is gone.1 parent 6cadb48 commit fa9afda
20 files changed
Lines changed: 821 additions & 65 deletions
File tree
- gradle
- okapi-benchmarks/src/jmh/kotlin/com/softwaremill/okapi/benchmarks/support
- okapi-integration-tests/src/test/kotlin/com/softwaremill/okapi/test
- support
- transaction
- okapi-postgres/src/main/resources/com/softwaremill/okapi/db/postgres
- okapi-spring-boot
- src
- main
- kotlin/com/softwaremill/okapi/springboot
- resources/META-INF
- spring
- test
- kotlin/com/softwaremill/okapi/springboot
- resources/com/softwaremill/okapi/springboot
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
99 | 99 | | |
100 | 100 | | |
101 | 101 | | |
102 | | - | |
| 102 | + | |
103 | 103 | | |
104 | 104 | | |
105 | 105 | | |
| |||
273 | 273 | | |
274 | 274 | | |
275 | 275 | | |
276 | | - | |
| 276 | + | |
277 | 277 | | |
278 | 278 | | |
279 | 279 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
14 | 14 | | |
15 | 15 | | |
16 | 16 | | |
| 17 | + | |
17 | 18 | | |
18 | 19 | | |
19 | 20 | | |
| |||
53 | 54 | | |
54 | 55 | | |
55 | 56 | | |
| 57 | + | |
56 | 58 | | |
57 | 59 | | |
58 | 60 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
47 | 47 | | |
48 | 48 | | |
49 | 49 | | |
50 | | - | |
| 50 | + | |
51 | 51 | | |
52 | 52 | | |
53 | 53 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
40 | 40 | | |
41 | 41 | | |
42 | 42 | | |
43 | | - | |
| 43 | + | |
44 | 44 | | |
45 | 45 | | |
46 | 46 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
105 | 105 | | |
106 | 106 | | |
107 | 107 | | |
108 | | - | |
| 108 | + | |
109 | 109 | | |
110 | 110 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
139 | 139 | | |
140 | 140 | | |
141 | 141 | | |
142 | | - | |
| 142 | + | |
143 | 143 | | |
144 | 144 | | |
0 commit comments