Introduce ServerHttp[Encoder|Decoder]

ServerHttpEncoder and ServerHttpDecoder are HTTP-specific
specializations that can prepare encoding and decoding hints from
extra information available on the server side.

As a result Jackson2ServerHttpMessageReader is no longer needed.
This commit is contained in:
Rossen Stoyanchev 2017-03-20 16:01:30 -04:00
parent 5f8bc4552f
commit f65544c192
10 changed files with 222 additions and 116 deletions

View File

@ -34,7 +34,11 @@ import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.util.Assert;
/**
* Implementation of {@code HttpMessageReader} delegating to a {@link Decoder}.
* {@code HttpMessageReader} that wraps and delegates to a {@link Decoder}.
*
* <p>Also a {@code ServerHttpMessageReader} that pre-resolves decoding hints
* from the extra information available on the server side such as the request
* or controller method parameter annotations.
*
* @author Arjen Poutsma
* @author Sebastien Deleuze
@ -105,7 +109,7 @@ public class DecoderHttpMessageReader<T> implements ServerHttpMessageReader<T> {
ServerHttpRequest request, ServerHttpResponse response, Map<String, Object> hints) {
Map<String, Object> allHints = new HashMap<>(4);
allHints.putAll(resolveReadHints(streamType, elementType, request, response));
allHints.putAll(getReadHints(streamType, elementType, request, response));
allHints.putAll(hints);
return read(elementType, request, allHints);
@ -116,19 +120,24 @@ public class DecoderHttpMessageReader<T> implements ServerHttpMessageReader<T> {
ServerHttpRequest request, ServerHttpResponse response, Map<String, Object> hints) {
Map<String, Object> allHints = new HashMap<>(4);
allHints.putAll(resolveReadHints(streamType, elementType, request, response));
allHints.putAll(getReadHints(streamType, elementType, request, response));
allHints.putAll(hints);
return readMono(elementType, request, allHints);
}
/**
* Resolve hints to pass to the decoder, e.g. by checking for annotations
* on a controller method parameter or checking the server request.
* Get additional hints for decoding for example based on the server request
* or annotations from controller method parameters. By default, delegate to
* the decoder if it is an instance of {@link ServerHttpDecoder}.
*/
protected Map<String, Object> resolveReadHints(ResolvableType streamType,
protected Map<String, Object> getReadHints(ResolvableType streamType,
ResolvableType elementType, ServerHttpRequest request, ServerHttpResponse response) {
if (this.decoder instanceof ServerHttpDecoder) {
ServerHttpDecoder<?> httpDecoder = (ServerHttpDecoder<?>) this.decoder;
return httpDecoder.getDecodeHints(streamType, elementType, request, response);
}
return Collections.emptyMap();
}

View File

@ -39,7 +39,11 @@ import static org.springframework.core.codec.AbstractEncoder.FLUSHING_STRATEGY_H
import static org.springframework.core.codec.AbstractEncoder.FlushingStrategy.AFTER_EACH_ELEMENT;
/**
* Implementation of {@code HttpMessageWriter} delegating to an {@link Encoder}.
* {@code HttpMessageWriter} that wraps and delegates to a {@link Encoder}.
*
* <p>Also a {@code ServerHttpMessageWriter} that pre-resolves encoding hints
* from the extra information available on the server side such as the request
* or controller method annotations.
*
* @author Arjen Poutsma
* @author Sebastien Deleuze
@ -132,19 +136,24 @@ public class EncoderHttpMessageWriter<T> implements ServerHttpMessageWriter<T> {
ServerHttpResponse response, Map<String, Object> hints) {
Map<String, Object> allHints = new HashMap<>();
allHints.putAll(resolveWriteHints(streamType, elementType, mediaType, request, response));
allHints.putAll(getWriteHints(streamType, elementType, mediaType, request, response));
allHints.putAll(hints);
return write(inputStream, elementType, mediaType, response, allHints);
}
/**
* Resolve hints to pass to the encoder, e.g. by checking for annotations
* on a controller method parameter or checking the server request.
* Get additional hints for encoding for example based on the server request
* or annotations from controller method parameters. By default, delegate to
* the encoder if it is an instance of {@link ServerHttpEncoder}.
*/
protected Map<String, Object> resolveWriteHints(ResolvableType streamType, ResolvableType elementType,
protected Map<String, Object> getWriteHints(ResolvableType streamType, ResolvableType elementType,
MediaType mediaType, ServerHttpRequest request, ServerHttpResponse response) {
if (this.encoder instanceof ServerHttpEncoder) {
ServerHttpEncoder<?> httpEncoder = (ServerHttpEncoder<?>) this.encoder;
return httpEncoder.getEncodeHints(streamType, elementType, mediaType, request, response);
}
return Collections.emptyMap();
}

View File

@ -1,70 +0,0 @@
/*
* Copyright 2002-2017 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;
import java.util.Collections;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonView;
import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.Decoder;
import org.springframework.http.codec.json.AbstractJackson2Codec;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
/**
* {@link ServerHttpMessageReader} that resolves those annotation or request based Jackson 2 hints:
* <ul>
* <li>{@code @JsonView} + {@code @RequestBody} annotated handler method parameter</li>
* </ul>
*
* @author Sebastien Deleuze
* @author Rossen Stoyanchev
* @since 5.0
* @see com.fasterxml.jackson.annotation.JsonView
*/
public class Jackson2ServerHttpMessageReader extends DecoderHttpMessageReader<Object> {
public Jackson2ServerHttpMessageReader(Decoder<Object> decoder) {
super(decoder);
}
@Override
protected Map<String, Object> resolveReadHints(ResolvableType streamType,
ResolvableType elementType, ServerHttpRequest request, ServerHttpResponse response) {
Object source = streamType.getSource();
MethodParameter parameter = (source instanceof MethodParameter ? (MethodParameter)source : null);
if (parameter != null) {
JsonView annotation = parameter.getParameterAnnotation(JsonView.class);
if (annotation != null) {
Class<?>[] classes = annotation.value();
if (classes.length != 1) {
throw new IllegalArgumentException(
"@JsonView only supported for read hints with exactly 1 class argument: " + parameter);
}
return Collections.singletonMap(AbstractJackson2Codec.JSON_VIEW_HINT, classes[0]);
}
}
return Collections.emptyMap();
}
}

View File

@ -20,22 +20,20 @@ package org.springframework.http.codec;
import java.util.HashMap;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonView;
import org.reactivestreams.Publisher;
import static org.springframework.core.codec.AbstractEncoder.FLUSHING_STRATEGY_HINT;
import static org.springframework.core.codec.AbstractEncoder.FlushingStrategy.AFTER_EACH_ELEMENT;
import reactor.core.publisher.Mono;
import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.AbstractEncoder;
import org.springframework.core.codec.Encoder;
import org.springframework.http.MediaType;
import org.springframework.http.ReactiveHttpOutputMessage;
import org.springframework.http.codec.json.AbstractJackson2Codec;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import static org.springframework.core.codec.AbstractEncoder.FLUSHING_STRATEGY_HINT;
import static org.springframework.core.codec.AbstractEncoder.FlushingStrategy.AFTER_EACH_ELEMENT;
/**
* Jackson {@link ServerHttpMessageWriter} that resolves {@code @JsonView} annotated handler
* method and deals with {@link AbstractEncoder#FLUSHING_STRATEGY_HINT}.
@ -76,25 +74,4 @@ public class Jackson2ServerHttpMessageWriter extends EncoderHttpMessageWriter<Ob
return super.write(inputStream, streamType, elementType, mediaType, request, response, hints);
}
@Override
protected Map<String, Object> resolveWriteHints(ResolvableType streamType, ResolvableType elementType,
MediaType mediaType, ServerHttpRequest request, ServerHttpResponse response) {
Map<String, Object> hints = new HashMap<>();
Object source = streamType.getSource();
MethodParameter returnValue = (source instanceof MethodParameter ? (MethodParameter)source : null);
if (returnValue != null) {
JsonView annotation = returnValue.getMethodAnnotation(JsonView.class);
if (annotation != null) {
Class<?>[] classes = annotation.value();
if (classes.length != 1) {
throw new IllegalArgumentException(
"@JsonView only supported for write hints with exactly 1 class argument: " + returnValue);
}
hints.put(AbstractJackson2Codec.JSON_VIEW_HINT, classes[0]);
}
}
return hints;
}
}

View File

@ -0,0 +1,51 @@
/*
* Copyright 2002-2017 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;
import java.util.Map;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.Decoder;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
/**
* {@code Decoder} extension for server-side decoding of the HTTP request body.
*
* @author Rossen Stoyanchev
* @since 5.0
*/
public interface ServerHttpDecoder<T> extends Decoder<T> {
/**
* Get decoding hints based on the server request or annotations on the
* target controller method parameter.
*
* @param actualType the actual target type to decode to, possibly a reactive
* wrapper and sourced from {@link org.springframework.core.MethodParameter},
* i.e. providing access to method parameter annotations.
* @param elementType the element type within {@code Flux/Mono} that we're
* trying to decode to.
* @param request the current request
* @param response the current response
* @return a Map with hints, possibly empty
*/
Map<String, Object> getDecodeHints(ResolvableType actualType, ResolvableType elementType,
ServerHttpRequest request, ServerHttpResponse response);
}

View File

@ -0,0 +1,53 @@
/*
* Copyright 2002-2017 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;
import java.util.Map;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.Encoder;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
/**
* {@code Encoder} extension for server-side encoding of the HTTP response body.
*
* @author Rossen Stoyanchev
* @since 5.0
*/
public interface ServerHttpEncoder<T> extends Encoder<T> {
/**
* Get decoding hints based on the server request or annotations on the
* target controller method parameter.
*
* @param actualType the actual source type to encode, possibly a reactive
* wrapper and sourced from {@link org.springframework.core.MethodParameter},
* i.e. providing access to method annotations.
* @param elementType the element type within {@code Flux/Mono} that we're
* trying to encode.
* @param request the current request
* @param response the current response
* @return a Map with hints, possibly empty
*/
Map<String, Object> getEncodeHints(ResolvableType actualType, ResolvableType elementType,
MediaType mediaType, ServerHttpRequest request, ServerHttpResponse response);
}

View File

@ -35,6 +35,8 @@ import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.http.MediaType;
import org.springframework.http.ReactiveHttpOutputMessage;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.util.Assert;
import org.springframework.util.MimeTypeUtils;
@ -47,7 +49,7 @@ import org.springframework.util.MimeTypeUtils;
* @author Arjen Poutsma
* @since 5.0
*/
public class ServerSentEventHttpMessageWriter implements HttpMessageWriter<Object> {
public class ServerSentEventHttpMessageWriter implements ServerHttpMessageWriter<Object> {
/**
* Server-Sent Events hint key expecting a {@link Boolean} value which when set to true
@ -161,4 +163,23 @@ public class ServerSentEventHttpMessageWriter implements HttpMessageWriter<Objec
return Mono.just(buffer);
}
@Override
public Mono<Void> write(Publisher<?> inputStream, ResolvableType streamType, ResolvableType elementType,
MediaType mediaType, ServerHttpRequest request, ServerHttpResponse response,
Map<String, Object> hints) {
Map<String, Object> allHints = this.dataEncoders.stream()
.filter(encoder -> encoder instanceof ServerHttpEncoder)
.map(encoder -> (ServerHttpEncoder<?>) encoder)
.map(encoder -> encoder.getEncodeHints(streamType, elementType, mediaType, request, response))
.reduce(new HashMap<>(), (t, u) -> {
t.putAll(u);
return t;
});
allHints.putAll(hints);
return write(inputStream, elementType, mediaType, response, allHints);
}
}

View File

@ -17,9 +17,11 @@
package org.springframework.http.codec.json;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonView;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
@ -30,10 +32,12 @@ 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.Decoder;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.codec.ServerHttpDecoder;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.util.Assert;
import org.springframework.util.MimeType;
@ -45,7 +49,7 @@ import org.springframework.util.MimeType;
* @since 5.0
* @see Jackson2JsonEncoder
*/
public class Jackson2JsonDecoder extends AbstractJackson2Codec implements Decoder<Object> {
public class Jackson2JsonDecoder extends AbstractJackson2Codec implements ServerHttpDecoder<Object> {
private final JsonObjectDecoder fluxObjectDecoder = new JsonObjectDecoder(true);
@ -123,4 +127,27 @@ public class Jackson2JsonDecoder extends AbstractJackson2Codec implements Decode
});
}
// ServerHttpDecoder...
@Override
public Map<String, Object> getDecodeHints(ResolvableType actualType, ResolvableType elementType,
ServerHttpRequest request, ServerHttpResponse response) {
Object source = actualType.getSource();
MethodParameter parameter = (source instanceof MethodParameter ? (MethodParameter)source : null);
if (parameter != null) {
JsonView annotation = parameter.getParameterAnnotation(JsonView.class);
if (annotation != null) {
Class<?>[] classes = annotation.value();
if (classes.length != 1) {
throw new IllegalArgumentException(
"@JsonView only supported for read hints with exactly 1 class argument: " + parameter);
}
return Collections.singletonMap(AbstractJackson2Codec.JSON_VIEW_HINT, classes[0]);
}
}
return Collections.emptyMap();
}
}

View File

@ -18,9 +18,11 @@ package org.springframework.http.codec.json;
import java.io.IOException;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonView;
import com.fasterxml.jackson.core.PrettyPrinter;
import com.fasterxml.jackson.core.util.DefaultIndenter;
import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
@ -31,20 +33,25 @@ import com.fasterxml.jackson.databind.SerializationConfig;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.type.TypeFactory;
import org.reactivestreams.Publisher;
import static org.springframework.http.MediaType.APPLICATION_STREAM_JSON;
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.Encoder;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerHttpEncoder;
import org.springframework.http.codec.ServerSentEventHttpMessageWriter;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.util.Assert;
import org.springframework.util.MimeType;
import static org.springframework.http.MediaType.APPLICATION_STREAM_JSON;
/**
* Encode from an {@code Object} stream to a byte stream of JSON objects,
* using Jackson 2.6+.
@ -54,7 +61,7 @@ import org.springframework.util.MimeType;
* @since 5.0
* @see Jackson2JsonDecoder
*/
public class Jackson2JsonEncoder extends AbstractJackson2Codec implements Encoder<Object> {
public class Jackson2JsonEncoder extends AbstractJackson2Codec implements ServerHttpEncoder<Object> {
private final PrettyPrinter ssePrettyPrinter;
@ -144,4 +151,28 @@ public class Jackson2JsonEncoder extends AbstractJackson2Codec implements Encode
return buffer;
}
// ServerHttpEncoder...
@Override
public Map<String, Object> getEncodeHints(ResolvableType actualType, ResolvableType elementType,
MediaType mediaType, ServerHttpRequest request, ServerHttpResponse response) {
Map<String, Object> hints = new HashMap<>();
Object source = actualType.getSource();
MethodParameter returnValue = (source instanceof MethodParameter ? (MethodParameter)source : null);
if (returnValue != null) {
JsonView annotation = returnValue.getMethodAnnotation(JsonView.class);
if (annotation != null) {
Class<?>[] classes = annotation.value();
if (classes.length != 1) {
throw new IllegalArgumentException(
"@JsonView only supported for write hints with exactly 1 class argument: " + returnValue);
}
hints.put(AbstractJackson2Codec.JSON_VIEW_HINT, classes[0]);
}
}
return hints;
}
}

View File

@ -50,7 +50,6 @@ import org.springframework.http.codec.DecoderHttpMessageReader;
import org.springframework.http.codec.EncoderHttpMessageWriter;
import org.springframework.http.codec.HttpMessageReader;
import org.springframework.http.codec.HttpMessageWriter;
import org.springframework.http.codec.Jackson2ServerHttpMessageReader;
import org.springframework.http.codec.Jackson2ServerHttpMessageWriter;
import org.springframework.http.codec.ResourceHttpMessageWriter;
import org.springframework.http.codec.ServerSentEventHttpMessageWriter;
@ -339,8 +338,7 @@ public class WebFluxConfigurationSupport implements ApplicationContextAware {
readers.add(new DecoderHttpMessageReader<>(new Jaxb2XmlDecoder()));
}
if (jackson2Present) {
readers.add(new Jackson2ServerHttpMessageReader(
new DecoderHttpMessageReader<>(new Jackson2JsonDecoder())));
readers.add(new DecoderHttpMessageReader<>(new Jackson2JsonDecoder()));
}
}