[Encoder|Decoder]HttpMessageWriter server support

There is a natural way to implement ServerHttpMessage[Reader|Writer]
from [Encoder|Decoder]HttpMessageWriter by resolving hints first via
a protected method and then delegating to the regular read or write.

There is no downside either since it does not prevent
[Encoder|Decoder]HttpMessageWriter from being used for both client and
server scenarios while they're more useful.

As a positive side effect AbstractServerHttpMessage[Reader|Writer] can
be removed further simplfications can be made (in a future commit) to
accept ServerHttpMessageWriter for configuration purposes on the server
side and remove instanceof checks for ServerHttpMessageWriter.
This commit is contained in:
Rossen Stoyanchev 2017-03-20 15:57:48 -04:00
parent 1be2d8343d
commit 5f8bc4552f
7 changed files with 104 additions and 239 deletions

View File

@ -1,107 +0,0 @@
/*
* 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;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.http.MediaType;
import org.springframework.http.ReactiveHttpInputMessage;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
/**
* {@link HttpMessageReader} wrapper that implements {@link ServerHttpMessageReader} in order
* to allow providing hints to the nested {@code reader} or setting the response status for
* example, by implementing {@link #resolveReadHints(ResolvableType, ResolvableType, ServerHttpRequest)}.
*
* @author Sebastien Deleuze
* @since 5.0
*/
public abstract class AbstractServerHttpMessageReader<T> implements ServerHttpMessageReader<T> {
private HttpMessageReader<T> reader;
public AbstractServerHttpMessageReader(HttpMessageReader<T> reader) {
this.reader = reader;
}
@Override
public boolean canRead(ResolvableType elementType, MediaType mediaType) {
return this.reader.canRead(elementType, mediaType);
}
@Override
public Flux<T> read(ResolvableType elementType, ReactiveHttpInputMessage inputMessage, Map<String, Object> hints) {
return this.reader.read(elementType, inputMessage, hints);
}
@Override
public Mono<T> readMono(ResolvableType elementType, ReactiveHttpInputMessage inputMessage, Map<String, Object> hints) {
return this.reader.readMono(elementType, inputMessage, hints);
}
@Override
public List<MediaType> getReadableMediaTypes() {
return this.reader.getReadableMediaTypes();
}
@Override
public Flux<T> read(ResolvableType streamType, ResolvableType elementType,
ServerHttpRequest request, ServerHttpResponse response, Map<String, Object> hints) {
Map<String, Object> mergedHints = new HashMap<>(hints);
mergedHints.putAll(resolveReadHints(streamType, elementType, request));
return (this.reader instanceof ServerHttpMessageReader ?
((ServerHttpMessageReader<T>)this.reader).read(streamType, elementType, request, response, mergedHints) :
this.read(elementType, request, mergedHints));
}
@Override
public Mono<T> readMono(ResolvableType streamType, ResolvableType elementType,
ServerHttpRequest request, ServerHttpResponse response, Map<String, Object> hints) {
Map<String, Object> mergedHints = new HashMap<>(hints);
mergedHints.putAll(resolveReadHints(streamType, elementType, request));
return (this.reader instanceof ServerHttpMessageReader ?
((ServerHttpMessageReader<T>)this.reader).readMono(streamType, elementType, request, response, mergedHints) :
this.readMono(elementType, request, mergedHints));
}
/**
* Invoked before reading the request to resolve hints by
* {@link #read(ResolvableType, ResolvableType, ServerHttpRequest, ServerHttpResponse, Map)}.
*
* @param streamType the original type used for the method return value. For annotation
* based controllers, the {@link MethodParameter} is available via {@link ResolvableType#getSource()}.
* @param elementType the stream element type to process
* @param request the current HTTP request
* @return Additional information about how to write the body
*/
protected abstract Map<String, Object> resolveReadHints(ResolvableType streamType,
ResolvableType elementType, ServerHttpRequest request);
}

View File

