提交 903770f0 编写于 作者: S Sebastien Deleuze

Add JsonView and type resolution support to JacksonJsonEncoder

Issue: SPR-14158
上级 4d035e3a
......@@ -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);
......
/*
* 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");
super(mapper);
}
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
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;
}
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册