diff --git a/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/JacksonAutoConfiguration.java b/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/JacksonAutoConfiguration.java index 7a0c9041765..1643c29a09f 100644 --- a/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/JacksonAutoConfiguration.java +++ b/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/JacksonAutoConfiguration.java @@ -43,6 +43,7 @@ import tools.jackson.databind.PropertyNamingStrategies; import tools.jackson.databind.PropertyNamingStrategy; import tools.jackson.databind.cfg.ConstructorDetector; import tools.jackson.databind.cfg.DateTimeFeature; +import tools.jackson.databind.cfg.HandlerInstantiator; import tools.jackson.databind.cfg.MapperBuilder; import tools.jackson.databind.json.JsonMapper; import tools.jackson.dataformat.cbor.CBORFactory; @@ -57,6 +58,7 @@ import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigurationPackages; @@ -174,8 +176,10 @@ public final class JacksonAutoConfiguration { } @Bean - StandardJsonMapperBuilderCustomizer standardJsonMapperBuilderCustomizer(ObjectProvider modules) { - return new StandardJsonMapperBuilderCustomizer(this.jacksonProperties, modules.stream().toList()); + StandardJsonMapperBuilderCustomizer standardJsonMapperBuilderCustomizer(ObjectProvider modules, + AutowireCapableBeanFactory beanFactory) { + return new StandardJsonMapperBuilderCustomizer(this.jacksonProperties, modules.stream().toList(), + beanFactory); } static final class StandardJsonFactoryBuilderCustomizer @@ -195,9 +199,9 @@ public final class JacksonAutoConfiguration { static final class StandardJsonMapperBuilderCustomizer extends AbstractMapperBuilderCustomizer implements JsonMapperBuilderCustomizer { - StandardJsonMapperBuilderCustomizer(JacksonProperties jacksonProperties, - Collection modules) { - super(jacksonProperties, modules); + StandardJsonMapperBuilderCustomizer(JacksonProperties jacksonProperties, Collection modules, + AutowireCapableBeanFactory beanFactory) { + super(jacksonProperties, modules, beanFactory); } @Override @@ -280,9 +284,9 @@ public final class JacksonAutoConfiguration { @Bean StandardCborMapperBuilderCustomizer standardCborMapperBuilderCustomizer(ObjectProvider modules, - JacksonCborProperties cborProperties) { + JacksonCborProperties cborProperties, AutowireCapableBeanFactory beanFactory) { return new StandardCborMapperBuilderCustomizer(this.jacksonProperties, modules.stream().toList(), - cborProperties); + cborProperties, beanFactory); } static final class StandardCborFactoryBuilderCustomizer @@ -305,8 +309,8 @@ public final class JacksonAutoConfiguration { private final JacksonCborProperties cborProperties; StandardCborMapperBuilderCustomizer(JacksonProperties jacksonProperties, Collection modules, - JacksonCborProperties cborProperties) { - super(jacksonProperties, modules); + JacksonCborProperties cborProperties, AutowireCapableBeanFactory beanFactory) { + super(jacksonProperties, modules, beanFactory); this.cborProperties = cborProperties; } @@ -370,9 +374,9 @@ public final class JacksonAutoConfiguration { @Bean StandardXmlMapperBuilderCustomizer standardXmlMapperBuilderCustomizer(ObjectProvider modules, - JacksonXmlProperties xmlProperties) { + JacksonXmlProperties xmlProperties, AutowireCapableBeanFactory beanFactory) { return new StandardXmlMapperBuilderCustomizer(this.jacksonProperties, modules.stream().toList(), - xmlProperties); + xmlProperties, beanFactory); } @Configuration(proxyBeanMethods = false) @@ -415,8 +419,8 @@ public final class JacksonAutoConfiguration { private final JacksonXmlProperties xmlProperties; StandardXmlMapperBuilderCustomizer(JacksonProperties jacksonProperties, Collection modules, - JacksonXmlProperties xmlProperties) { - super(jacksonProperties, modules); + JacksonXmlProperties xmlProperties, AutowireCapableBeanFactory beanFactory) { + super(jacksonProperties, modules, beanFactory); this.xmlProperties = xmlProperties; } @@ -508,9 +512,13 @@ public final class JacksonAutoConfiguration { private final Collection modules; - AbstractMapperBuilderCustomizer(JacksonProperties jacksonProperties, Collection modules) { + private final HandlerInstantiator handlerInstantiator; + + AbstractMapperBuilderCustomizer(JacksonProperties jacksonProperties, Collection modules, + AutowireCapableBeanFactory beanFactory) { this.jacksonProperties = jacksonProperties; this.modules = modules; + this.handlerInstantiator = new SpringBeanHandlerInstantiator(beanFactory); } @Override @@ -538,6 +546,7 @@ public final class JacksonAutoConfiguration { if (this.jacksonProperties.getTimeZone() != null) { builder.defaultTimeZone(this.jacksonProperties.getTimeZone()); } + builder.handlerInstantiator(this.handlerInstantiator); configureVisibility(builder, this.jacksonProperties.getVisibility()); configureFeatures(builder, this.jacksonProperties.getDeserialization(), builder::configure); configureFeatures(builder, this.jacksonProperties.getSerialization(), builder::configure); diff --git a/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/SpringBeanHandlerInstantiator.java b/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/SpringBeanHandlerInstantiator.java new file mode 100644 index 00000000000..ec413399d55 --- /dev/null +++ b/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/SpringBeanHandlerInstantiator.java @@ -0,0 +1,120 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.jackson.autoconfigure; + +import com.fasterxml.jackson.annotation.ObjectIdGenerator; +import com.fasterxml.jackson.annotation.ObjectIdResolver; +import org.jspecify.annotations.Nullable; +import tools.jackson.databind.DeserializationConfig; +import tools.jackson.databind.KeyDeserializer; +import tools.jackson.databind.PropertyNamingStrategy; +import tools.jackson.databind.SerializationConfig; +import tools.jackson.databind.ValueDeserializer; +import tools.jackson.databind.ValueSerializer; +import tools.jackson.databind.cfg.HandlerInstantiator; +import tools.jackson.databind.cfg.MapperConfig; +import tools.jackson.databind.deser.ValueInstantiator; +import tools.jackson.databind.introspect.Annotated; +import tools.jackson.databind.jsontype.TypeIdResolver; +import tools.jackson.databind.jsontype.TypeResolverBuilder; +import tools.jackson.databind.ser.VirtualBeanPropertyWriter; +import tools.jackson.databind.util.Converter; + +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.util.Assert; + +/** + * Package-private adaptation of Spring Framework's + * {@link org.springframework.http.support.JacksonHandlerInstantiator}. Allows Spring Boot + * to offer bean-based handler instantiation without requiring {@code spring-web}. + * + * @author Sebastien Deleuze + * @author Andy Wilkinson + * @see org.springframework.http.support.JacksonHandlerInstantiator + */ +class SpringBeanHandlerInstantiator extends HandlerInstantiator { + + private final AutowireCapableBeanFactory beanFactory; + + SpringBeanHandlerInstantiator(AutowireCapableBeanFactory beanFactory) { + Assert.notNull(beanFactory, "BeanFactory must not be null"); + this.beanFactory = beanFactory; + } + + @Override + @Nullable public ValueDeserializer deserializerInstance(DeserializationConfig config, Annotated annotated, + Class deserClass) { + return (ValueDeserializer) this.beanFactory.createBean(deserClass); + } + + @Override + public KeyDeserializer keyDeserializerInstance(DeserializationConfig config, Annotated annotated, + Class keyDeserClass) { + return (KeyDeserializer) this.beanFactory.createBean(keyDeserClass); + } + + @Override + public ValueSerializer serializerInstance(SerializationConfig config, Annotated annotated, Class serClass) { + return (ValueSerializer) this.beanFactory.createBean(serClass); + } + + @Override + public TypeResolverBuilder typeResolverBuilderInstance(MapperConfig config, Annotated annotated, + Class builderClass) { + return (TypeResolverBuilder) this.beanFactory.createBean(builderClass); + } + + @Override + public TypeIdResolver typeIdResolverInstance(MapperConfig config, Annotated annotated, Class resolverClass) { + return (TypeIdResolver) this.beanFactory.createBean(resolverClass); + } + + @Override + public ValueInstantiator valueInstantiatorInstance(MapperConfig config, Annotated annotated, + Class implClass) { + return (ValueInstantiator) this.beanFactory.createBean(implClass); + } + + @Override + public ObjectIdGenerator objectIdGeneratorInstance(MapperConfig config, Annotated annotated, + Class implClass) { + return (ObjectIdGenerator) this.beanFactory.createBean(implClass); + } + + @Override + public ObjectIdResolver resolverIdGeneratorInstance(MapperConfig config, Annotated annotated, + Class implClass) { + return (ObjectIdResolver) this.beanFactory.createBean(implClass); + } + + @Override + public PropertyNamingStrategy namingStrategyInstance(MapperConfig config, Annotated annotated, + Class implClass) { + return (PropertyNamingStrategy) this.beanFactory.createBean(implClass); + } + + @Override + public Converter converterInstance(MapperConfig config, Annotated annotated, Class implClass) { + return (Converter) this.beanFactory.createBean(implClass); + } + + @Override + public VirtualBeanPropertyWriter virtualPropertyWriterInstance(MapperConfig config, Class implClass) { + return (VirtualBeanPropertyWriter) this.beanFactory.createBean(implClass); + } + +} diff --git a/module/spring-boot-jackson/src/test/java/org/springframework/boot/jackson/autoconfigure/JacksonAutoConfigurationTests.java b/module/spring-boot-jackson/src/test/java/org/springframework/boot/jackson/autoconfigure/JacksonAutoConfigurationTests.java index 96ecbab9748..49da0a45064 100644 --- a/module/spring-boot-jackson/src/test/java/org/springframework/boot/jackson/autoconfigure/JacksonAutoConfigurationTests.java +++ b/module/spring-boot-jackson/src/test/java/org/springframework/boot/jackson/autoconfigure/JacksonAutoConfigurationTests.java @@ -913,6 +913,17 @@ class JacksonAutoConfigurationTests { }); } + @EnumSource + @ParameterizedTest + void mapperHasASpringBeanHandlerInstantiator(MapperType mapperType) { + this.contextRunner.run((context) -> { + assertThat(mapperType.getMapper(context).deserializationConfig().getHandlerInstantiator()) + .isInstanceOf(SpringBeanHandlerInstantiator.class); + assertThat(mapperType.getMapper(context).serializationConfig().getHandlerInstantiator()) + .isInstanceOf(SpringBeanHandlerInstantiator.class); + }); + } + static class MyDateFormat extends SimpleDateFormat { MyDateFormat() { diff --git a/module/spring-boot-jackson/src/test/java/org/springframework/boot/jackson/autoconfigure/SpringBeanHandlerInstantiatorTests.java b/module/spring-boot-jackson/src/test/java/org/springframework/boot/jackson/autoconfigure/SpringBeanHandlerInstantiatorTests.java new file mode 100644 index 00000000000..131a1b49a15 --- /dev/null +++ b/module/spring-boot-jackson/src/test/java/org/springframework/boot/jackson/autoconfigure/SpringBeanHandlerInstantiatorTests.java @@ -0,0 +1,273 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.jackson.autoconfigure; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.function.Consumer; + +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.Test; +import tools.jackson.core.JsonGenerator; +import tools.jackson.core.JsonParser; +import tools.jackson.databind.DatabindContext; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.JavaType; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.KeyDeserializer; +import tools.jackson.databind.SerializationContext; +import tools.jackson.databind.ValueDeserializer; +import tools.jackson.databind.ValueSerializer; +import tools.jackson.databind.annotation.JsonDeserialize; +import tools.jackson.databind.annotation.JsonSerialize; +import tools.jackson.databind.annotation.JsonTypeIdResolver; +import tools.jackson.databind.annotation.JsonTypeResolver; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.databind.jsontype.NamedType; +import tools.jackson.databind.jsontype.TypeDeserializer; +import tools.jackson.databind.jsontype.TypeIdResolver; +import tools.jackson.databind.jsontype.TypeSerializer; +import tools.jackson.databind.jsontype.impl.StdTypeResolverBuilder; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link SpringBeanHandlerInstantiator}. Adapted from Spring Framework's + * JacksonHandlerInstantiatorTests. + * + * @author Sebastien Deleuze + * @author Andy Wilkinson + */ +class SpringBeanHandlerInstantiatorTests { + + @Test + void autowiredSerializer() { + withHandlerInstantiator((jsonMapper) -> { + User user = new User("bob"); + String json = jsonMapper.writeValueAsString(user); + assertThat(json).isEqualTo("{\"username\":\"BOB\"}"); + }); + } + + @Test + void autowiredDeserializer() { + withHandlerInstantiator((jsonMapper) -> { + String json = "{\"username\":\"bob\"}"; + User user = jsonMapper.readValue(json, User.class); + assertThat(user.getUsername()).isEqualTo("BOB"); + }); + } + + @Test + void autowiredKeyDeserializer() { + withHandlerInstantiator((jsonMapper) -> { + String json = "{\"credentials\":{\"bob\":\"admin\"}}"; + SecurityRegistry registry = jsonMapper.readValue(json, SecurityRegistry.class); + assertThat(registry.getCredentials()).containsKey("BOB"); + assertThat(registry.getCredentials()).doesNotContainKey("bob"); + }); + } + + @Test + void applicationContextAwaretypeResolverBuilder() { + withHandlerInstantiator((jsonMapper) -> { + jsonMapper.writeValueAsString(new Group()); + assertThat(CustomTypeResolverBuilder.autowiredFieldInitialized).isTrue(); + }); + } + + @Test + void applicationContextAwareTypeIdResolver() { + withHandlerInstantiator((jsonMapper) -> { + jsonMapper.writeValueAsString(new Group()); + assertThat(CustomTypeIdResolver.autowiredFieldInitialized).isTrue(); + }); + } + + private void withHandlerInstantiator(Consumer consumer) { + new ApplicationContextRunner().withBean(Capitalizer.class) + .run((context) -> consumer.accept(JsonMapper.builder() + .handlerInstantiator(new SpringBeanHandlerInstantiator(context.getBeanFactory())) + .build())); + } + + static class UserDeserializer extends ValueDeserializer { + + @Autowired + private Capitalizer capitalizer; + + @Override + public User deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) { + JsonNode node = jsonParser.readValueAsTree(); + return new User(this.capitalizer.capitalize(node.get("username").asString())); + } + + } + + static class UserSerializer extends ValueSerializer { + + @Autowired + private Capitalizer capitalizer; + + @Override + public void serialize(User user, JsonGenerator jsonGenerator, SerializationContext serializationContext) { + + jsonGenerator.writeStartObject(); + jsonGenerator.writeStringProperty("username", this.capitalizer.capitalize(user.getUsername())); + jsonGenerator.writeEndObject(); + } + + } + + static class UpperCaseKeyDeserializer extends KeyDeserializer { + + @Autowired + private Capitalizer capitalizer; + + @Override + public Object deserializeKey(String key, DeserializationContext context) { + return this.capitalizer.capitalize(key); + } + + } + + static class CustomTypeResolverBuilder extends StdTypeResolverBuilder { + + @Autowired + private Capitalizer capitalizer; + + static boolean autowiredFieldInitialized; + + @Override + public TypeSerializer buildTypeSerializer(SerializationContext serializationContext, JavaType baseType, + Collection subtypes) { + + autowiredFieldInitialized = (this.capitalizer != null); + return super.buildTypeSerializer(serializationContext, baseType, subtypes); + } + + @Override + public TypeDeserializer buildTypeDeserializer(DeserializationContext deserializationContext, JavaType baseType, + Collection subtypes) { + + return super.buildTypeDeserializer(deserializationContext, baseType, subtypes); + } + + } + + static class CustomTypeIdResolver implements TypeIdResolver { + + @Autowired + private Capitalizer capitalizer; + + static boolean autowiredFieldInitialized; + + @Override + public String idFromValueAndType(DatabindContext ctxt, Object o, Class type) { + return type.getClass().getName(); + } + + @Override + public JsonTypeInfo.Id getMechanism() { + return JsonTypeInfo.Id.CUSTOM; + } + + @Override + public String idFromValue(DatabindContext databindContext, Object value) { + autowiredFieldInitialized = (this.capitalizer != null); + return value.getClass().getName(); + } + + @Override + public void init(JavaType type) { + } + + @Override + public @Nullable String idFromBaseType(DatabindContext ctxt) { + return null; + } + + @Override + public @Nullable JavaType typeFromId(DatabindContext context, String id) { + return null; + } + + @Override + public @Nullable String getDescForKnownTypeIds() { + return null; + } + + } + + @JsonDeserialize(using = UserDeserializer.class) + @JsonSerialize(using = UserSerializer.class) + public static class User { + + private final String username; + + User(String username) { + this.username = username; + } + + public String getUsername() { + return this.username; + } + + } + + public static class SecurityRegistry { + + @JsonDeserialize(keyUsing = UpperCaseKeyDeserializer.class) + private Map credentials = new HashMap<>(); + + public void addCredential(String username, String credential) { + this.credentials.put(username, credential); + } + + public Map getCredentials() { + return this.credentials; + } + + } + + @JsonTypeInfo(use = JsonTypeInfo.Id.CUSTOM, property = "type") + @JsonTypeResolver(CustomTypeResolverBuilder.class) + @JsonTypeIdResolver(CustomTypeIdResolver.class) + public static class Group { + + public String getType() { + return Group.class.getName(); + } + + } + + static class Capitalizer { + + String capitalize(String text) { + return text.toUpperCase(Locale.ROOT); + } + + } + +}