@ -1,93 +0,0 @@
/*
* 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;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;
import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.http.MediaType;
import org.springframework.http.ReactiveHttpOutputMessage;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
/**
* {@link HttpMessageWriter} wrapper that implements {@link ServerHttpMessageWriter} in order
* to allow providing hints to the nested {@code writer} or setting the response status for
* example, by implementing {@link #resolveWriteHints(ResolvableType, ResolvableType, MediaType, ServerHttpRequest)}.
*
* @author Sebastien Deleuze
* @since 5.0
*/
public abstract class AbstractServerHttpMessageWriter<T> implements ServerHttpMessageWriter<T> {
private HttpMessageWriter<T> writer;
public AbstractServerHttpMessageWriter(HttpMessageWriter<T> writer) {
this.writer = writer;
}
@Override
public boolean canWrite(ResolvableType elementType, MediaType mediaType) {
return this.writer.canWrite(elementType, mediaType);
}
@Override
public List<MediaType> getWritableMediaTypes() {
return this.writer.getWritableMediaTypes();
}
@Override
public Mono<Void> write(Publisher<? extends T> inputStream, ResolvableType elementType,
MediaType mediaType, ReactiveHttpOutputMessage outputMessage, Map<String, Object> hints) {
return this.writer.write(inputStream, elementType, mediaType, outputMessage, hints);
}
@Override
public Mono<Void> write(Publisher<? extends T> inputStream, ResolvableType streamType, ResolvableType elementType,
MediaType mediaType, ServerHttpRequest request, ServerHttpResponse response, Map<String, Object> hints) {
Map<String, Object> mergedHints = new HashMap<>(hints);
mergedHints.putAll(resolveWriteHints(streamType, elementType, mediaType, request));
return (this.writer instanceof ServerHttpMessageWriter ?
((ServerHttpMessageWriter<T>)this.writer).write(inputStream, streamType,
elementType, mediaType, request, response, mergedHints) :
this.writer.write(inputStream, elementType, mediaType, response, mergedHints));
}
/**
* Invoked before writing the response to resolve hints by
* {@link #write(Publisher, ResolvableType, ResolvableType, MediaType, ServerHttpRequest, ServerHttpResponse, Map)}.
* @param streamType the original type used for the method return value. For annotation
* based controllers, the {@link MethodParameter} is available via {@link ResolvableType#getSource()}.
* @param elementType the stream element type to process
* @param mediaType the content type to use when writing. May be {@code null} to
* indicate that the default content type of the converter must be used.
* @param request the current HTTP request
* @return additional information about how to write the body
*/
protected abstract Map<String, Object> resolveWriteHints(ResolvableType streamType, ResolvableType elementType,
MediaType mediaType, ServerHttpRequest request);
}

View File

