Extra ObjectMapper registrations in Jackson2CodecSupport

See gh-26212
This commit is contained in:
Rossen Stoyanchev 2021-02-03 17:07:11 +00:00
parent f4c9f6b860
commit 7cdaaa22bd
4 changed files with 153 additions and 28 deletions

View File

@ -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

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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);