diff --git a/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Decoder.java b/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Decoder.java index c6f75a8ffda..38335957fa6 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Decoder.java +++ b/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Decoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 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. @@ -98,17 +98,21 @@ public abstract class AbstractJackson2Decoder extends Jackson2CodecSupport imple @Override public boolean canDecode(ResolvableType elementType, @Nullable MimeType mimeType) { - JavaType javaType = getObjectMapper().constructType(elementType.getType()); + ObjectMapper mapper = selectObjectMapper(elementType, mimeType); + if (mapper == null) { + return false; + } + JavaType javaType = mapper.constructType(elementType.getType()); // Skip String: CharSequenceDecoder + "*/*" comes after if (CharSequence.class.isAssignableFrom(elementType.toClass()) || !supportsMimeType(mimeType)) { return false; } if (!logger.isDebugEnabled()) { - return getObjectMapper().canDeserialize(javaType); + return mapper.canDeserialize(javaType); } else { AtomicReference causeRef = new AtomicReference<>(); - if (getObjectMapper().canDeserialize(javaType, causeRef)) { + if (mapper.canDeserialize(javaType, causeRef)) { return true; } logWarningIfNecessary(javaType, causeRef.get()); @@ -120,7 +124,10 @@ public abstract class AbstractJackson2Decoder extends Jackson2CodecSupport imple public Flux decode(Publisher input, ResolvableType elementType, @Nullable MimeType mimeType, @Nullable Map hints) { - ObjectMapper mapper = getObjectMapper(); + ObjectMapper mapper = selectObjectMapper(elementType, mimeType); + if (mapper == null) { + throw new IllegalStateException("No ObjectMapper for " + elementType); + } boolean forceUseOfBigDecimal = mapper.isEnabled(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS); if (BigDecimal.class.equals(elementType.getType())) { @@ -131,11 +138,11 @@ public abstract class AbstractJackson2Decoder extends Jackson2CodecSupport imple Flux tokens = Jackson2Tokenizer.tokenize(processed, mapper.getFactory(), mapper, true, forceUseOfBigDecimal, getMaxInMemorySize()); - ObjectReader reader = getObjectReader(elementType, hints); + ObjectReader reader = getObjectReader(mapper, elementType, hints); return tokens.handle((tokenBuffer, sink) -> { try { - Object value = reader.readValue(tokenBuffer.asParser(getObjectMapper())); + Object value = reader.readValue(tokenBuffer.asParser(mapper)); logValue(value, hints); if (value != null) { sink.next(value); @@ -176,8 +183,13 @@ public abstract class AbstractJackson2Decoder extends Jackson2CodecSupport imple public Object decode(DataBuffer dataBuffer, ResolvableType targetType, @Nullable MimeType mimeType, @Nullable Map hints) throws DecodingException { + ObjectMapper mapper = selectObjectMapper(targetType, mimeType); + if (mapper == null) { + throw new IllegalStateException("No ObjectMapper for " + targetType); + } + try { - ObjectReader objectReader = getObjectReader(targetType, hints); + ObjectReader objectReader = getObjectReader(mapper, targetType, hints); Object value = objectReader.readValue(dataBuffer.asInputStream()); logValue(value, hints); return value; @@ -190,7 +202,9 @@ public abstract class AbstractJackson2Decoder extends Jackson2CodecSupport imple } } - private ObjectReader getObjectReader(ResolvableType elementType, @Nullable Map hints) { + private ObjectReader getObjectReader( + ObjectMapper mapper, ResolvableType elementType, @Nullable Map hints) { + Assert.notNull(elementType, "'elementType' must not be null"); Class contextClass = getContextClass(elementType); if (contextClass == null && hints != null) { @@ -199,8 +213,8 @@ public abstract class AbstractJackson2Decoder extends Jackson2CodecSupport imple JavaType javaType = getJavaType(elementType.getType(), contextClass); Class jsonView = (hints != null ? (Class) hints.get(Jackson2CodecSupport.JSON_VIEW_HINT) : null); return jsonView != null ? - getObjectMapper().readerWithView(jsonView).forType(javaType) : - getObjectMapper().readerFor(javaType); + mapper.readerWithView(jsonView).forType(javaType) : + mapper.readerFor(javaType); } @Nullable diff --git a/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Encoder.java b/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Encoder.java index aa6ac5a26ab..70ff83924c5 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Encoder.java +++ b/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Encoder.java @@ -104,7 +104,6 @@ public abstract class AbstractJackson2Encoder extends Jackson2CodecSupport imple @Override public boolean canEncode(ResolvableType elementType, @Nullable MimeType mimeType) { - Class clazz = elementType.toClass(); if (!supportsMimeType(mimeType)) { return false; } @@ -114,6 +113,11 @@ public abstract class AbstractJackson2Encoder extends Jackson2CodecSupport imple return false; } } + ObjectMapper mapper = selectObjectMapper(elementType, mimeType); + if (mapper == null) { + return false; + } + Class clazz = elementType.toClass(); if (String.class.isAssignableFrom(elementType.resolve(clazz))) { return false; } @@ -121,11 +125,11 @@ public abstract class AbstractJackson2Encoder extends Jackson2CodecSupport imple return true; } if (!logger.isDebugEnabled()) { - return getObjectMapper().canSerialize(clazz); + return mapper.canSerialize(clazz); } else { AtomicReference causeRef = new AtomicReference<>(); - if (getObjectMapper().canSerialize(clazz, causeRef)) { + if (mapper.canSerialize(clazz, causeRef)) { return true; } logWarningIfNecessary(clazz, causeRef.get()); @@ -150,10 +154,14 @@ public abstract class AbstractJackson2Encoder extends Jackson2CodecSupport imple byte[] separator = getStreamingMediaTypeSeparator(mimeType); if (separator != null) { // streaming try { - ObjectWriter writer = createObjectWriter(elementType, mimeType, null, hints); + ObjectMapper mapper = selectObjectMapper(elementType, mimeType); + if (mapper == null) { + throw new IllegalStateException("No ObjectMapper for " + elementType); + } + ObjectWriter writer = createObjectWriter(mapper, elementType, mimeType, null, hints); ByteArrayBuilder byteBuilder = new ByteArrayBuilder(writer.getFactory()._getBufferRecycler()); JsonEncoding encoding = getJsonEncoding(mimeType); - JsonGenerator generator = getObjectMapper().getFactory().createGenerator(byteBuilder, encoding); + JsonGenerator generator = mapper.getFactory().createGenerator(byteBuilder, encoding); SequenceWriter sequenceWriter = writer.writeValues(generator); return Flux.from(inputStream) @@ -188,6 +196,10 @@ public abstract class AbstractJackson2Encoder extends Jackson2CodecSupport imple public DataBuffer encodeValue(Object value, DataBufferFactory bufferFactory, ResolvableType valueType, @Nullable MimeType mimeType, @Nullable Map hints) { + ObjectMapper mapper = selectObjectMapper(valueType, mimeType); + if (mapper == null) { + throw new IllegalStateException("No ObjectMapper for " + valueType); + } Class jsonView = null; FilterProvider filters = null; if (value instanceof MappingJacksonValue) { @@ -196,7 +208,7 @@ public abstract class AbstractJackson2Encoder extends Jackson2CodecSupport imple jsonView = container.getSerializationView(); filters = container.getFilters(); } - ObjectWriter writer = createObjectWriter(valueType, mimeType, jsonView, hints); + ObjectWriter writer = createObjectWriter(mapper, valueType, mimeType, jsonView, hints); if (filters != null) { writer = writer.with(filters); } @@ -206,7 +218,7 @@ public abstract class AbstractJackson2Encoder extends Jackson2CodecSupport imple logValue(hints, value); - try (JsonGenerator generator = getObjectMapper().getFactory().createGenerator(byteBuilder, encoding)) { + try (JsonGenerator generator = mapper.getFactory().createGenerator(byteBuilder, encoding)) { writer.writeValue(generator, value); generator.flush(); } @@ -282,20 +294,18 @@ public abstract class AbstractJackson2Encoder extends Jackson2CodecSupport imple } } - private ObjectWriter createObjectWriter(ResolvableType valueType, @Nullable MimeType mimeType, + private ObjectWriter createObjectWriter( + ObjectMapper mapper, ResolvableType valueType, @Nullable MimeType mimeType, @Nullable Class jsonView, @Nullable Map hints) { JavaType javaType = getJavaType(valueType.getType(), null); if (jsonView == null && hints != null) { jsonView = (Class) hints.get(Jackson2CodecSupport.JSON_VIEW_HINT); } - ObjectWriter writer = (jsonView != null ? - getObjectMapper().writerWithView(jsonView) : getObjectMapper().writer()); - + ObjectWriter writer = (jsonView != null ? mapper.writerWithView(jsonView) : mapper.writer()); if (javaType.isContainerType()) { writer = writer.forType(javaType); } - return customizeWriter(writer, mimeType, valueType, hints); } diff --git a/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2CodecSupport.java b/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2CodecSupport.java index 5d68e63265f..a34411c99f7 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2CodecSupport.java +++ b/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2CodecSupport.java @@ -21,8 +21,10 @@ import java.lang.reflect.Type; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.function.Consumer; import com.fasterxml.jackson.annotation.JsonView; import com.fasterxml.jackson.databind.JavaType; @@ -40,6 +42,7 @@ import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; import org.springframework.util.MimeType; import org.springframework.util.ObjectUtils; @@ -80,7 +83,10 @@ public abstract class Jackson2CodecSupport { protected final Log logger = HttpLogging.forLogName(getClass()); - private final ObjectMapper objectMapper; + private final ObjectMapper defaultObjectMapper; + + @Nullable + private Map, Map> objectMapperRegistrations; private final List mimeTypes; @@ -90,14 +96,60 @@ public abstract class Jackson2CodecSupport { */ protected Jackson2CodecSupport(ObjectMapper objectMapper, MimeType... mimeTypes) { Assert.notNull(objectMapper, "ObjectMapper must not be null"); - this.objectMapper = objectMapper; + this.defaultObjectMapper = objectMapper; this.mimeTypes = !ObjectUtils.isEmpty(mimeTypes) ? Collections.unmodifiableList(Arrays.asList(mimeTypes)) : DEFAULT_MIME_TYPES; } public ObjectMapper getObjectMapper() { - return this.objectMapper; + return this.defaultObjectMapper; + } + + /** + * Configure the {@link ObjectMapper} instances to use for the given + * {@link Class}. This is useful when you want to deviate from the + * {@link #getObjectMapper() default} ObjectMapper or have the + * {@code ObjectMapper} vary by {@code MediaType}. + *

Note: Use of this method effectively turns off use of + * the default {@link #getObjectMapper() ObjectMapper} and supported + * {@link #getMimeTypes() MimeTypes} for the given class. Therefore it is + * important for the mappings configured here to + * {@link MediaType#includes(MediaType) include} every MediaType that must + * be supported for the given class. + * @param clazz the type of Object to register ObjectMapper instances for + * @param registrar a consumer to populate or otherwise update the + * MediaType-to-ObjectMapper associations for the given Class + * @since 5.3.4 + */ + public void registerObjectMappersForType(Class clazz, Consumer> registrar) { + if (this.objectMapperRegistrations == null) { + this.objectMapperRegistrations = new LinkedHashMap<>(); + } + Map registrations = + this.objectMapperRegistrations.computeIfAbsent(clazz, c -> new LinkedHashMap<>()); + registrar.accept(registrations); + } + + /** + * Return ObjectMapper registrations for the given class, if any. + * @param clazz the class to look up for registrations for + * @return a map with registered MediaType-to-ObjectMapper registrations, + * or empty if in case of no registrations for the given class. + * @since 5.3.4 + */ + @Nullable + public Map getObjectMappersForType(Class clazz) { + for (Map.Entry, Map> entry : getObjectMapperRegistrations().entrySet()) { + if (entry.getKey().isAssignableFrom(clazz)) { + return entry.getValue(); + } + } + return Collections.emptyMap(); + } + + private Map, Map> getObjectMapperRegistrations() { + return (this.objectMapperRegistrations != null ? this.objectMapperRegistrations : Collections.emptyMap()); } /** @@ -140,7 +192,7 @@ public abstract class Jackson2CodecSupport { } protected JavaType getJavaType(Type type, @Nullable Class contextClass) { - return this.objectMapper.constructType(GenericTypeResolver.resolveType(type, contextClass)); + return this.defaultObjectMapper.constructType(GenericTypeResolver.resolveType(type, contextClass)); } protected Map getHints(ResolvableType resolvableType) { @@ -173,4 +225,31 @@ public abstract class Jackson2CodecSupport { @Nullable protected abstract A getAnnotation(MethodParameter parameter, Class annotType); + /** + * Select an ObjectMapper to use, either the main ObjectMapper or another + * if the handling for the given Class has been customized through + * {@link #registerObjectMappersForType(Class, Consumer)}. + * @since 5.3.4 + */ + @Nullable + protected ObjectMapper selectObjectMapper(ResolvableType targetType, @Nullable MimeType targetMimeType) { + if (targetMimeType == null || CollectionUtils.isEmpty(this.objectMapperRegistrations)) { + return this.defaultObjectMapper; + } + Class targetClass = targetType.toClass(); + for (Map.Entry, Map> typeEntry : getObjectMapperRegistrations().entrySet()) { + if (typeEntry.getKey().isAssignableFrom(targetClass)) { + for (Map.Entry objectMapperEntry : typeEntry.getValue().entrySet()) { + if (objectMapperEntry.getKey().includes(targetMimeType)) { + return objectMapperEntry.getValue(); + } + } + // No matching registrations + return null; + } + } + // No registrations + return this.defaultObjectMapper; + } + } diff --git a/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2JsonDecoderTests.java b/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2JsonDecoderTests.java index 4a31da9d3b5..8cfeb045f82 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2JsonDecoderTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2JsonDecoderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 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. @@ -92,6 +92,28 @@ public class Jackson2JsonDecoderTests extends AbstractDecoderTests { + map.put(halJsonMediaType, new ObjectMapper()); + map.put(MediaType.APPLICATION_JSON, new ObjectMapper()); + }); + + assertThat(decoder.canDecode(ResolvableType.forClass(Pojo.class), halJsonMediaType)).isTrue(); + assertThat(decoder.canDecode(ResolvableType.forClass(Pojo.class), MediaType.APPLICATION_JSON)).isTrue(); + assertThat(decoder.canDecode(ResolvableType.forClass(Pojo.class), halFormsJsonMediaType)).isFalse(); + assertThat(decoder.canDecode(ResolvableType.forClass(Map.class), MediaType.APPLICATION_JSON)).isTrue(); + + } + @Test // SPR-15866 public void canDecodeWithProvidedMimeType() { MimeType textJavascript = new MimeType("text", "javascript", StandardCharsets.UTF_8);