Add JsonView and type resolution support to JacksonJsonEncoder

Issue: SPR-14158
This commit is contained in:
Sebastien Deleuze
2016-07-21 13:18:52 +02:00
parent 4d035e3ab1
commit 903770f008
4 changed files with 236 additions and 15 deletions

View File

@@ -0,0 +1,127 @@
/*
* Copyright 2002-2016 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
*
* http://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.http.codec.json;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.type.TypeFactory;
import org.springframework.core.ResolvableType;
import org.springframework.util.MimeType;
/**
* @author Sebastien Deleuze
*/
public class AbstractJacksonJsonCodec {
protected static final List<MimeType> JSON_MIME_TYPES = Arrays.asList(
new MimeType("application", "json", StandardCharsets.UTF_8),
new MimeType("application", "*+json", StandardCharsets.UTF_8));
protected final ObjectMapper mapper;
protected AbstractJacksonJsonCodec(ObjectMapper mapper) {
this.mapper = mapper;
}
/**
* Return the Jackson {@link JavaType} for the specified type and context class.
* <p>The default implementation returns {@code typeFactory.constructType(type, contextClass)},
* but this can be overridden in subclasses, to allow for custom generic collection handling.
* For instance:
* <pre class="code">
* protected JavaType getJavaType(Type type) {
* if (type instanceof Class && List.class.isAssignableFrom((Class)type)) {
* return TypeFactory.collectionType(ArrayList.class, MyBean.class);
* } else {
* return super.getJavaType(type);
* }
* }
* </pre>
* @param type the generic type to return the Jackson JavaType for
* @param contextClass a context class for the target type, for example a class
* in which the target type appears in a method signature (can be {@code null})
* @return the Jackson JavaType
*/
protected JavaType getJavaType(Type type, Class<?> contextClass) {
TypeFactory typeFactory = this.mapper.getTypeFactory();
if (contextClass != null) {
ResolvableType resolvedType = ResolvableType.forType(type);
if (type instanceof TypeVariable) {
ResolvableType resolvedTypeVariable = resolveVariable(
(TypeVariable<?>) type, ResolvableType.forClass(contextClass));
if (resolvedTypeVariable != ResolvableType.NONE) {
return typeFactory.constructType(resolvedTypeVariable.resolve());
}
}
else if (type instanceof ParameterizedType && resolvedType.hasUnresolvableGenerics()) {
ParameterizedType parameterizedType = (ParameterizedType) type;
Class<?>[] generics = new Class<?>[parameterizedType.getActualTypeArguments().length];
Type[] typeArguments = parameterizedType.getActualTypeArguments();
for (int i = 0; i < typeArguments.length; i++) {
Type typeArgument = typeArguments[i];
if (typeArgument instanceof TypeVariable) {
ResolvableType resolvedTypeArgument = resolveVariable(
(TypeVariable<?>) typeArgument, ResolvableType.forClass(contextClass));
if (resolvedTypeArgument != ResolvableType.NONE) {
generics[i] = resolvedTypeArgument.resolve();
}
else {
generics[i] = ResolvableType.forType(typeArgument).resolve();
}
}
else {
generics[i] = ResolvableType.forType(typeArgument).resolve();
}
}
return typeFactory.constructType(ResolvableType.
forClassWithGenerics(resolvedType.getRawClass(), generics).getType());
}
}
return typeFactory.constructType(type);
}
private ResolvableType resolveVariable(TypeVariable<?> typeVariable, ResolvableType contextType) {
ResolvableType resolvedType;
if (contextType.hasGenerics()) {
resolvedType = ResolvableType.forType(typeVariable, contextType);
if (resolvedType.resolve() != null) {
return resolvedType;
}
}
resolvedType = resolveVariable(typeVariable, contextType.getSuperType());
if (resolvedType.resolve() != null) {
return resolvedType;
}
for (ResolvableType ifc : contextType.getInterfaces()) {
resolvedType = resolveVariable(typeVariable, ifc);
if (resolvedType.resolve() != null) {
return resolvedType;
}
}
return ResolvableType.NONE;
}
}

View File

@@ -19,8 +19,9 @@ package org.springframework.http.codec.json;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonView;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
@@ -29,11 +30,13 @@ import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.CodecException;
import org.springframework.core.codec.AbstractEncoder;
import org.springframework.core.codec.Encoder;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.util.Assert;
import org.springframework.util.MimeType;
@@ -45,7 +48,7 @@ import org.springframework.util.MimeType;
* @since 5.0
* @see JacksonJsonDecoder
*/
public class JacksonJsonEncoder extends AbstractEncoder<Object> {
public class JacksonJsonEncoder extends AbstractJacksonJsonCodec implements Encoder<Object> {
private static final ByteBuffer START_ARRAY_BUFFER = ByteBuffer.wrap(new byte[]{'['});
@@ -54,21 +57,26 @@ public class JacksonJsonEncoder extends AbstractEncoder<Object> {
private static final ByteBuffer END_ARRAY_BUFFER = ByteBuffer.wrap(new byte[]{']'});
private final ObjectMapper mapper;
public JacksonJsonEncoder() {
this(new ObjectMapper());
super(Jackson2ObjectMapperBuilder.json().build());
}
public JacksonJsonEncoder(ObjectMapper mapper) {
super(new MimeType("application", "json", StandardCharsets.UTF_8),
new MimeType("application", "*+json", StandardCharsets.UTF_8));
Assert.notNull(mapper, "'mapper' must not be null");
this.mapper = mapper;
super(mapper);
}
@Override
public boolean canEncode(ResolvableType elementType, MimeType mimeType, Object... hints) {
if (mimeType == null) {
return true;
}
return JSON_MIME_TYPES.stream().anyMatch(m -> m.isCompatibleWith(mimeType));
}
@Override
public List<MimeType> getEncodableMimeTypes() {
return JSON_MIME_TYPES;
}
@Override
public Flux<DataBuffer> encode(Publisher<?> inputStream, DataBufferFactory bufferFactory,
@@ -97,7 +105,29 @@ public class JacksonJsonEncoder extends AbstractEncoder<Object> {
private DataBuffer encodeValue(Object value, DataBufferFactory bufferFactory, ResolvableType type) {
TypeFactory typeFactory = this.mapper.getTypeFactory();
JavaType javaType = typeFactory.constructType(type.getType());
ObjectWriter writer = this.mapper.writerFor(javaType);
MethodParameter returnType = (type.getSource() instanceof MethodParameter ?
(MethodParameter)type.getSource() : null);
if (type != null && value != null && type.isAssignableFrom(value.getClass())) {
javaType = getJavaType(type.getType(), null);
}
ObjectWriter writer;
if (returnType != null && returnType.getMethodAnnotation(JsonView.class) != null) {
JsonView annotation = returnType.getMethodAnnotation(JsonView.class);
Class<?>[] classes = annotation.value();
if (classes.length != 1) {
throw new IllegalArgumentException(
"@JsonView only supported for response body advice with exactly 1 class argument: " + returnType);
}
writer = this.mapper.writerWithView(classes[0]);
}
else {
writer = this.mapper.writer();
}
if (javaType != null && javaType.isContainerType()) {
writer = writer.forType(javaType);
}
DataBuffer buffer = bufferFactory.allocateBuffer();
OutputStream outputStream = buffer.asOutputStream();