Extra ObjectMapper registrations in Jackson2CodecSupport
See gh-26212
This commit is contained in:
parent
f4c9f6b860
commit
7cdaaa22bd
|
@ -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<Throwable> 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<Object> decode(Publisher<DataBuffer> input, ResolvableType elementType,
|
||||
@Nullable MimeType mimeType, @Nullable Map<String, Object> 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<TokenBuffer> 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<String, Object> 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<String, Object> hints) {
|
||||
private ObjectReader getObjectReader(
|
||||
ObjectMapper mapper, ResolvableType elementType, @Nullable Map<String, Object> 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
|
||||
|
|
|
@ -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<Throwable> 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<String, Object> 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<String, Object> 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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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<Class<?>, Map<MimeType, ObjectMapper>> objectMapperRegistrations;
|
||||
|
||||
private final List<MimeType> 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}.
|
||||
* <p><strong>Note:</strong> 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<Map<MimeType, ObjectMapper>> registrar) {
|
||||
if (this.objectMapperRegistrations == null) {
|
||||
this.objectMapperRegistrations = new LinkedHashMap<>();
|
||||
}
|
||||
Map<MimeType, ObjectMapper> 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<MimeType, ObjectMapper> getObjectMappersForType(Class<?> clazz) {
|
||||
for (Map.Entry<Class<?>, Map<MimeType, ObjectMapper>> entry : getObjectMapperRegistrations().entrySet()) {
|
||||
if (entry.getKey().isAssignableFrom(clazz)) {
|
||||
return entry.getValue();
|
||||
}
|
||||
}
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
private Map<Class<?>, Map<MimeType, ObjectMapper>> 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<String, Object> getHints(ResolvableType resolvableType) {
|
||||
|
@ -173,4 +225,31 @@ public abstract class Jackson2CodecSupport {
|
|||
@Nullable
|
||||
protected abstract <A extends Annotation> A getAnnotation(MethodParameter parameter, Class<A> 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<Class<?>, Map<MimeType, ObjectMapper>> typeEntry : getObjectMapperRegistrations().entrySet()) {
|
||||
if (typeEntry.getKey().isAssignableFrom(targetClass)) {
|
||||
for (Map.Entry<MimeType, ObjectMapper> objectMapperEntry : typeEntry.getValue().entrySet()) {
|
||||
if (objectMapperEntry.getKey().includes(targetMimeType)) {
|
||||
return objectMapperEntry.getValue();
|
||||
}
|
||||
}
|
||||
// No matching registrations
|
||||
return null;
|
||||
}
|
||||
}
|
||||
// No registrations
|
||||
return this.defaultObjectMapper;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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<Jackson2JsonD
|
|||
new MediaType("application", "json", StandardCharsets.ISO_8859_1))).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canDecodeWithObjectMapperRegistrationForType() {
|
||||
MediaType halJsonMediaType = MediaType.parseMediaType("application/hal+json");
|
||||
MediaType halFormsJsonMediaType = MediaType.parseMediaType("application/prs.hal-forms+json");
|
||||
|
||||
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)).isTrue();
|
||||
assertThat(decoder.canDecode(ResolvableType.forClass(Map.class), MediaType.APPLICATION_JSON)).isTrue();
|
||||
|
||||
decoder.registerObjectMappersForType(Pojo.class, map -> {
|
||||
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);
|
||||
|
|
Loading…
Reference in New Issue