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
|
@Test // SPR-13318
|
||||||
@Ignore
|
|
||||||
public void jacksonTypeWithSubType() throws Exception {
|
public void jacksonTypeWithSubType() throws Exception {
|
||||||
SimpleBean body = new SimpleBean(123L, "foo");
|
SimpleBean body = new SimpleBean(123L, "foo");
|
||||||
ResolvableType type = ResolvableType.forClass(Identifiable.class);
|
ResolvableType type = ResolvableType.forClass(Identifiable.class);
|
||||||
|
|
@ -159,7 +158,6 @@ public class MessageWriterResultHandlerTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test // SPR-13318
|
@Test // SPR-13318
|
||||||
@Ignore
|
|
||||||
public void jacksonTypeWithSubTypeOfListElement() throws Exception {
|
public void jacksonTypeWithSubTypeOfListElement() throws Exception {
|
||||||
List<SimpleBean> body = Arrays.asList(new SimpleBean(123L, "foo"), new SimpleBean(456L, "bar"));
|
List<SimpleBean> body = Arrays.asList(new SimpleBean(123L, "foo"), new SimpleBean(456L, "bar"));
|
||||||
ResolvableType type = ResolvableType.forClassWithGenerics(List.class, Identifiable.class);
|
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.IOException;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.nio.ByteBuffer;
|
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.JavaType;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.fasterxml.jackson.databind.ObjectWriter;
|
import com.fasterxml.jackson.databind.ObjectWriter;
|
||||||
|
|
@ -29,11 +30,13 @@ import org.reactivestreams.Publisher;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import org.springframework.core.MethodParameter;
|
||||||
import org.springframework.core.ResolvableType;
|
import org.springframework.core.ResolvableType;
|
||||||
import org.springframework.core.codec.CodecException;
|
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.DataBuffer;
|
||||||
import org.springframework.core.io.buffer.DataBufferFactory;
|
import org.springframework.core.io.buffer.DataBufferFactory;
|
||||||
|
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
import org.springframework.util.MimeType;
|
import org.springframework.util.MimeType;
|
||||||
|
|
||||||
|
|
@ -45,7 +48,7 @@ import org.springframework.util.MimeType;
|
||||||
* @since 5.0
|
* @since 5.0
|
||||||
* @see JacksonJsonDecoder
|
* @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[]{'['});
|
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 static final ByteBuffer END_ARRAY_BUFFER = ByteBuffer.wrap(new byte[]{']'});
|
||||||
|
|
||||||
|
|
||||||
private final ObjectMapper mapper;
|
|
||||||
|
|
||||||
|
|
||||||
public JacksonJsonEncoder() {
|
public JacksonJsonEncoder() {
|
||||||
this(new ObjectMapper());
|
super(Jackson2ObjectMapperBuilder.json().build());
|
||||||
}
|
}
|
||||||
|
|
||||||
public JacksonJsonEncoder(ObjectMapper mapper) {
|
public JacksonJsonEncoder(ObjectMapper mapper) {
|
||||||
super(new MimeType("application", "json", StandardCharsets.UTF_8),
|
super(mapper);
|
||||||
new MimeType("application", "*+json", StandardCharsets.UTF_8));
|
|
||||||
Assert.notNull(mapper, "'mapper' must not be null");
|
|
||||||
|
|
||||||
this.mapper = 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
|
@Override
|
||||||
public Flux<DataBuffer> encode(Publisher<?> inputStream, DataBufferFactory bufferFactory,
|
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) {
|
private DataBuffer encodeValue(Object value, DataBufferFactory bufferFactory, ResolvableType type) {
|
||||||
TypeFactory typeFactory = this.mapper.getTypeFactory();
|
TypeFactory typeFactory = this.mapper.getTypeFactory();
|
||||||
JavaType javaType = typeFactory.constructType(type.getType());
|
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();
|
DataBuffer buffer = bufferFactory.allocateBuffer();
|
||||||
OutputStream outputStream = buffer.asOutputStream();
|
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.JsonTypeInfo;
|
||||||
import com.fasterxml.jackson.annotation.JsonTypeName;
|
import com.fasterxml.jackson.annotation.JsonTypeName;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonView;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
import reactor.test.TestSubscriber;
|
import reactor.test.TestSubscriber;
|
||||||
|
|
||||||
import org.springframework.core.ResolvableType;
|
import org.springframework.core.ResolvableType;
|
||||||
|
|
@ -92,6 +94,22 @@ public class JacksonJsonEncoderTests extends AbstractDataBufferAllocatingTestCas
|
||||||
stringConsumer("]"));
|
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")
|
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
|
||||||
private static class ParentClass {
|
private static class ParentClass {
|
||||||
|
|
@ -105,4 +123,52 @@ public class JacksonJsonEncoderTests extends AbstractDataBufferAllocatingTestCas
|
||||||
private static class Bar extends ParentClass {
|
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