From dee32e0914701258b11ea9324d5db1e1f7c1193b Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 13 Oct 2025 11:57:38 +0100 Subject: [PATCH] Consider Jackson 2 in RSocket auto-configuration See gh-47688 --- module/spring-boot-rsocket/build.gradle | 2 + .../RSocketStrategiesAutoConfiguration.java | 139 ++++++++++++++---- ...itional-spring-configuration-metadata.json | 26 +++- ...ocketStrategiesAutoConfigurationTests.java | 39 +++++ 4 files changed, 177 insertions(+), 29 deletions(-) diff --git a/module/spring-boot-rsocket/build.gradle b/module/spring-boot-rsocket/build.gradle index c446b1e8b02..c2bf28560ee 100644 --- a/module/spring-boot-rsocket/build.gradle +++ b/module/spring-boot-rsocket/build.gradle @@ -33,10 +33,12 @@ dependencies { optional(project(":core:spring-boot-autoconfigure")) optional(project(":module:spring-boot-jackson")) + optional(project(":module:spring-boot-jackson2")) optional(project(":module:spring-boot-reactor-netty")) optional("io.rsocket:rsocket-transport-netty") optional("org.springframework:spring-web") optional("tools.jackson.dataformat:jackson-dataformat-cbor") + optional("com.fasterxml.jackson.dataformat:jackson-dataformat-cbor") testImplementation(project(":core:spring-boot-test")) testImplementation(project(":test-support:spring-boot-test-support")) diff --git a/module/spring-boot-rsocket/src/main/java/org/springframework/boot/rsocket/autoconfigure/RSocketStrategiesAutoConfiguration.java b/module/spring-boot-rsocket/src/main/java/org/springframework/boot/rsocket/autoconfigure/RSocketStrategiesAutoConfiguration.java index b672ff90705..a04bf899829 100644 --- a/module/spring-boot-rsocket/src/main/java/org/springframework/boot/rsocket/autoconfigure/RSocketStrategiesAutoConfiguration.java +++ b/module/spring-boot-rsocket/src/main/java/org/springframework/boot/rsocket/autoconfigure/RSocketStrategiesAutoConfiguration.java @@ -16,6 +16,8 @@ package org.springframework.boot.rsocket.autoconfigure; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.cbor.CBORFactory; import io.netty.buffer.PooledByteBufAllocator; import tools.jackson.databind.json.JsonMapper; import tools.jackson.dataformat.cbor.CBORMapper; @@ -23,11 +25,15 @@ import tools.jackson.dataformat.cbor.CBORMapper; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.AnyNestedCondition; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.rsocket.messaging.RSocketStrategiesCustomizer; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; import org.springframework.http.MediaType; @@ -45,7 +51,8 @@ import org.springframework.web.util.pattern.PathPatternRouteMatcher; * @author Brian Clozel * @since 4.0.0 */ -@AutoConfiguration(afterName = "org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration") +@AutoConfiguration(afterName = { "org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration", + "org.springframework.boot.jackson2.autoconfigure.Jackson2AutoConfiguration" }) @ConditionalOnClass({ io.rsocket.RSocket.class, RSocketStrategies.class, PooledByteBufAllocator.class }) public final class RSocketStrategiesAutoConfiguration { @@ -63,38 +70,114 @@ public final class RSocketStrategiesAutoConfiguration { } @Configuration(proxyBeanMethods = false) - @ConditionalOnClass(CBORMapper.class) - @ConditionalOnBean(CBORMapper.class) - protected static class JacksonCborStrategyConfiguration { - - private static final MediaType[] SUPPORTED_TYPES = { MediaType.APPLICATION_CBOR }; - - @Bean - @Order(0) - RSocketStrategiesCustomizer jacksonCborRSocketStrategyCustomizer(CBORMapper cborMapper) { - return (strategy) -> { - strategy.decoder(new JacksonCborDecoder(cborMapper, SUPPORTED_TYPES)); - strategy.encoder(new JacksonCborEncoder(cborMapper, SUPPORTED_TYPES)); - }; + @ConditionalOnProperty(name = "spring.rsocket.preferred-mapper", havingValue = "jackson", matchIfMissing = true) + static class JacksonStrategyConfiguration { + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(CBORMapper.class) + @ConditionalOnBean(CBORMapper.class) + static class JacksonCborStrategyConfiguration { + + private static final MediaType[] SUPPORTED_TYPES = { MediaType.APPLICATION_CBOR }; + + @Bean + @Order(0) + RSocketStrategiesCustomizer jacksonCborRSocketStrategyCustomizer(CBORMapper cborMapper) { + return (strategy) -> { + strategy.decoder(new JacksonCborDecoder(cborMapper, SUPPORTED_TYPES)); + strategy.encoder(new JacksonCborEncoder(cborMapper, SUPPORTED_TYPES)); + }; + } + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(JsonMapper.class) + static class JacksonJsonStrategyConfiguration { + + private static final MediaType[] SUPPORTED_TYPES = { MediaType.APPLICATION_JSON, + new MediaType("application", "*+json") }; + + @Bean + @Order(1) + @ConditionalOnBean(JsonMapper.class) + RSocketStrategiesCustomizer jacksonJsonRSocketStrategyCustomizer(JsonMapper jsonMapper) { + return (strategy) -> { + strategy.decoder(new JacksonJsonDecoder(jsonMapper, SUPPORTED_TYPES)); + strategy.encoder(new JacksonJsonEncoder(jsonMapper, SUPPORTED_TYPES)); + }; + } + } } @Configuration(proxyBeanMethods = false) - @ConditionalOnClass(JsonMapper.class) - @ConditionalOnBean(JsonMapper.class) - protected static class JacksonJsonStrategyConfiguration { - - private static final MediaType[] SUPPORTED_TYPES = { MediaType.APPLICATION_JSON, - new MediaType("application", "*+json") }; - - @Bean - @Order(1) - RSocketStrategiesCustomizer jacksonJsonRSocketStrategyCustomizer(JsonMapper jsonMapper) { - return (strategy) -> { - strategy.decoder(new JacksonJsonDecoder(jsonMapper, SUPPORTED_TYPES)); - strategy.encoder(new JacksonJsonEncoder(jsonMapper, SUPPORTED_TYPES)); - }; + @Conditional(NoJacksonOrJackson2Preferred.class) + @SuppressWarnings("removal") + @Deprecated(since = "4.0.0", forRemoval = true) + static class Jackson2StrategyConfiguration { + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass({ ObjectMapper.class, CBORFactory.class }) + static class Jackson2CborStrategyConfiguration { + + private static final MediaType[] SUPPORTED_TYPES = { MediaType.APPLICATION_CBOR }; + + @Bean + @Order(0) + @ConditionalOnBean(org.springframework.http.converter.json.Jackson2ObjectMapperBuilder.class) + RSocketStrategiesCustomizer jackson2CborRSocketStrategyCustomizer( + org.springframework.http.converter.json.Jackson2ObjectMapperBuilder builder) { + return (strategy) -> { + ObjectMapper objectMapper = builder.createXmlMapper(false) + .factory(new com.fasterxml.jackson.dataformat.cbor.CBORFactory()) + .build(); + strategy.decoder( + new org.springframework.http.codec.cbor.Jackson2CborDecoder(objectMapper, SUPPORTED_TYPES)); + strategy.encoder( + new org.springframework.http.codec.cbor.Jackson2CborEncoder(objectMapper, SUPPORTED_TYPES)); + }; + } + + } + + @ConditionalOnClass(ObjectMapper.class) + static class Jackson2JsonStrategyConfiguration { + + private static final MediaType[] SUPPORTED_TYPES = { MediaType.APPLICATION_JSON, + new MediaType("application", "*+json") }; + + @Bean + @Order(1) + @ConditionalOnBean(ObjectMapper.class) + RSocketStrategiesCustomizer jackson2JsonRSocketStrategyCustomizer(ObjectMapper objectMapper) { + return (strategy) -> { + strategy.decoder( + new org.springframework.http.codec.json.Jackson2JsonDecoder(objectMapper, SUPPORTED_TYPES)); + strategy.encoder( + new org.springframework.http.codec.json.Jackson2JsonEncoder(objectMapper, SUPPORTED_TYPES)); + }; + } + + } + + } + + static class NoJacksonOrJackson2Preferred extends AnyNestedCondition { + + NoJacksonOrJackson2Preferred() { + super(ConfigurationPhase.PARSE_CONFIGURATION); + } + + @ConditionalOnMissingClass("tools.jackson.databind.json.JsonMapper") + static class NoJackson { + + } + + @ConditionalOnProperty(name = "spring.rsocket.preferred-mapper", havingValue = "jackson2") + static class Jackson2Preferred { + } } diff --git a/module/spring-boot-rsocket/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/module/spring-boot-rsocket/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 64913148487..d43649e554c 100644 --- a/module/spring-boot-rsocket/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/module/spring-boot-rsocket/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -1,4 +1,28 @@ { "properties": [ + { + "name": "spring.rsocket.preferred-mapper", + "type": "java.lang.String", + "defaultValue": "jackson", + "description": "Preferred JSON and CBOR mapper to use. By default, auto-detected according to the environment. Supported values are 'jackson' and 'jackson2' (deprecated)." + } + ], + "hints": [ + { + "name": "spring.rsocket.preferred-mapper", + "values": [ + { + "value": "jackson" + }, + { + "value": "jackson2" + } + ], + "providers": [ + { + "name": "any" + } + ] + } ] -} \ No newline at end of file +} diff --git a/module/spring-boot-rsocket/src/test/java/org/springframework/boot/rsocket/autoconfigure/RSocketStrategiesAutoConfigurationTests.java b/module/spring-boot-rsocket/src/test/java/org/springframework/boot/rsocket/autoconfigure/RSocketStrategiesAutoConfigurationTests.java index f45793953e1..4c8b6231846 100644 --- a/module/spring-boot-rsocket/src/test/java/org/springframework/boot/rsocket/autoconfigure/RSocketStrategiesAutoConfigurationTests.java +++ b/module/spring-boot-rsocket/src/test/java/org/springframework/boot/rsocket/autoconfigure/RSocketStrategiesAutoConfigurationTests.java @@ -22,6 +22,7 @@ import tools.jackson.dataformat.cbor.CBORMapper; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.rsocket.messaging.RSocketStrategiesCustomizer; +import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -82,6 +83,44 @@ class RSocketStrategiesAutoConfigurationTests { }); } + @Test + @Deprecated(since = "4.0.0", forRemoval = true) + @SuppressWarnings("removal") + void shouldUseJackson2WhenPreferred() { + this.contextRunner + .withConfiguration(AutoConfigurations + .of(org.springframework.boot.jackson2.autoconfigure.Jackson2AutoConfiguration.class)) + .withPropertyValues("spring.rsocket.preferred-mapper=jackson2") + .run((context) -> { + RSocketStrategies strategies = context.getBean(RSocketStrategies.class); + assertThat(strategies.decoders()) + .hasAtLeastOneElementOfType(org.springframework.http.codec.cbor.Jackson2CborDecoder.class) + .hasAtLeastOneElementOfType(org.springframework.http.codec.json.Jackson2JsonDecoder.class); + assertThat(strategies.encoders()) + .hasAtLeastOneElementOfType(org.springframework.http.codec.cbor.Jackson2CborEncoder.class) + .hasAtLeastOneElementOfType(org.springframework.http.codec.json.Jackson2JsonEncoder.class); + }); + } + + @Test + @Deprecated(since = "4.0.0", forRemoval = true) + @SuppressWarnings("removal") + void shouldUseJackson2WhenJacksonIsAbsent() { + this.contextRunner + .withConfiguration(AutoConfigurations + .of(org.springframework.boot.jackson2.autoconfigure.Jackson2AutoConfiguration.class)) + .withClassLoader(new FilteredClassLoader(JsonMapper.class, CBORMapper.class)) + .run((context) -> { + RSocketStrategies strategies = context.getBean(RSocketStrategies.class); + assertThat(strategies.decoders()) + .hasAtLeastOneElementOfType(org.springframework.http.codec.cbor.Jackson2CborDecoder.class) + .hasAtLeastOneElementOfType(org.springframework.http.codec.json.Jackson2JsonDecoder.class); + assertThat(strategies.encoders()) + .hasAtLeastOneElementOfType(org.springframework.http.codec.cbor.Jackson2CborEncoder.class) + .hasAtLeastOneElementOfType(org.springframework.http.codec.json.Jackson2JsonEncoder.class); + }); + } + @Configuration(proxyBeanMethods = false) static class UserStrategies {