@ -17,6 +17,7 @@
package org.springframework.http.codec;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -28,6 +29,8 @@ import org.springframework.core.codec.Decoder;
import org.springframework.http.HttpMessage;
import org.springframework.http.MediaType;
import org.springframework.http.ReactiveHttpInputMessage;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.util.Assert;
/**
@ -38,7 +41,7 @@ import org.springframework.util.Assert;
* @author Rossen Stoyanchev
* @since 5.0
*/
public class DecoderHttpMessageReader<T> implements HttpMessageReader<T> {
public class DecoderHttpMessageReader<T> implements ServerHttpMessageReader<T> {
private final Decoder<T> decoder;
@ -94,4 +97,39 @@ public class DecoderHttpMessageReader<T> implements HttpMessageReader<T> {
return (contentType != null ? contentType : MediaType.APPLICATION_OCTET_STREAM);
}
// ServerHttpMessageReader...
@Override
public Flux<T> read(ResolvableType streamType, ResolvableType elementType,
ServerHttpRequest request, ServerHttpResponse response, Map<String, Object> hints) {
Map<String, Object> allHints = new HashMap<>(4);
allHints.putAll(resolveReadHints(streamType, elementType, request, response));
allHints.putAll(hints);
return read(elementType, request, allHints);
}
@Override
public Mono<T> readMono(ResolvableType streamType, ResolvableType elementType,
ServerHttpRequest request, ServerHttpResponse response, Map<String, Object> hints) {
Map<String, Object> allHints = new HashMap<>(4);
allHints.putAll(resolveReadHints(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.
*/
protected Map<String, Object> resolveReadHints(ResolvableType streamType,
ResolvableType elementType, ServerHttpRequest request, ServerHttpResponse response) {
return Collections.emptyMap();
}
}

View File

@ -16,6 +16,8 @@
package org.springframework.http.codec;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -29,6 +31,8 @@ import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
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 static org.springframework.core.codec.AbstractEncoder.FLUSHING_STRATEGY_HINT;
@ -42,7 +46,7 @@ import static org.springframework.core.codec.AbstractEncoder.FlushingStrategy.AF
* @author Rossen Stoyanchev
* @since 5.0
*/
public class EncoderHttpMessageWriter<T> implements HttpMessageWriter<T> {
public class EncoderHttpMessageWriter<T> implements ServerHttpMessageWriter<T> {
private final Encoder<T> encoder;
@ -119,4 +123,29 @@ public class EncoderHttpMessageWriter<T> implements HttpMessageWriter<T> {
return main;
}
// ServerHttpMessageWriter...
@Override
public Mono<Void> write(Publisher<? extends T> inputStream, ResolvableType streamType,
ResolvableType elementType, MediaType mediaType, ServerHttpRequest request,
ServerHttpResponse response, Map<String, Object> hints) {
Map<String, Object> allHints = new HashMap<>();
allHints.putAll(resolveWriteHints(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.
*/
protected Map<String, Object> resolveWriteHints(ResolvableType streamType, ResolvableType elementType,
MediaType mediaType, ServerHttpRequest request, ServerHttpResponse response) {
return Collections.emptyMap();
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* 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.
@ -23,8 +23,10 @@ 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:
@ -33,18 +35,21 @@ import org.springframework.http.server.reactive.ServerHttpRequest;
* </ul>
*
* @author Sebastien Deleuze
* @author Rossen Stoyanchev
* @since 5.0
* @see com.fasterxml.jackson.annotation.JsonView
*/
public class Jackson2ServerHttpMessageReader extends AbstractServerHttpMessageReader<Object> {
public class Jackson2ServerHttpMessageReader extends DecoderHttpMessageReader<Object> {
public Jackson2ServerHttpMessageReader(HttpMessageReader<Object> reader) {
super(reader);
public Jackson2ServerHttpMessageReader(Decoder<Object> decoder) {
super(decoder);
}
@Override
protected Map<String, Object> resolveReadHints(ResolvableType streamType,
ResolvableType elementType, ServerHttpRequest request) {
ResolvableType elementType, ServerHttpRequest request, ServerHttpResponse response) {
Object source = streamType.getSource();
MethodParameter parameter = (source instanceof MethodParameter ? (MethodParameter)source : null);

View File

@ -44,38 +44,13 @@ import org.springframework.http.server.reactive.ServerHttpResponse;
* @since 5.0
* @see com.fasterxml.jackson.annotation.JsonView
*/
public class Jackson2ServerHttpMessageWriter extends AbstractServerHttpMessageWriter<Object> {
public class Jackson2ServerHttpMessageWriter extends EncoderHttpMessageWriter<Object> {
public Jackson2ServerHttpMessageWriter(Encoder<Object> encoder) {
super(new EncoderHttpMessageWriter<>(encoder));
super(encoder);
}
public Jackson2ServerHttpMessageWriter(HttpMessageWriter<Object> writer) {
super(writer);
}
@Override
protected Map<String, Object> resolveWriteHints(ResolvableType streamType,
ResolvableType elementType, MediaType mediaType, ServerHttpRequest request) {
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;
}
@Override
public Mono<Void> write(Publisher<?> inputStream, ResolvableType elementType, MediaType mediaType,
@ -100,4 +75,26 @@ public class Jackson2ServerHttpMessageWriter extends AbstractServerHttpMessageWr
}
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

@ -491,12 +491,8 @@ public class WebFluxConfigurationSupport implements ApplicationContextAware {
Jackson2JsonEncoder encoder = new Jackson2JsonEncoder();
writers.add(new Jackson2ServerHttpMessageWriter(encoder));
sseDataEncoders.add(encoder);
HttpMessageWriter<Object> writer = new ServerSentEventHttpMessageWriter(sseDataEncoders);
writers.add(new Jackson2ServerHttpMessageWriter(writer));
}
else {
writers.add(new ServerSentEventHttpMessageWriter(sseDataEncoders));
}
writers.add(new ServerSentEventHttpMessageWriter(sseDataEncoders));
}
/**