fix(spring-config): declare kotlin-reflect in okapi-spring-boot - #89
Merged
Conversation
Spring binds this module's @ConfigurationProperties data classes through their Kotlin primary constructor, which it can only identify via Kotlin reflection: every parameter has a default, so kotlinc also emits a synthetic no-arg constructor, and Spring's value-object binding only applies when there is a single parameterized constructor. Without kotlin-reflect binding falls back to JavaBean binding and fails on the first okapi.* property with "No setter found" (the properties are all `val`). kotlin-reflect was reaching consumers only as a transitive of jackson-module-kotlin, declared by okapi-http and okapi-kafka alone. Any application implementing its own MessageDeliverer therefore failed to start as soon as it set any okapi.* property; the store modules do not help, since okapi-postgres lost that transitive in the plain-JDBC rewrite. Verified with a plain Java Spring Boot consumer (single DataSource, custom MessageDeliverer, okapi.processor.interval=PT2S): fails to start before this change, starts and applies the property after it. Claude-Session: https://claude.ai/code/session_013gbZvpKg73nNXtucDMY88d
Contributor
There was a problem hiding this comment.
Pull request overview
This pull request fixes Spring Boot @ConfigurationProperties binding failures in consumer applications by ensuring kotlin-reflect is present on the runtime classpath when using okapi-spring-boot.
Changes:
- Declare
kotlin-reflectas a dependency ofokapi-spring-bootso Spring can resolve Kotlin primary constructors during configuration-properties binding. - Add in-file rationale documenting why the dependency is required and why alternatives (like
@ConstructorBinding) don’t address the underlying problem.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
The full story (transitive history via jackson-module-kotlin, the rejected @ConstructorBinding alternative) lives in #88 and the PR description; the build file only needs why the dependency is required and that it is not optional. Claude-Session: https://claude.ai/code/session_013gbZvpKg73nNXtucDMY88d
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.
Fixes #88.
okapi-spring-bootbinds its@ConfigurationPropertiesclasses through the Kotlin primary constructor, which Spring can only identify via Kotlin reflection. No okapi module declaredkotlin-reflect, so whether a consumer application starts depended on whether an unrelated dependency happened to drag it onto the runtime classpath.Why the primary constructor cannot be resolved without it
All four properties classes (
OkapiProperties+ nestedLiquibase,OkapiMetricsProperties,OutboxProcessorProperties,OutboxPurgerProperties) are data classes whose parameters all have defaults, so kotlinc also emits a synthetic no-arg constructor:Spring Boot uses value-object binding only when there is a single parameterized constructor; otherwise it must pick the Kotlin primary constructor. Without
kotlin-reflectit falls back to JavaBean binding, finds no setters (allval), and fails.Who was affected
kotlin-reflectreached consumers only as a transitive ofjackson-module-kotlin, declared byokapi-httpandokapi-kafkaalone. Any application using its ownMessageDelivererwas affected — the store modules do not help, sinceokapi-postgreslost that transitive in the plain-JDBC rewrite (it still had it in 0.1.0/0.3.0). Java consumers are the most exposed, having no route by whichkotlin-reflectcould arrive by accident.Verification
A plain Java Spring Boot service (single DataSource, custom
MessageDeliverer,okapi-core+okapi-spring-boot+okapi-postgres, one ordinaryokapi.processor.interval: PT2S):APPLICATION FAILED TO START—No setter found for property: intervalOutbox processor started [interval=PT2S, batchSize=10, concurrency=1]Also run on this branch:
ktlintFormat, fullktlintCheck,:okapi-spring-boot:test— all green. The generated POM now carrieskotlin-reflectatruntimescope, so consumers get it transitively without declaring anything.Note on test coverage
This cannot be covered by a test in this repository:
kotlin-reflectis on the test runtime classpath of every module (transitively, via Kotest), so property-binding tests pass with or without the fix. That is precisely why it went unnoticed. The failure is only observable from a consumer build, which is how it was reproduced and verified above. Happy to add a build-level guard asserting the dependency stays onokapi-spring-boot's runtime classpath if you would like one — say the word and I will push it.Rejected alternative
Annotating the primary constructor with
@ConstructorBindingdoes not work: withoutkotlin-reflectSpring cannot tell the constructors apart, Kotlin copies the annotation onto the synthetic no-arg constructor, and startup fails earlier withdeclares @ConstructorBinding on a no-args constructor. Tested.https://claude.ai/code/session_013gbZvpKg73nNXtucDMY88d