diff --git a/module/spring-boot-graphql/build.gradle b/module/spring-boot-graphql/build.gradle index 2034744222b..c48e36d96d9 100644 --- a/module/spring-boot-graphql/build.gradle +++ b/module/spring-boot-graphql/build.gradle @@ -28,10 +28,11 @@ dependencies { api(project(":core:spring-boot")) api("org.springframework.graphql:spring-graphql") - implementation(project(":module:spring-boot-jackson")) optional(project(":core:spring-boot-autoconfigure")) optional(project(":module:spring-boot-http-converter")) + optional(project(":module:spring-boot-jackson")) + optional(project(":module:spring-boot-jackson2")) optional(project(":module:spring-boot-micrometer-observation")) optional(project(":module:spring-boot-rsocket")) optional(project(":module:spring-boot-security")) diff --git a/module/spring-boot-graphql/src/main/java/org/springframework/boot/graphql/autoconfigure/rsocket/GraphQlRSocketAutoConfiguration.java b/module/spring-boot-graphql/src/main/java/org/springframework/boot/graphql/autoconfigure/rsocket/GraphQlRSocketAutoConfiguration.java index 2e10fee9938..ce242924ab1 100644 --- a/module/spring-boot-graphql/src/main/java/org/springframework/boot/graphql/autoconfigure/rsocket/GraphQlRSocketAutoConfiguration.java +++ b/module/spring-boot-graphql/src/main/java/org/springframework/boot/graphql/autoconfigure/rsocket/GraphQlRSocketAutoConfiguration.java @@ -16,6 +16,7 @@ package org.springframework.boot.graphql.autoconfigure.rsocket; +import com.fasterxml.jackson.databind.ObjectMapper; import graphql.GraphQL; import io.rsocket.core.RSocketServer; import reactor.netty.http.server.HttpServer; @@ -24,12 +25,17 @@ import tools.jackson.databind.json.JsonMapper; 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.graphql.autoconfigure.GraphQlAutoConfiguration; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.codec.Encoder; import org.springframework.graphql.ExecutionGraphQlService; import org.springframework.graphql.data.method.annotation.support.AnnotatedControllerConfigurer; import org.springframework.graphql.execution.GraphQlSource; @@ -46,7 +52,9 @@ import org.springframework.messaging.rsocket.annotation.support.RSocketMessageHa * @since 4.0.0 */ @AutoConfiguration(after = GraphQlAutoConfiguration.class, - afterName = "org.springframework.boot.rsocket.autoconfigure.RSocketMessagingAutoConfiguration") + afterName = { "org.springframework.boot.rsocket.autoconfigure.RSocketMessagingAutoConfiguration", + "org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration", + "org.springframework.boot.jackson2.autoconfigure.Jackson2AutoConfiguration" }) @ConditionalOnClass({ GraphQL.class, GraphQlSource.class, RSocketServer.class, HttpServer.class }) @ConditionalOnBean({ RSocketMessageHandler.class, AnnotatedControllerConfigurer.class }) @ConditionalOnProperty("spring.graphql.rsocket.mapping") @@ -55,9 +63,9 @@ public final class GraphQlRSocketAutoConfiguration { @Bean @ConditionalOnMissingBean GraphQlRSocketHandler graphQlRSocketHandler(ExecutionGraphQlService graphQlService, - ObjectProvider interceptors, JsonMapper jsonMapper) { + ObjectProvider interceptors, JsonEncoderSupplier jsonEncoderSupplier) { return new GraphQlRSocketHandler(graphQlService, interceptors.orderedStream().toList(), - new JacksonJsonEncoder(jsonMapper)); + jsonEncoderSupplier.jsonEncoder()); } @Bean @@ -66,4 +74,55 @@ public final class GraphQlRSocketAutoConfiguration { return new GraphQlRSocketController(handler); } + interface JsonEncoderSupplier { + + Encoder jsonEncoder(); + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnBean(JsonMapper.class) + @ConditionalOnProperty(name = "spring.graphql.rsocket.preferred-json-mapper", havingValue = "jackson", + matchIfMissing = true) + static class JacksonJsonEncoderSupplierConfiguration { + + @Bean + JsonEncoderSupplier jacksonJsonEncoderSupplier(JsonMapper jsonMapper) { + return () -> new JacksonJsonEncoder(jsonMapper); + } + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnBean(ObjectMapper.class) + @Conditional(NoJacksonOrJackson2Preferred.class) + @Deprecated(since = "4.0.0", forRemoval = true) + @SuppressWarnings("removal") + static class Jackson2JsonEncoderSupplierConfiguration { + + @Bean + JsonEncoderSupplier jackson2JsonEncoderSupplier(ObjectMapper objectMapper) { + return () -> new org.springframework.http.codec.json.Jackson2JsonEncoder(objectMapper); + } + + } + + static class NoJacksonOrJackson2Preferred extends AnyNestedCondition { + + NoJacksonOrJackson2Preferred() { + super(ConfigurationPhase.PARSE_CONFIGURATION); + } + + @ConditionalOnMissingClass("tools.jackson.databind.json.JsonMapper") + static class NoJackson { + + } + + @ConditionalOnProperty(name = "spring.graphql.rsocket.preferred-json-mapper", havingValue = "jackson2") + static class Jackson2Preferred { + + } + + } + } diff --git a/module/spring-boot-graphql/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/module/spring-boot-graphql/src/main/resources/META-INF/additional-spring-configuration-metadata.json index ac88283fccf..78f7cf91540 100644 --- a/module/spring-boot-graphql/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/module/spring-boot-graphql/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -37,6 +37,12 @@ "since": "3.5.0" } }, + { + "name": "spring.graphql.rsocket.preferred-json-mapper", + "type": "java.lang.String", + "defaultValue": "jackson", + "description": "Preferred JSON mapper to use. By default, auto-detected according to the environment. Supported values are 'jackson' and 'jackson2' (deprecated)." + }, { "name": "spring.graphql.schema.file-extensions", "defaultValue": ".graphqls,.gqls" @@ -95,6 +101,22 @@ "name": "any" } ] + }, + { + "name": "spring.graphql.rsocket.preferred-json-mapper", + "values": [ + { + "value": "jackson" + }, + { + "value": "jackson2" + } + ], + "providers": [ + { + "name": "any" + } + ] } ] } diff --git a/module/spring-boot-graphql/src/test/java/org/springframework/boot/graphql/autoconfigure/rsocket/GraphQlRSocketAutoConfigurationTests.java b/module/spring-boot-graphql/src/test/java/org/springframework/boot/graphql/autoconfigure/rsocket/GraphQlRSocketAutoConfigurationTests.java index 9c1d18740b1..a2fafebc802 100644 --- a/module/spring-boot-graphql/src/test/java/org/springframework/boot/graphql/autoconfigure/rsocket/GraphQlRSocketAutoConfigurationTests.java +++ b/module/spring-boot-graphql/src/test/java/org/springframework/boot/graphql/autoconfigure/rsocket/GraphQlRSocketAutoConfigurationTests.java @@ -22,18 +22,22 @@ import java.util.function.Consumer; import graphql.schema.idl.TypeRuntimeWiring; import org.junit.jupiter.api.Test; +import tools.jackson.databind.json.JsonMapper; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener; import org.springframework.boot.graphql.autoconfigure.GraphQlAutoConfiguration; import org.springframework.boot.graphql.autoconfigure.GraphQlTestDataFetchers; import org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration; +import org.springframework.boot.logging.LogLevel; import org.springframework.boot.reactor.netty.NettyReactiveWebServerFactory; import org.springframework.boot.reactor.netty.NettyRouteProvider; import org.springframework.boot.rsocket.autoconfigure.RSocketMessagingAutoConfiguration; import org.springframework.boot.rsocket.autoconfigure.RSocketServerAutoConfiguration; import org.springframework.boot.rsocket.autoconfigure.RSocketStrategiesAutoConfiguration; import org.springframework.boot.rsocket.context.RSocketPortInfoApplicationContextInitializer; +import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; import org.springframework.boot.testsupport.classpath.resources.WithResource; import org.springframework.boot.web.server.context.ServerPortInfoApplicationContextInitializer; @@ -99,6 +103,33 @@ class GraphQlRSocketAutoConfigurationTests { testWithRSocketWebSocket(this::assertThatSimpleQueryWorks); } + @Test + void usesJacksonByDefault() { + this.contextRunner.run((context) -> assertThat(context).hasBean("jacksonJsonEncoderSupplier")); + } + + @Test + @Deprecated(since = "4.0.0", forRemoval = true) + @SuppressWarnings("removal") + void usesJackson2WhenItIsPreferred() { + this.contextRunner.withPropertyValues("spring.graphql.rsocket.preferred-json-mapper=jackson2") + .withConfiguration(AutoConfigurations + .of(org.springframework.boot.jackson2.autoconfigure.Jackson2AutoConfiguration.class)) + .run((context) -> assertThat(context).hasBean("jackson2JsonEncoderSupplier")); + } + + @Test + @Deprecated(since = "4.0.0", forRemoval = true) + @SuppressWarnings("removal") + void usesJackson2WhenJacksonIsAbsent() { + this.contextRunner + .withClassLoader(new FilteredClassLoader(Thread.currentThread().getContextClassLoader(), JsonMapper.class)) + .withConfiguration(AutoConfigurations + .of(org.springframework.boot.jackson2.autoconfigure.Jackson2AutoConfiguration.class)) + .withInitializer(ConditionEvaluationReportLoggingListener.forLogLevel(LogLevel.INFO)) + .run((context) -> assertThat(context).hasBean("jackson2JsonEncoderSupplier")); + } + private void assertThatSimpleQueryWorks(RSocketGraphQlClient client) { String document = "{ bookById(id: \"book-1\"){ id name pageCount author } }"; String bookName = client.document(document)