Skip to content

Commit a179720

Browse files
committed
feat: upgrade to Spring Boot 4.0 with Jackson 3 as the default
- Bump to Spring Boot 4.0.6 / Kotlin 2.2.21; Jackson 3 versions come from the BOM. - util: Jackson 3 becomes the runtime default (api), Jackson 2 is compileOnly; createDataSource defaults to the Jackson 3 handler. - spring starter: JsonHandler bean prefers Jackson 3, falls back to Jackson 2 (spring-boot-jackson2); DataMessageMapping doc references tools.jackson. - spring tests use Logback only (drop slf4j-simple) with a quiet logback-test.xml; add kotest SpringExtension + a context test asserting the Jackson 3 handler. - Docs: README compatibility table + MIGRATION.md (2.x/Boot 3.5 -> 3.x/Boot 4.0). Depends on the ChannelSendOperator removal (PR #30) landing on main first.
1 parent ce1f291 commit a179720

12 files changed

Lines changed: 206 additions & 50 deletions

File tree

AGENTS.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ Guidance for AI coding assistants working in this repository.
66

77
`DataSource-Extensions` is a multi-module Gradle (Kotlin DSL) library that wraps Caplin's `com.caplin.platform.integration.java:datasource` SDK with modern reactive APIs and a Spring Boot starter. Kotlin Coroutines `Flow` is the canonical internal representation; Java `Flow.Publisher` and Reactive Streams `Publisher` variants are thin adapters over it.
88

9-
JDK 17. Spring Boot pinned to 3.5.x and Kotlin to 2.2.x (see `gradle/libs.versions.toml`). The `common-library` convention plugin applies `io.spring.dependency-management` and overrides Spring's BOM `kotlin.version` to our catalog value — without this, transitive Jackson updates raise `kotlin-stdlib` past what the compiler can read.
9+
JDK 17. Two parallel release lines (see the compatibility table in `README.md`): **`main` targets Spring Boot 4.0.x** (Jackson 3 is the default JSON binding; Jackson 2 is a `compileOnly` opt-in), and **`springboot-3.5.x` is the Spring Boot 3.5.x maintenance branch** (Jackson 2 default). Kotlin pinned to 2.2.21 (see `gradle/libs.versions.toml`). The `common-library` convention plugin applies `io.spring.dependency-management` and overrides Spring's BOM `kotlin.version` to our catalog value — without this, transitive Jackson updates raise `kotlin-stdlib` past what the compiler can read.
10+
11+
`datasourcex-util` ships both Jackson serialization layers under `serialization/{jackson2,jackson3}` (mirrored serializers + a `JsonHandler` each, sharing zjsonpatch for RFC 6902 diff/patch). On `main`, Jackson 3 (`tools.jackson.*`) is the runtime default and Jackson 2 (`com.fasterxml.jackson.*`) is `compileOnly`; on `springboot-3.5.x` it's the reverse. The Spring starter's `JsonHandler` bean auto-selects Jackson 3 when present and falls back to Jackson 2 (`DataSourceAutoConfiguration`).
1012

1113
## Common commands
1214

MIGRATION.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# Migration guide
2+
3+
## 2.x (Spring Boot 3.5) → 3.x (Spring Boot 4.0)
4+
5+
Version `3.x` targets **Spring Boot 4.0** and makes **Jackson 3** (`tools.jackson.*`) the default
6+
JSON binding, matching Spring Boot 4's own default. To stay on Spring Boot 3.5, remain on the
7+
[`2.x` line](./README.md#compatibility) (branch `springboot-3.5.x`).
8+
9+
### Prerequisites
10+
11+
- Spring Boot **4.0.x**
12+
- Kotlin **2.2.21+**
13+
- JDK **17+** (unchanged)
14+
15+
### 1. Bump the dependency
16+
17+
```kotlin
18+
dependencies {
19+
implementation("com.caplin.integration.datasourcex:spring-boot-starter-datasource:3.+")
20+
// or, for the reactive / util modules:
21+
implementation("com.caplin.integration.datasourcex:datasourcex-kotlin:3.+")
22+
}
23+
```
24+
25+
### 2. Jackson 3 is now the default
26+
27+
Spring Boot 4 auto-configures a Jackson 3 `tools.jackson.databind.ObjectMapper`, and the starter
28+
wires a Jackson-3-backed `JsonHandler` onto the DataSource by default. For most applications no
29+
change is required — plain POJOs serialize as before.
30+
31+
If you registered **custom Jackson 2 modules, serializers, or `ObjectMapper` customizers**, port them
32+
to Jackson 3. See the
33+
[Jackson 3 release notes](https://github.com/FasterXML/jackson/wiki/Jackson-Release-3.0); the main
34+
changes are:
35+
36+
- group/package `com.fasterxml.jackson.*``tools.jackson.*` (annotations stay on
37+
`com.fasterxml.jackson`),
38+
- `JsonSerializer` / `JsonDeserializer``ValueSerializer` / `ValueDeserializer`,
39+
`SerializerProvider``SerializationContext`, `JsonMappingException``DatabindException`,
40+
- the `ObjectMapper` is immutable — build it via `JsonMapper.builder()…build()`.
41+
42+
### 3. Keeping Jackson 2 (optional)
43+
44+
To keep using Jackson 2 for DataSource JSON, add Spring Boot's (deprecated) Jackson 2 module and
45+
define your own `JsonHandler` bean. The starter's handler is `@ConditionalOnMissingBean`, so yours
46+
takes precedence:
47+
48+
```kotlin
49+
// build.gradle.kts
50+
implementation("org.springframework.boot:spring-boot-jackson2")
51+
```
52+
53+
```kotlin
54+
import com.caplin.datasource.messaging.json.JsonHandler
55+
import com.caplin.integration.datasourcex.util.SimpleDataSourceFactory
56+
import com.fasterxml.jackson.databind.ObjectMapper
57+
import org.springframework.context.annotation.Bean
58+
59+
@Bean
60+
fun dataSourceJsonHandler(objectMapper: ObjectMapper): JsonHandler<*> =
61+
SimpleDataSourceFactory.createJackson2JsonHandler(objectMapper)
62+
```
63+
64+
Outside Spring, `SimpleDataSourceFactory.createDataSource(...)` now defaults to the Jackson 3
65+
handler; pass `SimpleDataSourceFactory.defaultJackson2JsonHandler` (or your own) explicitly to keep
66+
Jackson 2. On the `3.x` line the Jackson 2 artifacts are `compileOnly` in `datasourcex-util`, so add
67+
them to your own classpath if you use the Jackson 2 helpers directly.
68+
69+
### 4. Versioning note
70+
71+
The published `.api` surface is unchanged between `2.x` and `3.x`; the breaking change is the Spring
72+
Boot 4 / Jackson 3 runtime baseline, which is why this is a major version bump.

README.md

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,19 @@
11
# Extensions for DataSource
22

3-
Requires Kotlin 2.2 or later.
3+
Requires Kotlin 2.2 or later and JDK 17 or later.
4+
5+
## Compatibility
6+
7+
This library is maintained in two parallel lines — choose the version that matches your Spring Boot
8+
major version:
9+
10+
| Library version | Branch | Spring Boot | Default JSON binding | Kotlin |
11+
|------------------|---------------------|-------------|---------------------------------------------------------------------------------------------------------------|---------|
12+
| `3.x` | `main` | 4.0.x | Jackson 3 (Jackson 2 available via [`spring-boot-jackson2`](https://docs.spring.io/spring-boot/reference/features/json.html)) | 2.2.21+ |
13+
| `2.x` | `springboot-3.5.x` | 3.5.x | Jackson 2 (Jackson 3 available by adding the `tools.jackson` dependencies) | 2.2+ |
14+
15+
Upgrading from `2.x` (Spring Boot 3.5) to `3.x` (Spring Boot 4.0)? See the
16+
[migration guide](./MIGRATION.md).
417

518
## Reactive
619

@@ -32,7 +45,7 @@ Then refer to the documentation:
3245
## Spring
3346

3447
This module provides a starter for integrating Caplin DataSource with your
35-
[Spring Boot 3.5](https://spring.io/projects/spring-boot) application, and integration with
48+
[Spring Boot 4.0](https://spring.io/projects/spring-boot) application, and integration with
3649
[Spring Messaging](https://docs.spring.io/spring-boot/docs/current/reference/html/messaging.html)
3750
for publishing data from annotated functions.
3851

gradle/libs.versions.toml

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
[versions]
2-
springBoot = "3.5.14"
3-
kotlin = "2.2.0"
2+
springBoot = "4.0.6"
3+
kotlin = "2.2.21"
44
kotlinCollectionsImmutable = "0.4.0"
5-
jackson3 = "3.1.3"
65
zjsonpatch = "0.6.2"
76
jmh = "1.37"
87
jmh-plugin = "0.7.3"
@@ -31,8 +30,6 @@ spring-dependency-management-plugin = "1.1.7"
3130

3231
[libraries]
3332
kotlin-collections-immutable = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version.ref = "kotlinCollectionsImmutable" }
34-
jackson3-databind = { module = "tools.jackson.core:jackson-databind", version.ref = "jackson3" }
35-
jackson3-module-kotlin = { module = "tools.jackson.module:jackson-module-kotlin", version.ref = "jackson3" }
3633
zjsonpatch = { module = "io.github.vishwakarma:zjsonpatch", version.ref = "zjsonpatch" }
3734
kotlinpoet = { module = "com.squareup:kotlinpoet", version.ref = "kotlinpoet" }
3835
fory-core = { module = "org.apache.fory:fory-core", version.ref = "fory" }
@@ -46,6 +43,7 @@ datasource = { module = "com.caplin.platform.integration.java:datasource", versi
4643
# Test Dependencies
4744
kotest-runner = { module = "io.kotest:kotest-runner-junit5", version.ref = "kotest" }
4845
kotest-assertions = { module = "io.kotest:kotest-assertions-core", version.ref = "kotest" }
46+
kotest-extensions-spring = { module = "io.kotest:kotest-extensions-spring", version.ref = "kotest" }
4947
turbine = { module = "app.cash.turbine:turbine", version.ref = "turbine" }
5048
mockk = { module = "io.mockk:mockk", version.ref = "mockk" }
5149

spring/build.gradle.kts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,22 @@ dependencies {
1414
implementation("io.projectreactor.kotlin:reactor-kotlin-extensions")
1515
implementation("org.springframework.boot:spring-boot-starter")
1616
implementation("org.springframework.boot:spring-boot-starter-json")
17-
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
17+
implementation("tools.jackson.module:jackson-module-kotlin")
18+
// Jackson 2 is only needed to compile the jackson2 fallback in DataSourceAutoConfiguration; it is
19+
// present at runtime only if the consumer adds spring-boot-jackson2.
20+
compileOnly("com.fasterxml.jackson.core:jackson-databind")
1821
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
1922
implementation("org.slf4j:slf4j-api")
2023

2124
kapt(libs.spring.boot.configuration.processor)
2225

23-
testRuntimeOnly("org.slf4j:slf4j-simple")
2426
testImplementation(libs.mockk)
2527
testImplementation(libs.kotest.assertions)
2628
testImplementation(libs.kotest.runner)
2729
testImplementation(libs.turbine)
30+
// Spring-context test of the autoconfiguration (kotest SpringExtension + @SpringBootTest).
31+
testImplementation("org.springframework.boot:spring-boot-test")
32+
testImplementation(libs.kotest.extensions.spring)
2833
}
2934

3035
val prepareReadme =

spring/src/main/kotlin/com/caplin/integration/datasourcex/spring/annotations/DataMessageMapping.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ annotation class DataMessageMapping(
2020
* Methods annotated with this message should return one or more objects to be serialized to
2121
* JSON.
2222
*
23-
* The JSON handler installed in [DataSource] will be used. By default, this is the
24-
* [com.fasterxml.jackson.databind.ObjectMapper] provided by Spring Boot.
23+
* The JSON handler installed in [DataSource] will be used. By default, this is backed by the
24+
* [tools.jackson.databind.ObjectMapper] provided by Spring Boot.
2525
*/
2626
JSON,
2727

spring/src/main/kotlin/com/caplin/integration/datasourcex/spring/internal/DataSourceAutoConfiguration.kt

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,19 @@ import com.caplin.integration.datasourcex.util.SimpleDataSourceConfig.Discovery
99
import com.caplin.integration.datasourcex.util.SimpleDataSourceConfig.Peer
1010
import com.caplin.integration.datasourcex.util.SimpleDataSourceFactory.createDataSource
1111
import com.caplin.integration.datasourcex.util.SimpleDataSourceFactory.createJackson2JsonHandler
12+
import com.caplin.integration.datasourcex.util.SimpleDataSourceFactory.createJackson3JsonHandler
1213
import com.caplin.integration.datasourcex.util.getLogger
13-
import com.fasterxml.jackson.databind.ObjectMapper
14+
import com.fasterxml.jackson.databind.ObjectMapper as Jackson2ObjectMapper
1415
import java.nio.file.Paths
1516
import java.util.UUID
1617
import java.util.logging.Logger
1718
import org.springframework.beans.factory.annotation.Value
1819
import org.springframework.boot.autoconfigure.AutoConfiguration
20+
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass
1921
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
2022
import org.springframework.boot.context.properties.EnableConfigurationProperties
2123
import org.springframework.context.annotation.Bean
24+
import tools.jackson.databind.ObjectMapper as Jackson3ObjectMapper
2225

2326
@AutoConfiguration
2427
@EnableConfigurationProperties(DataSourceConfigurationProperties::class)
@@ -31,9 +34,20 @@ internal class DataSourceAutoConfiguration {
3134
internal const val DEFAULT_DATASOURCE_NAME = "caplin-adapter"
3235
}
3336

37+
// Prefer Jackson 3 (the Spring Boot 4 default). Falls back to Jackson 2 only when Jackson 3 is
38+
// absent and a Jackson 2 ObjectMapper is present (e.g. the consumer added spring-boot-jackson2).
39+
// Both are @ConditionalOnMissingBean(JsonHandler) so a consumer-defined JsonHandler wins
40+
// outright.
3441
@Bean
35-
@ConditionalOnMissingBean
36-
fun jsonHandler(objectMapper: ObjectMapper): JsonHandler<*> =
42+
@ConditionalOnClass(Jackson3ObjectMapper::class)
43+
@ConditionalOnMissingBean(JsonHandler::class)
44+
fun jackson3JsonHandler(objectMapper: Jackson3ObjectMapper): JsonHandler<*> =
45+
createJackson3JsonHandler(objectMapper)
46+
47+
@Bean
48+
@ConditionalOnClass(Jackson2ObjectMapper::class)
49+
@ConditionalOnMissingBean(JsonHandler::class)
50+
fun jackson2JsonHandler(objectMapper: Jackson2ObjectMapper): JsonHandler<*> =
3751
createJackson2JsonHandler(objectMapper)
3852

3953
@Bean

spring/src/main/kotlin/com/caplin/integration/datasourcex/spring/internal/DataSourcePayloadReturnValueHandler.kt

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ import org.reactivestreams.Publisher
88
import org.springframework.core.MethodParameter
99
import org.springframework.core.ReactiveAdapterRegistry
1010
import org.springframework.core.ResolvableType
11-
import org.springframework.http.server.reactive.ChannelSendOperator
12-
import org.springframework.lang.Nullable
1311
import org.springframework.messaging.Message
1412
import org.springframework.messaging.handler.invocation.reactive.HandlerMethodReturnValueHandler
1513
import org.springframework.util.Assert
@@ -32,26 +30,18 @@ internal class DataSourcePayloadReturnValueHandler(
3230
returnType: MethodParameter,
3331
message: Message<*>,
3432
): Mono<Void> {
35-
if (returnValue == null) {
36-
val responseRef = getResponseReference(message)
37-
responseRef?.set(emptyFlow())
38-
}
39-
40-
val content = getContent(returnValue, returnType)
41-
return ChannelSendOperator(content) { publisher -> handleContent(publisher.asFlow(), message) }
33+
val responseRef = getResponseReference(message)
34+
checkNotNull(responseRef) { "Missing '$RESPONSE_HEADER'" }
35+
responseRef.set(
36+
if (returnValue == null) emptyFlow() else getContent(returnValue, returnType).asFlow()
37+
)
38+
return Mono.empty()
4239
}
4340

44-
private fun getContent(@Nullable content: Any?, returnType: MethodParameter): Publisher<Any> {
41+
private fun getContent(content: Any, returnType: MethodParameter): Publisher<Any> {
4542
val returnValueType = ResolvableType.forMethodParameter(returnType)
4643
val adapter = reactiveAdapterRegistry.getAdapter(returnValueType.resolve(), content)
47-
return adapter?.run { toPublisher(content) } ?: Mono.justOrEmpty(content)
48-
}
49-
50-
private fun handleContent(encodedContent: Flow<Any>, message: Message<*>): Mono<Void?> {
51-
val responseRef: AtomicReference<Flow<Any>>? = getResponseReference(message)
52-
checkNotNull(responseRef) { "Missing '$RESPONSE_HEADER'" }
53-
responseRef.set(encodedContent)
54-
return Mono.empty()
44+
return adapter?.run { toPublisher(content) } ?: Mono.just(content)
5545
}
5646

5747
@Suppress("UNCHECKED_CAST")
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.caplin.integration.datasourcex.spring.internal
2+
3+
import com.caplin.datasource.DataSource
4+
import com.caplin.datasource.messaging.json.JsonHandler
5+
import com.caplin.integration.datasourcex.util.serialization.jackson3.Jackson3JsonHandler
6+
import io.kotest.core.spec.style.FunSpec
7+
import io.kotest.extensions.spring.SpringExtension
8+
import io.kotest.matchers.types.shouldBeInstanceOf
9+
import io.mockk.mockk
10+
import org.springframework.beans.factory.annotation.Autowired
11+
import org.springframework.boot.autoconfigure.ImportAutoConfiguration
12+
import org.springframework.boot.test.context.SpringBootTest
13+
import org.springframework.context.annotation.Bean
14+
import org.springframework.context.annotation.Configuration
15+
import org.springframework.test.context.TestPropertySource
16+
import tools.jackson.databind.ObjectMapper as Jackson3ObjectMapper
17+
import tools.jackson.databind.json.JsonMapper
18+
19+
/**
20+
* Verifies the autoconfiguration selects the Jackson 3 [JsonHandler] when a Jackson 3
21+
* [ObjectMapper] is present — the Spring Boot 4 default. [ImportAutoConfiguration] loads the
22+
* autoconfiguration with proper ordering so its `@ConditionalOnMissingBean` conditions see the
23+
* test's beans first, letting us mock the DataSource rather than create a real one.
24+
*/
25+
@SpringBootTest(classes = [DataSourceAutoConfigurationTest.TestConfig::class])
26+
@ImportAutoConfiguration(DataSourceAutoConfiguration::class)
27+
@TestPropertySource(properties = ["caplin.datasource.managed.discovery.hostname=localhost"])
28+
class DataSourceAutoConfigurationTest : FunSpec() {
29+
30+
@Autowired private lateinit var jsonHandler: JsonHandler<*>
31+
32+
init {
33+
extension(SpringExtension())
34+
35+
test("wires the Jackson 3 JsonHandler by default") {
36+
jsonHandler.shouldBeInstanceOf<Jackson3JsonHandler>()
37+
}
38+
}
39+
40+
@Configuration(proxyBeanMethods = false)
41+
class TestConfig {
42+
@Bean fun jackson3ObjectMapper(): Jackson3ObjectMapper = JsonMapper.builder().build()
43+
44+
@Bean fun dataSource(): DataSource = mockk(relaxed = true)
45+
}
46+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<configuration>
2+
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
3+
<encoder>
4+
<pattern>%d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n</pattern>
5+
</encoder>
6+
</appender>
7+
<root level="WARN">
8+
<appender-ref ref="STDOUT" />
9+
</root>
10+
</configuration>

0 commit comments

Comments
 (0)