Add JsonView and type resolution support to JacksonJsonEncoder
Issue: SPR-14158
This commit is contained in:
parent
4d035e3ab1
commit
903770f008
|
|
@ -148,7 +148,6 @@ public class MessageWriterResultHandlerTests {
|
|||
}
|
||||
|
||||
@Test // SPR-13318
|
||||
@Ignore
|
||||
public void jacksonTypeWithSubType() throws Exception {
|
||||
SimpleBean body = new SimpleBean(123L, "foo");
|
||||
ResolvableType type = ResolvableType.forClass(Identifiable.class);
|
||||
|
|
@ -159,7 +158,6 @@ public class MessageWriterResultHandlerTests {
|
|||
}
|
||||
|
||||
@Test // SPR-13318
|
||||
@Ignore
|
||||
public void jacksonTypeWithSubTypeOfListElement() throws Exception {
|
||||
List<SimpleBean> body = Arrays.asList(new SimpleBean(123L, "foo"), new SimpleBean(456L, "bar"));
|
||||
ResolvableType type = ResolvableType.forClassWithGenerics(List.class, Identifiable.class);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -18,9 +18,11 @@ package org.springframework.http.codec.json;
|
|||
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||
import com.fasterxml.jackson.annotation.JsonTypeName;
|
||||
import com.fasterxml.jackson.annotation.JsonView;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.TestSubscriber;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
|
|
@ -92,6 +94,22 @@ public class JacksonJsonEncoderTests extends AbstractDataBufferAllocatingTestCas
|
|||
stringConsumer("]"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void jsonView() throws Exception {
|
||||
JacksonViewBean bean = new JacksonViewBean();
|
||||
bean.setWithView1("with");
|
||||
bean.setWithView2("with");
|
||||
bean.setWithoutView("without");
|
||||
|
||||
ResolvableType type = ResolvableType.forMethodReturnType(JacksonController.class.getMethod("foo"));
|
||||
Flux<DataBuffer> output = this.encoder.encode(Mono.just(bean), this.bufferFactory, type, null);
|
||||
|
||||
TestSubscriber.subscribe(output)
|
||||
.assertComplete()
|
||||
.assertNoError()
|
||||
.assertValuesWith(stringConsumer("{\"withView1\":\"with\"}"));
|
||||
}
|
||||
|
||||
|
||||
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
|
||||
private static class ParentClass {
|
||||
|
|
@ -105,4 +123,52 @@ public class JacksonJsonEncoderTests extends AbstractDataBufferAllocatingTestCas
|
|||
private static class Bar extends ParentClass {
|
||||
}
|
||||
|
||||
private interface MyJacksonView1 {}
|
||||
|
||||
private interface MyJacksonView2 {}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static class JacksonViewBean {
|
||||
|
||||
@JsonView(MyJacksonView1.class)
|
||||
private String withView1;
|
||||
|
||||
@JsonView(MyJacksonView2.class)
|
||||
private String withView2;
|
||||
|
||||
private String withoutView;
|
||||
|
||||
public String getWithView1() {
|
||||
return withView1;
|
||||
}
|
||||
|
||||
public void setWithView1(String withView1) {
|
||||
this.withView1 = withView1;
|
||||
}
|
||||
|
||||
public String getWithView2() {
|
||||
return withView2;
|
||||
}
|
||||
|
||||
public void setWithView2(String withView2) {
|
||||
this.withView2 = withView2;
|
||||
}
|
||||
|
||||
public String getWithoutView() {
|
||||
return withoutView;
|
||||
}
|
||||
|
||||
public void setWithoutView(String withoutView) {
|
||||
this.withoutView = withoutView;
|
||||
}
|
||||
}
|
||||
|
||||
private static class JacksonController {
|
||||
|
||||
@JsonView(MyJacksonView1.class)
|
||||
public JacksonViewBean foo() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue