From 857e77eec2cc02ffd4b4f1f22808ccbe32351e3f Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Tue, 20 Sep 2016 11:55:19 +0200 Subject: [PATCH] Make ServerHttpMessageReader/Writer more powerful and flexible This commit makes it possible, in addition to provide hints, to perform additional operations with the request and the response at ServerHttpMessageReader/Writer level. AbstractServerHttpMessageReader/Writer now provide convenient beforeRead/beforeWrite abstract methods for such need. Issue: SPR-14557 --- ...AbstractMessageReaderArgumentResolver.java | 23 ++++---- .../AbstractMessageWriterResultHandler.java | 21 ++++---- .../AbstractServerHttpMessageReader.java | 54 ++++++++++++------- .../AbstractServerHttpMessageWriter.java | 50 +++++++++-------- .../Jackson2ServerHttpMessageReader.java | 6 +-- .../Jackson2ServerHttpMessageWriter.java | 15 ++++-- .../http/codec/ServerHttpMessageReader.java | 38 ++++++++++--- .../http/codec/ServerHttpMessageWriter.java | 21 +++++--- 8 files changed, 143 insertions(+), 85 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java index 44e779dfbdb..72d82b53a2a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java @@ -18,7 +18,6 @@ package org.springframework.web.reactive.result.method.annotation; import java.lang.annotation.Annotation; import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.function.Function; import java.util.stream.Collectors; @@ -35,6 +34,7 @@ import org.springframework.http.MediaType; import org.springframework.http.codec.HttpMessageReader; import org.springframework.http.codec.ServerHttpMessageReader; import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.validation.BeanPropertyBindingResult; @@ -126,6 +126,7 @@ public abstract class AbstractMessageReaderArgumentResolver { } ServerHttpRequest request = exchange.getRequest(); + ServerHttpResponse response = exchange.getResponse(); MediaType mediaType = request.getHeaders().getContentType(); if (mediaType == null) { mediaType = MediaType.APPLICATION_OCTET_STREAM; @@ -133,15 +134,14 @@ public abstract class AbstractMessageReaderArgumentResolver { for (HttpMessageReader reader : getMessageReaders()) { - Map hints = (reader instanceof ServerHttpMessageReader ? - ((ServerHttpMessageReader)reader).resolveReadHints(bodyType, elementType, - mediaType, exchange.getRequest()) : Collections.emptyMap()); - - if (reader.canRead(elementType, mediaType, hints)) { + if (reader.canRead(elementType, mediaType)) { if (adapter != null && adapter.getDescriptor().isMultiValue()) { - Flux flux = reader.read(elementType, request, hints) - .onErrorResumeWith(ex -> Flux.error(getReadError(ex, bodyParameter))); + Flux flux = (reader instanceof ServerHttpMessageReader ? + ((ServerHttpMessageReader)reader).read(bodyType, elementType, + request, response, Collections.emptyMap()) : + reader.read(elementType, request, Collections.emptyMap()) + .onErrorResumeWith(ex -> Flux.error(getReadError(ex, bodyParameter)))); if (checkRequired(adapter, isBodyRequired)) { flux = flux.switchIfEmpty(Flux.error(getRequiredBodyError(bodyParameter))); } @@ -151,8 +151,11 @@ public abstract class AbstractMessageReaderArgumentResolver { return Mono.just(adapter.fromPublisher(flux)); } else { - Mono mono = reader.readMono(elementType, request, hints) - .otherwise(ex -> Mono.error(getReadError(ex, bodyParameter))); + Mono mono = (reader instanceof ServerHttpMessageReader ? + ((ServerHttpMessageReader)reader).readMono(bodyType, elementType, + request, response, Collections.emptyMap()) : + reader.readMono(elementType, request, Collections.emptyMap()) + .otherwise(ex -> Mono.error(getReadError(ex, bodyParameter)))); if (checkRequired(adapter, isBodyRequired)) { mono = mono.otherwiseIfEmpty(Mono.error(getRequiredBodyError(bodyParameter))); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageWriterResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageWriterResultHandler.java index f0e04a226ea..713481ec09d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageWriterResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageWriterResultHandler.java @@ -17,7 +17,6 @@ package org.springframework.web.reactive.result.method.annotation; import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.stream.Collectors; import org.reactivestreams.Publisher; @@ -30,6 +29,7 @@ import org.springframework.core.ResolvableType; import org.springframework.http.MediaType; import org.springframework.http.codec.HttpMessageWriter; import org.springframework.http.codec.ServerHttpMessageWriter; +import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.util.Assert; import org.springframework.web.reactive.accept.RequestedContentTypeResolver; @@ -119,18 +119,17 @@ public abstract class AbstractMessageWriterResultHandler extends ContentNegotiat "No converter for return value type: " + elementType)); } + ServerHttpRequest request = exchange.getRequest(); + ServerHttpResponse response = exchange.getResponse(); MediaType bestMediaType = selectMediaType(exchange, producibleTypes); - if (bestMediaType != null) { for (HttpMessageWriter messageWriter : getMessageWriters()) { - Map hints = (messageWriter instanceof ServerHttpMessageWriter ? - ((ServerHttpMessageWriter)messageWriter).resolveWriteHints(bodyType, elementType, - bestMediaType, exchange.getRequest()) : Collections.emptyMap()); - if (messageWriter.canWrite(elementType, bestMediaType, hints)) { - - ServerHttpResponse response = exchange.getResponse(); - return messageWriter.write((Publisher) publisher, elementType, - bestMediaType, response, hints); + if (messageWriter.canWrite(elementType, bestMediaType)) { + return (messageWriter instanceof ServerHttpMessageWriter ? + ((ServerHttpMessageWriter)messageWriter).write((Publisher) publisher, + bodyType, elementType, bestMediaType, request, response, Collections.emptyMap()) : + messageWriter.write((Publisher) publisher, elementType, + bestMediaType, response, Collections.emptyMap())); } } } @@ -140,7 +139,7 @@ public abstract class AbstractMessageWriterResultHandler extends ContentNegotiat private List getProducibleMediaTypes(ResolvableType elementType) { return getMessageWriters().stream() - .filter(converter -> converter.canWrite(elementType, null, Collections.emptyMap())) + .filter(converter -> converter.canWrite(elementType, null)) .flatMap(converter -> converter.getWritableMediaTypes().stream()) .collect(Collectors.toList()); } diff --git a/spring-web/src/main/java/org/springframework/http/codec/AbstractServerHttpMessageReader.java b/spring-web/src/main/java/org/springframework/http/codec/AbstractServerHttpMessageReader.java index 90dff62480d..38883331dc5 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/AbstractServerHttpMessageReader.java +++ b/spring-web/src/main/java/org/springframework/http/codec/AbstractServerHttpMessageReader.java @@ -28,6 +28,7 @@ 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 @@ -46,8 +47,8 @@ public abstract class AbstractServerHttpMessageReader implements ServerHttpMe } @Override - public boolean canRead(ResolvableType elementType, MediaType mediaType, Map hints) { - return this.reader.canRead(elementType, mediaType, hints); + public boolean canRead(ResolvableType elementType, MediaType mediaType) { + return this.reader.canRead(elementType, mediaType); } @Override @@ -66,29 +67,42 @@ public abstract class AbstractServerHttpMessageReader implements ServerHttpMe } @Override - public final Map resolveReadHints(ResolvableType streamType, - ResolvableType elementType, MediaType mediaType, ServerHttpRequest request) { + public Flux read(ResolvableType streamType, ResolvableType elementType, + ServerHttpRequest request, ServerHttpResponse response, Map hints) { - Map hints = new HashMap<>(); - if (this.reader instanceof ServerHttpMessageReader) { - hints.putAll(((ServerHttpMessageReader)this.reader).resolveReadHints(streamType, elementType, mediaType, request)); - } - hints.putAll(resolveReadHintsInternal(streamType, elementType, mediaType, request)); - return hints; + Map mergedHints = new HashMap<>(hints); + mergedHints.putAll(beforeRead(streamType, elementType, request, response)); + + return (this.reader instanceof ServerHttpMessageReader ? + ((ServerHttpMessageReader)this.reader).read(streamType, elementType, request, response, mergedHints) : + this.read(elementType, request, mergedHints)); + } + + @Override + public Mono readMono(ResolvableType streamType, ResolvableType elementType, + ServerHttpRequest request, ServerHttpResponse response, Map hints) { + + Map mergedHints = new HashMap<>(hints); + mergedHints.putAll(beforeRead(streamType, elementType, request, response)); + + return (this.reader instanceof ServerHttpMessageReader ? + ((ServerHttpMessageReader)this.reader).readMono(streamType, elementType, request, response, mergedHints) : + this.readMono(elementType, request, mergedHints)); } /** - * Abstract method that returns hints which can be used to customize how the body should be read. - * Invoked from {@link #resolveReadHints}. - * @param streamType the original type used in the method parameter. For annotation + * Invoked before reading the request 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 return - * @param mediaType the media type to read, can be {@code null} if not specified. - * Typically the value of a {@code Content-Type} header. - * @param request the current HTTP request - * @return Additional information about how to read the body + * Can be {@code null}. + * @param elementType the stream element type to process + * @param request the current HTTP request, can be {@code null} + * @param response the current HTTP response, can be {@code null} + * @return Additional information about how to write the body */ - protected abstract Map resolveReadHintsInternal(ResolvableType streamType, - ResolvableType elementType, MediaType mediaType, ServerHttpRequest request); + protected abstract Map beforeRead(ResolvableType streamType, + ResolvableType elementType, ServerHttpRequest request, ServerHttpResponse response); } diff --git a/spring-web/src/main/java/org/springframework/http/codec/AbstractServerHttpMessageWriter.java b/spring-web/src/main/java/org/springframework/http/codec/AbstractServerHttpMessageWriter.java index 9fb5d20bd7a..c76ca3a6bec 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/AbstractServerHttpMessageWriter.java +++ b/spring-web/src/main/java/org/springframework/http/codec/AbstractServerHttpMessageWriter.java @@ -28,10 +28,13 @@ 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 allow providing hints to the nested {@code writer} or setting the response status, for + * example, by implementing {@link #beforeWrite(ResolvableType, ResolvableType, MediaType, ServerHttpRequest, ServerHttpResponse)} + * * * @author Sebastien Deleuze * @since 5.0 @@ -46,15 +49,8 @@ public abstract class AbstractServerHttpMessageWriter implements ServerHttpMe } @Override - public boolean canWrite(ResolvableType elementType, MediaType mediaType, Map hints) { - return this.writer.canWrite(elementType, mediaType, hints); - } - - @Override - public Mono write(Publisher inputStream, ResolvableType elementType, - MediaType mediaType, ReactiveHttpOutputMessage outputMessage, Map hints) { - - return this.writer.write(inputStream, elementType, mediaType, outputMessage, hints); + public boolean canWrite(ResolvableType elementType, MediaType mediaType) { + return this.writer.canWrite(elementType, mediaType); } @Override @@ -63,29 +59,37 @@ public abstract class AbstractServerHttpMessageWriter implements ServerHttpMe } @Override - public final Map resolveWriteHints(ResolvableType streamType, - ResolvableType elementType, MediaType mediaType, ServerHttpRequest request) { + public Mono write(Publisher inputStream, ResolvableType elementType, + MediaType mediaType, ReactiveHttpOutputMessage outputMessage, Map hints) { + return this.writer.write(inputStream, elementType, mediaType, outputMessage, hints); + } - Map hints = new HashMap<>(); - if (this.writer instanceof ServerHttpMessageWriter) { - hints.putAll(((ServerHttpMessageWriter)this.writer).resolveWriteHints(streamType, elementType, mediaType, request)); - } - hints.putAll(resolveWriteHintsInternal(streamType, elementType, mediaType, request)); - return hints; + @Override + public Mono write(Publisher inputStream, ResolvableType streamType, ResolvableType elementType, + MediaType mediaType, ServerHttpRequest request, ServerHttpResponse response, Map hints) { + + Map mergedHints = new HashMap<>(hints); + mergedHints.putAll(beforeWrite(streamType, elementType, mediaType, request, response)); + return (this.writer instanceof ServerHttpMessageWriter ? + ((ServerHttpMessageWriter)this.writer).write(inputStream, streamType, elementType, mediaType, request, response, mergedHints) : + this.writer.write(inputStream, elementType, mediaType, response, mergedHints)); } /** - * Abstract method that returns hints which can be used to customize how the body should be written. - * Invoked from {@link #resolveWriteHints}. + * Invoked before writing the response 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()}. + * Can be {@code null}. * @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 + * @param request the current HTTP request, can be {@code null} + * @param response the current HTTP response, can be {@code null} * @return Additional information about how to write the body */ - protected abstract Map resolveWriteHintsInternal(ResolvableType streamType, - ResolvableType elementType, MediaType mediaType, ServerHttpRequest request); + protected abstract Map beforeWrite(ResolvableType streamType, ResolvableType elementType, + MediaType mediaType, ServerHttpRequest request, ServerHttpResponse response); } diff --git a/spring-web/src/main/java/org/springframework/http/codec/Jackson2ServerHttpMessageReader.java b/spring-web/src/main/java/org/springframework/http/codec/Jackson2ServerHttpMessageReader.java index 0e3b39fac3f..972a2167dfb 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/Jackson2ServerHttpMessageReader.java +++ b/spring-web/src/main/java/org/springframework/http/codec/Jackson2ServerHttpMessageReader.java @@ -23,9 +23,9 @@ import com.fasterxml.jackson.annotation.JsonView; import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; -import org.springframework.http.MediaType; 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: @@ -44,8 +44,8 @@ public class Jackson2ServerHttpMessageReader extends AbstractServerHttpMessageRe } @Override - protected Map resolveReadHintsInternal(ResolvableType streamType, - ResolvableType elementType, MediaType mediaType, ServerHttpRequest request) { + protected Map beforeRead(ResolvableType streamType, + ResolvableType elementType, ServerHttpRequest request, ServerHttpResponse response) { Object source = streamType.getSource(); MethodParameter parameter = (source instanceof MethodParameter ? (MethodParameter)source : null); diff --git a/spring-web/src/main/java/org/springframework/http/codec/Jackson2ServerHttpMessageWriter.java b/spring-web/src/main/java/org/springframework/http/codec/Jackson2ServerHttpMessageWriter.java index 2caead5bc45..be9c7e92265 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/Jackson2ServerHttpMessageWriter.java +++ b/spring-web/src/main/java/org/springframework/http/codec/Jackson2ServerHttpMessageWriter.java @@ -16,16 +16,20 @@ package org.springframework.http.codec; -import java.util.Collections; + +import java.util.HashMap; import java.util.Map; import com.fasterxml.jackson.annotation.JsonView; +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.codec.json.AbstractJackson2Codec; import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; /** * {@link ServerHttpMessageWriter} that resolves those annotation or request based Jackson 2 hints: @@ -44,9 +48,10 @@ public class Jackson2ServerHttpMessageWriter extends AbstractServerHttpMessageWr } @Override - protected Map resolveWriteHintsInternal(ResolvableType streamType, - ResolvableType elementType, MediaType mediaType, ServerHttpRequest request) { + protected Map beforeWrite(ResolvableType streamType, ResolvableType elementType, + MediaType mediaType, ServerHttpRequest request, ServerHttpResponse response) { + Map hints = new HashMap<>(); Object source = streamType.getSource(); MethodParameter returnValue = (source instanceof MethodParameter ? (MethodParameter)source : null); if (returnValue != null) { @@ -57,10 +62,10 @@ public class Jackson2ServerHttpMessageWriter extends AbstractServerHttpMessageWr throw new IllegalArgumentException( "@JsonView only supported for write hints with exactly 1 class argument: " + returnValue); } - return Collections.singletonMap(AbstractJackson2Codec.JSON_VIEW_HINT, classes[0]); + hints.put(AbstractJackson2Codec.JSON_VIEW_HINT, classes[0]); } } - return Collections.emptyMap(); + return hints; } } diff --git a/spring-web/src/main/java/org/springframework/http/codec/ServerHttpMessageReader.java b/spring-web/src/main/java/org/springframework/http/codec/ServerHttpMessageReader.java index 6f085cf1c03..5574f79f2d1 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/ServerHttpMessageReader.java +++ b/spring-web/src/main/java/org/springframework/http/codec/ServerHttpMessageReader.java @@ -18,14 +18,17 @@ package org.springframework.http.codec; 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.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; /** - * Server and annotation based controller specific {@link HttpMessageReader} that allows to - * resolve hints using annotations or request based information. + * Server oriented {@link HttpMessageReader} that allows to resolve hints using annotations or + * perform additional operation using {@link ServerHttpRequest} or {@link ServerHttpResponse}. * * @author Sebastien Deleuze * @since 5.0 @@ -33,16 +36,37 @@ import org.springframework.http.server.reactive.ServerHttpRequest; public interface ServerHttpMessageReader extends HttpMessageReader { /** + * Read a {@link Flux} of the given type form the given input message with additional server related + * parameters which could be used to create some hints or set the response status for example. + * * Return hints that can be used to customize how the body should be read * @param streamType the original type used in the method parameter. For annotation * based controllers, the {@link MethodParameter} is available via {@link ResolvableType#getSource()}. * @param elementType the stream element type to return - * @param mediaType the media type to read, can be {@code null} if not specified. * Typically the value of a {@code Content-Type} header. * @param request the current HTTP request - * @return Additional information about how to read the body + * @param response the current HTTP response + * @param hints additional information about how to read the body + * @return the converted {@link Flux} of elements */ - Map resolveReadHints(ResolvableType streamType, ResolvableType elementType, - MediaType mediaType, ServerHttpRequest request); + Flux read(ResolvableType streamType, ResolvableType elementType, ServerHttpRequest request, + ServerHttpResponse response, Map hints); + + /** + * Read a {@link Mono} of the given type form the given input message with additional server related + * parameters which could be used to create some hints or set the response status for example. + * + * Return hints that can be used to customize how the body should be read + * @param streamType the original type used in the method parameter. For annotation + * based controllers, the {@link MethodParameter} is available via {@link ResolvableType#getSource()}. + * @param elementType the stream element type to return + * Typically the value of a {@code Content-Type} header. + * @param request the current HTTP request + * @param response the current HTTP response + * @param hints additional information about how to read the body + * @return the converted {@link Mono} of object + */ + Mono readMono(ResolvableType streamType, ResolvableType elementType, ServerHttpRequest request, + ServerHttpResponse response, Map hints); } diff --git a/spring-web/src/main/java/org/springframework/http/codec/ServerHttpMessageWriter.java b/spring-web/src/main/java/org/springframework/http/codec/ServerHttpMessageWriter.java index f72a5787115..1c6371bb31b 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/ServerHttpMessageWriter.java +++ b/spring-web/src/main/java/org/springframework/http/codec/ServerHttpMessageWriter.java @@ -18,14 +18,18 @@ package org.springframework.http.codec; 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.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; /** * Server oriented {@link HttpMessageWriter} that allows to resolve hints using annotations or - * request based information. + * perform additional operation using {@link ServerHttpRequest} or {@link ServerHttpResponse}. * * @author Sebastien Deleuze * @since 5.0 @@ -33,16 +37,21 @@ import org.springframework.http.server.reactive.ServerHttpRequest; public interface ServerHttpMessageWriter extends HttpMessageWriter { /** - * Return hints that can be used to customize how the body should be written + * Write a given object to the given output message with additional server related + * parameters which could be used to create some hints or set the response status for example. + * * @param streamType the original type used for the method return value. For annotation * based controllers, the {@link MethodParameter} is available via {@link ResolvableType#getSource()}. + * Can be {@code null}. * @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 + * @param request the current HTTP request, can be {@code null} + * @param response the current HTTP response, can be {@code null} + * @return a {@link Mono} that indicates completion or error */ - Map resolveWriteHints(ResolvableType streamType, ResolvableType elementType, - MediaType mediaType, ServerHttpRequest request); + Mono write(Publisher inputStream, ResolvableType streamType, + ResolvableType elementType, MediaType mediaType, ServerHttpRequest request, + ServerHttpResponse response, Map hints); }