提交 fbf88d19 编写于 作者: S Sebastien Deleuze

Allow to specify hints with the functional web API

The most common use case is specifying JSON views.

ServerResponse.BodyBuilder#hint(String, Object) allows to
specify response body serialization hints.

ServerRequest#body(BodyExtractor, Map) allows to specify
request body extraction hints.

Issue: SPR-15030
上级 f51fe5fd
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
package org.springframework.web.reactive.function; package org.springframework.web.reactive.function;
import java.util.Map;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.stream.Stream; import java.util.stream.Stream;
...@@ -52,6 +53,11 @@ public interface BodyExtractor<T, M extends ReactiveHttpInputMessage> { ...@@ -52,6 +53,11 @@ public interface BodyExtractor<T, M extends ReactiveHttpInputMessage> {
* @return the stream of message readers * @return the stream of message readers
*/ */
Supplier<Stream<HttpMessageReader<?>>> messageReaders(); Supplier<Stream<HttpMessageReader<?>>> messageReaders();
/**
* Return the map of hints to use to customize body extraction.
*/
Map<String, Object> hints();
} }
} }
...@@ -65,7 +65,7 @@ public abstract class BodyExtractors { ...@@ -65,7 +65,7 @@ public abstract class BodyExtractors {
Assert.notNull(elementType, "'elementType' must not be null"); Assert.notNull(elementType, "'elementType' must not be null");
return (request, context) -> readWithMessageReaders(request, context, return (request, context) -> readWithMessageReaders(request, context,
elementType, elementType,
reader -> reader.readMono(elementType, request, Collections.emptyMap()), reader -> reader.readMono(elementType, request, context.hints()),
Mono::error); Mono::error);
} }
...@@ -90,7 +90,7 @@ public abstract class BodyExtractors { ...@@ -90,7 +90,7 @@ public abstract class BodyExtractors {
Assert.notNull(elementType, "'elementType' must not be null"); Assert.notNull(elementType, "'elementType' must not be null");
return (inputMessage, context) -> readWithMessageReaders(inputMessage, context, return (inputMessage, context) -> readWithMessageReaders(inputMessage, context,
elementType, elementType,
reader -> reader.read(elementType, inputMessage, Collections.emptyMap()), reader -> reader.read(elementType, inputMessage, context.hints()),
Flux::error); Flux::error);
} }
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
package org.springframework.web.reactive.function; package org.springframework.web.reactive.function;
import java.util.Map;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.stream.Stream; import java.util.stream.Stream;
...@@ -54,6 +55,11 @@ public interface BodyInserter<T, M extends ReactiveHttpOutputMessage> { ...@@ -54,6 +55,11 @@ public interface BodyInserter<T, M extends ReactiveHttpOutputMessage> {
*/ */
Supplier<Stream<HttpMessageWriter<?>>> messageWriters(); Supplier<Stream<HttpMessageWriter<?>>> messageWriters();
/**
* Return the map of hints to use for response body conversion.
*/
Map<String, Object> hints();
} }
......
...@@ -16,7 +16,6 @@ ...@@ -16,7 +16,6 @@
package org.springframework.web.reactive.function; package org.springframework.web.reactive.function;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.stream.Collectors; import java.util.stream.Collectors;
...@@ -119,7 +118,7 @@ public abstract class BodyInserters { ...@@ -119,7 +118,7 @@ public abstract class BodyInserters {
return (response, context) -> { return (response, context) -> {
HttpMessageWriter<Resource> messageWriter = resourceHttpMessageWriter(context); HttpMessageWriter<Resource> messageWriter = resourceHttpMessageWriter(context);
return messageWriter.write(Mono.just(resource), RESOURCE_TYPE, null, return messageWriter.write(Mono.just(resource), RESOURCE_TYPE, null,
response, Collections.emptyMap()); response, context.hints());
}; };
} }
...@@ -146,7 +145,7 @@ public abstract class BodyInserters { ...@@ -146,7 +145,7 @@ public abstract class BodyInserters {
return (response, context) -> { return (response, context) -> {
HttpMessageWriter<ServerSentEvent<T>> messageWriter = sseMessageWriter(context); HttpMessageWriter<ServerSentEvent<T>> messageWriter = sseMessageWriter(context);
return messageWriter.write(eventsPublisher, SERVER_SIDE_EVENT_TYPE, return messageWriter.write(eventsPublisher, SERVER_SIDE_EVENT_TYPE,
MediaType.TEXT_EVENT_STREAM, response, Collections.emptyMap()); MediaType.TEXT_EVENT_STREAM, response, context.hints());
}; };
} }
...@@ -186,7 +185,7 @@ public abstract class BodyInserters { ...@@ -186,7 +185,7 @@ public abstract class BodyInserters {
return (outputMessage, context) -> { return (outputMessage, context) -> {
HttpMessageWriter<T> messageWriter = sseMessageWriter(context); HttpMessageWriter<T> messageWriter = sseMessageWriter(context);
return messageWriter.write(eventsPublisher, eventType, return messageWriter.write(eventsPublisher, eventType,
MediaType.TEXT_EVENT_STREAM, outputMessage, Collections.emptyMap()); MediaType.TEXT_EVENT_STREAM, outputMessage, context.hints());
}; };
} }
...@@ -227,8 +226,7 @@ public abstract class BodyInserters { ...@@ -227,8 +226,7 @@ public abstract class BodyInserters {
.findFirst() .findFirst()
.map(BodyInserters::cast) .map(BodyInserters::cast)
.map(messageWriter -> messageWriter .map(messageWriter -> messageWriter
.write(body, bodyType, contentType, m, Collections .write(body, bodyType, contentType, m, context.hints()))
.emptyMap()))
.orElseGet(() -> { .orElseGet(() -> {
List<MediaType> supportedMediaTypes = messageWriters.get() List<MediaType> supportedMediaTypes = messageWriters.get()
.flatMap(reader -> reader.getWritableMediaTypes().stream()) .flatMap(reader -> reader.getWritableMediaTypes().stream())
......
...@@ -22,6 +22,8 @@ import java.time.ZoneId; ...@@ -22,6 +22,8 @@ import java.time.ZoneId;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.stream.Stream; import java.util.stream.Stream;
...@@ -220,6 +222,11 @@ class DefaultClientRequestBuilder implements ClientRequest.BodyBuilder { ...@@ -220,6 +222,11 @@ class DefaultClientRequestBuilder implements ClientRequest.BodyBuilder {
public Supplier<Stream<HttpMessageWriter<?>>> messageWriters() { public Supplier<Stream<HttpMessageWriter<?>>> messageWriters() {
return strategies.messageWriters(); return strategies.messageWriters();
} }
@Override
public Map<String, Object> hints() {
return Collections.emptyMap();
}
}); });
} }
} }
......
...@@ -18,6 +18,7 @@ package org.springframework.web.reactive.function.client; ...@@ -18,6 +18,7 @@ package org.springframework.web.reactive.function.client;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.OptionalLong; import java.util.OptionalLong;
import java.util.function.Function; import java.util.function.Function;
...@@ -74,6 +75,10 @@ class DefaultClientResponse implements ClientResponse { ...@@ -74,6 +75,10 @@ class DefaultClientResponse implements ClientResponse {
public Supplier<Stream<HttpMessageReader<?>>> messageReaders() { public Supplier<Stream<HttpMessageReader<?>>> messageReaders() {
return strategies.messageReaders(); return strategies.messageReaders();
} }
@Override
public Map<String, Object> hints() {
return Collections.emptyMap();
}
}); });
} }
......
...@@ -78,6 +78,11 @@ class DefaultServerRequest implements ServerRequest { ...@@ -78,6 +78,11 @@ class DefaultServerRequest implements ServerRequest {
@Override @Override
public <T> T body(BodyExtractor<T, ? super ServerHttpRequest> extractor) { public <T> T body(BodyExtractor<T, ? super ServerHttpRequest> extractor) {
return body(extractor, Collections.emptyMap());
}
@Override
public <T> T body(BodyExtractor<T, ? super ServerHttpRequest> extractor, Map<String, Object> hints) {
Assert.notNull(extractor, "'extractor' must not be null"); Assert.notNull(extractor, "'extractor' must not be null");
return extractor.extract(request(), return extractor.extract(request(),
new BodyExtractor.Context() { new BodyExtractor.Context() {
...@@ -85,6 +90,10 @@ class DefaultServerRequest implements ServerRequest { ...@@ -85,6 +90,10 @@ class DefaultServerRequest implements ServerRequest {
public Supplier<Stream<HttpMessageReader<?>>> messageReaders() { public Supplier<Stream<HttpMessageReader<?>>> messageReaders() {
return DefaultServerRequest.this.strategies.messageReaders(); return DefaultServerRequest.this.strategies.messageReaders();
} }
@Override
public Map<String, Object> hints() {
return hints;
}
}); });
} }
......
...@@ -22,6 +22,7 @@ import java.time.ZonedDateTime; ...@@ -22,6 +22,7 @@ import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.Locale; import java.util.Locale;
...@@ -62,6 +63,8 @@ class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder { ...@@ -62,6 +63,8 @@ class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder {
private final HttpHeaders headers = new HttpHeaders(); private final HttpHeaders headers = new HttpHeaders();
private final Map<String, Object> hints = new HashMap<>();
public DefaultServerResponseBuilder(HttpStatus statusCode) { public DefaultServerResponseBuilder(HttpStatus statusCode) {
this.statusCode = statusCode; this.statusCode = statusCode;
...@@ -122,6 +125,12 @@ class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder { ...@@ -122,6 +125,12 @@ class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder {
return this; return this;
} }
@Override
public ServerResponse.BodyBuilder hint(String key, Object value) {
this.hints.put(key, value);
return this;
}
@Override @Override
public ServerResponse.BodyBuilder lastModified(ZonedDateTime lastModified) { public ServerResponse.BodyBuilder lastModified(ZonedDateTime lastModified) {
ZonedDateTime gmt = lastModified.withZoneSameInstant(ZoneId.of("GMT")); ZonedDateTime gmt = lastModified.withZoneSameInstant(ZoneId.of("GMT"));
...@@ -182,7 +191,7 @@ class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder { ...@@ -182,7 +191,7 @@ class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder {
public <T> Mono<ServerResponse> body(BodyInserter<T, ? super ServerHttpResponse> inserter) { public <T> Mono<ServerResponse> body(BodyInserter<T, ? super ServerHttpResponse> inserter) {
Assert.notNull(inserter, "'inserter' must not be null"); Assert.notNull(inserter, "'inserter' must not be null");
return Mono return Mono
.just(new BodyInserterServerResponse<T>(this.statusCode, this.headers, inserter)); .just(new BodyInserterServerResponse<T>(this.statusCode, this.headers, inserter, this.hints));
} }
@Override @Override
...@@ -276,11 +285,14 @@ class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder { ...@@ -276,11 +285,14 @@ class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder {
private final BodyInserter<T, ? super ServerHttpResponse> inserter; private final BodyInserter<T, ? super ServerHttpResponse> inserter;
private final Map<String, Object> hints;
public BodyInserterServerResponse(HttpStatus statusCode, HttpHeaders headers, public BodyInserterServerResponse(HttpStatus statusCode, HttpHeaders headers,
BodyInserter<T, ? super ServerHttpResponse> inserter) { BodyInserter<T, ? super ServerHttpResponse> inserter, Map<String, Object> hints) {
super(statusCode, headers); super(statusCode, headers);
this.inserter = inserter; this.inserter = inserter;
this.hints = hints;
} }
@Override @Override
...@@ -292,6 +304,10 @@ class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder { ...@@ -292,6 +304,10 @@ class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder {
public Supplier<Stream<HttpMessageWriter<?>>> messageWriters() { public Supplier<Stream<HttpMessageWriter<?>>> messageWriters() {
return strategies.messageWriters(); return strategies.messageWriters();
} }
@Override
public Map<String, Object> hints() {
return hints;
}
}); });
} }
} }
......
...@@ -323,6 +323,11 @@ public abstract class RequestPredicates { ...@@ -323,6 +323,11 @@ public abstract class RequestPredicates {
return this.request.body(extractor); return this.request.body(extractor);
} }
@Override
public <T> T body(BodyExtractor<T, ? super ServerHttpRequest> extractor, Map<String, Object> hints) {
return this.request.body(extractor, hints);
}
@Override @Override
public <T> Mono<T> bodyToMono(Class<? extends T> elementClass) { public <T> Mono<T> bodyToMono(Class<? extends T> elementClass) {
return this.request.bodyToMono(elementClass); return this.request.bodyToMono(elementClass);
......
...@@ -31,6 +31,7 @@ import org.springframework.http.HttpHeaders; ...@@ -31,6 +31,7 @@ import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
import org.springframework.http.HttpRange; import org.springframework.http.HttpRange;
import org.springframework.http.MediaType; 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.ServerHttpRequest;
import org.springframework.web.reactive.function.BodyExtractor; import org.springframework.web.reactive.function.BodyExtractor;
...@@ -40,6 +41,7 @@ import org.springframework.web.reactive.function.BodyExtractor; ...@@ -40,6 +41,7 @@ import org.springframework.web.reactive.function.BodyExtractor;
* {@link #body(BodyExtractor)} respectively. * {@link #body(BodyExtractor)} respectively.
* *
* @author Arjen Poutsma * @author Arjen Poutsma
* @author Sebastien Deleuze
* @since 5.0 * @since 5.0
*/ */
public interface ServerRequest { public interface ServerRequest {
...@@ -71,9 +73,20 @@ public interface ServerRequest { ...@@ -71,9 +73,20 @@ public interface ServerRequest {
* @param extractor the {@code BodyExtractor} that reads from the request * @param extractor the {@code BodyExtractor} that reads from the request
* @param <T> the type of the body returned * @param <T> the type of the body returned
* @return the extracted body * @return the extracted body
* @see #body(BodyExtractor, Map)
*/ */
<T> T body(BodyExtractor<T, ? super ServerHttpRequest> extractor); <T> T body(BodyExtractor<T, ? super ServerHttpRequest> extractor);
/**
* Extract the body with the given {@code BodyExtractor} and hints.
* @param extractor the {@code BodyExtractor} that reads from the request
* @param hints the map of hints like {@link AbstractJackson2Codec#JSON_VIEW_HINT}
* to use to customize body extraction
* @param <T> the type of the body returned
* @return the extracted body
*/
<T> T body(BodyExtractor<T, ? super ServerHttpRequest> extractor, Map<String, Object> hints);
/** /**
* Extract the body to a {@code Mono}. * Extract the body to a {@code Mono}.
* @param elementClass the class of element in the {@code Mono} * @param elementClass the class of element in the {@code Mono}
......
...@@ -31,6 +31,7 @@ import org.springframework.http.HttpHeaders; ...@@ -31,6 +31,7 @@ import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.codec.json.AbstractJackson2Codec;
import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.web.reactive.function.BodyInserter; import org.springframework.web.reactive.function.BodyInserter;
...@@ -42,6 +43,7 @@ import org.springframework.web.server.ServerWebExchange; ...@@ -42,6 +43,7 @@ import org.springframework.web.server.ServerWebExchange;
* {@linkplain HandlerFunction handler function} or {@linkplain HandlerFilterFunction filter function}. * {@linkplain HandlerFunction handler function} or {@linkplain HandlerFilterFunction filter function}.
* *
* @author Arjen Poutsma * @author Arjen Poutsma
* @author Sebastien Deleuze
* @since 5.0 * @since 5.0
*/ */
public interface ServerResponse { public interface ServerResponse {
...@@ -312,6 +314,12 @@ public interface ServerResponse { ...@@ -312,6 +314,12 @@ public interface ServerResponse {
*/ */
BodyBuilder contentType(MediaType contentType); BodyBuilder contentType(MediaType contentType);
/**
* Add a serialization hint like {@link AbstractJackson2Codec#JSON_VIEW_HINT} to
* customize how the body will be serialized.
*/
BodyBuilder hint(String key, Object value);
/** /**
* Set the body of the response to the given {@code Publisher} and return it. This * Set the body of the response to the given {@code Publisher} and return it. This
* convenience method combines {@link #body(BodyInserter)} and * convenience method combines {@link #body(BodyInserter)} and
......
...@@ -91,6 +91,11 @@ public class ServerRequestWrapper implements ServerRequest { ...@@ -91,6 +91,11 @@ public class ServerRequestWrapper implements ServerRequest {
return this.request.body(extractor); return this.request.body(extractor);
} }
@Override
public <T> T body(BodyExtractor<T, ? super ServerHttpRequest> extractor, Map<String, Object> hints) {
return this.request.body(extractor, hints);
}
@Override @Override
public <T> Mono<T> bodyToMono(Class<? extends T> elementClass) { public <T> Mono<T> bodyToMono(Class<? extends T> elementClass) {
return this.request.bodyToMono(elementClass); return this.request.bodyToMono(elementClass);
......
...@@ -19,10 +19,14 @@ package org.springframework.web.reactive.function; ...@@ -19,10 +19,14 @@ package org.springframework.web.reactive.function;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.stream.Stream; import java.util.stream.Stream;
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;
...@@ -43,13 +47,20 @@ import org.springframework.http.codec.json.Jackson2JsonDecoder; ...@@ -43,13 +47,20 @@ import org.springframework.http.codec.json.Jackson2JsonDecoder;
import org.springframework.http.codec.xml.Jaxb2XmlDecoder; import org.springframework.http.codec.xml.Jaxb2XmlDecoder;
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest; import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
import static org.springframework.http.codec.json.AbstractJackson2Codec.JSON_VIEW_HINT;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
/** /**
* @author Arjen Poutsma * @author Arjen Poutsma
* @author Sebastien Deleuze
*/ */
public class BodyExtractorsTests { public class BodyExtractorsTests {
private BodyExtractor.Context context; private BodyExtractor.Context context;
private Map<String, Object> hints;
@Before @Before
public void createContext() { public void createContext() {
final List<HttpMessageReader<?>> messageReaders = new ArrayList<>(); final List<HttpMessageReader<?>> messageReaders = new ArrayList<>();
...@@ -63,8 +74,12 @@ public class BodyExtractorsTests { ...@@ -63,8 +74,12 @@ public class BodyExtractorsTests {
public Supplier<Stream<HttpMessageReader<?>>> messageReaders() { public Supplier<Stream<HttpMessageReader<?>>> messageReaders() {
return messageReaders::stream; return messageReaders::stream;
} }
@Override
public Map<String, Object> hints() {
return hints;
}
}; };
this.hints = new HashMap();
} }
@Test @Test
...@@ -87,6 +102,31 @@ public class BodyExtractorsTests { ...@@ -87,6 +102,31 @@ public class BodyExtractorsTests {
.verify(); .verify();
} }
@Test
public void toMonoWithHints() throws Exception {
BodyExtractor<Mono<User>, ReactiveHttpInputMessage> extractor = BodyExtractors.toMono(User.class);
this.hints.put(JSON_VIEW_HINT, SafeToDeserialize.class);
DefaultDataBufferFactory factory = new DefaultDataBufferFactory();
DefaultDataBuffer dataBuffer =
factory.wrap(ByteBuffer.wrap("{\"username\":\"foo\",\"password\":\"bar\"}".getBytes(StandardCharsets.UTF_8)));
Flux<DataBuffer> body = Flux.just(dataBuffer);
MockServerHttpRequest request = new MockServerHttpRequest();
request.getHeaders().setContentType(MediaType.APPLICATION_JSON);
request.setBody(body);
Mono<User> result = extractor.extract(request, this.context);
StepVerifier.create(result)
.consumeNextWith(user -> {
assertEquals("foo", user.getUsername());
assertNull(user.getPassword());
})
.expectComplete()
.verify();
}
@Test @Test
public void toFlux() throws Exception { public void toFlux() throws Exception {
BodyExtractor<Flux<String>, ReactiveHttpInputMessage> extractor = BodyExtractors.toFlux(String.class); BodyExtractor<Flux<String>, ReactiveHttpInputMessage> extractor = BodyExtractors.toFlux(String.class);
...@@ -107,6 +147,35 @@ public class BodyExtractorsTests { ...@@ -107,6 +147,35 @@ public class BodyExtractorsTests {
.verify(); .verify();
} }
@Test
public void toFluxWithHints() throws Exception {
BodyExtractor<Flux<User>, ReactiveHttpInputMessage> extractor = BodyExtractors.toFlux(User.class);
this.hints.put(JSON_VIEW_HINT, SafeToDeserialize.class);
DefaultDataBufferFactory factory = new DefaultDataBufferFactory();
DefaultDataBuffer dataBuffer =
factory.wrap(ByteBuffer.wrap("[{\"username\":\"foo\",\"password\":\"bar\"},{\"username\":\"bar\",\"password\":\"baz\"}]".getBytes(StandardCharsets.UTF_8)));
Flux<DataBuffer> body = Flux.just(dataBuffer);
MockServerHttpRequest request = new MockServerHttpRequest();
request.getHeaders().setContentType(MediaType.APPLICATION_JSON);
request.setBody(body);
Flux<User> result = extractor.extract(request, this.context);
StepVerifier.create(result)
.consumeNextWith(user -> {
assertEquals("foo", user.getUsername());
assertNull(user.getPassword());
})
.consumeNextWith(user -> {
assertEquals("bar", user.getUsername());
assertNull(user.getPassword());
})
.expectComplete()
.verify();
}
@Test @Test
public void toFluxUnacceptable() throws Exception { public void toFluxUnacceptable() throws Exception {
BodyExtractor<Flux<String>, ReactiveHttpInputMessage> extractor = BodyExtractors.toFlux(String.class); BodyExtractor<Flux<String>, ReactiveHttpInputMessage> extractor = BodyExtractors.toFlux(String.class);
...@@ -125,6 +194,10 @@ public class BodyExtractorsTests { ...@@ -125,6 +194,10 @@ public class BodyExtractorsTests {
public Supplier<Stream<HttpMessageReader<?>>> messageReaders() { public Supplier<Stream<HttpMessageReader<?>>> messageReaders() {
return Stream::empty; return Stream::empty;
} }
@Override
public Map<String, Object> hints() {
return Collections.emptyMap();
}
}; };
Flux<String> result = extractor.extract(request, emptyContext); Flux<String> result = extractor.extract(request, emptyContext);
...@@ -153,4 +226,39 @@ public class BodyExtractorsTests { ...@@ -153,4 +226,39 @@ public class BodyExtractorsTests {
.verify(); .verify();
} }
interface SafeToDeserialize {}
private static class User {
@JsonView(SafeToDeserialize.class)
private String username;
private String password;
public User() {
}
public User(String username, String password) {
this.username = username;
this.password = password;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
} }
\ No newline at end of file
...@@ -21,10 +21,13 @@ import java.nio.charset.StandardCharsets; ...@@ -21,10 +21,13 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.stream.Stream; import java.util.stream.Stream;
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;
...@@ -51,14 +54,18 @@ import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse ...@@ -51,14 +54,18 @@ import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse
import static java.nio.charset.StandardCharsets.UTF_8; import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertArrayEquals;
import static org.springframework.http.codec.json.AbstractJackson2Codec.JSON_VIEW_HINT;
/** /**
* @author Arjen Poutsma * @author Arjen Poutsma
* @author Sebastien Deleuze
*/ */
public class BodyInsertersTests { public class BodyInsertersTests {
private BodyInserter.Context context; private BodyInserter.Context context;
private Map<String, Object> hints;
@Before @Before
public void createContext() { public void createContext() {
final List<HttpMessageWriter<?>> messageWriters = new ArrayList<>(); final List<HttpMessageWriter<?>> messageWriters = new ArrayList<>();
...@@ -71,19 +78,21 @@ public class BodyInsertersTests { ...@@ -71,19 +78,21 @@ public class BodyInsertersTests {
messageWriters messageWriters
.add(new ServerSentEventHttpMessageWriter(Collections.singletonList(jsonEncoder))); .add(new ServerSentEventHttpMessageWriter(Collections.singletonList(jsonEncoder)));
this.context = new BodyInserter.Context() { this.context = new BodyInserter.Context() {
@Override @Override
public Supplier<Stream<HttpMessageWriter<?>>> messageWriters() { public Supplier<Stream<HttpMessageWriter<?>>> messageWriters() {
return messageWriters::stream; return messageWriters::stream;
} }
@Override
public Map<String, Object> hints() {
return hints;
}
}; };
this.hints = new HashMap();
} }
@Test @Test
public void ofObject() throws Exception { public void ofString() throws Exception {
String body = "foo"; String body = "foo";
BodyInserter<String, ReactiveHttpOutputMessage> inserter = BodyInserters.fromObject(body); BodyInserter<String, ReactiveHttpOutputMessage> inserter = BodyInserters.fromObject(body);
...@@ -99,6 +108,35 @@ public class BodyInsertersTests { ...@@ -99,6 +108,35 @@ public class BodyInsertersTests {
.verify(); .verify();
} }
@Test
public void ofObject() throws Exception {
User body = new User("foo", "bar");
BodyInserter<User, ReactiveHttpOutputMessage> inserter = BodyInserters.fromObject(body);
MockServerHttpResponse response = new MockServerHttpResponse();
Mono<Void> result = inserter.insert(response, this.context);
StepVerifier.create(result).expectComplete().verify();
StepVerifier.create(response.getBodyAsString())
.expectNext("{\"username\":\"foo\",\"password\":\"bar\"}")
.expectComplete()
.verify();
}
@Test
public void ofObjectWithHints() throws Exception {
User body = new User("foo", "bar");
BodyInserter<User, ReactiveHttpOutputMessage> inserter = BodyInserters.fromObject(body);
this.hints.put(JSON_VIEW_HINT, SafeToSerialize.class);
MockServerHttpResponse response = new MockServerHttpResponse();
Mono<Void> result = inserter.insert(response, this.context);
StepVerifier.create(result).expectComplete().verify();
StepVerifier.create(response.getBodyAsString())
.expectNext("{\"username\":\"foo\"}")
.expectComplete()
.verify();
}
@Test @Test
public void ofPublisher() throws Exception { public void ofPublisher() throws Exception {
Flux<String> body = Flux.just("foo"); Flux<String> body = Flux.just("foo");
...@@ -180,4 +218,39 @@ public class BodyInsertersTests { ...@@ -180,4 +218,39 @@ public class BodyInsertersTests {
} }
interface SafeToSerialize {}
private static class User {
@JsonView(SafeToSerialize.class)
private String username;
private String password;
public User() {
}
public User(String username, String password) {
this.username = username;
this.password = password;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
} }
\ No newline at end of file
...@@ -99,6 +99,11 @@ public class MockServerRequest implements ServerRequest { ...@@ -99,6 +99,11 @@ public class MockServerRequest implements ServerRequest {
return (S) this.body; return (S) this.body;
} }
@Override
public <S> S body(BodyExtractor<S, ? super ServerHttpRequest> extractor, Map<String, Object> hints) {
return (S) this.body;
}
@Override @Override
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public <S> Mono<S> bodyToMono(Class<? extends S> elementClass) { public <S> Mono<S> bodyToMono(Class<? extends S> elementClass) {
......
...@@ -50,7 +50,7 @@ import org.springframework.util.MimeTypeUtils; ...@@ -50,7 +50,7 @@ import org.springframework.util.MimeTypeUtils;
public class ServerSentEventHttpMessageWriter implements HttpMessageWriter<Object> { public class ServerSentEventHttpMessageWriter implements HttpMessageWriter<Object> {
/** /**
* Server-Sent Events hint expecting a {@link Boolean} value which when set to true * Server-Sent Events hint key expecting a {@link Boolean} value which when set to true
* will adapt the content in order to comply with Server-Sent Events recommendation. * will adapt the content in order to comply with Server-Sent Events recommendation.
* For example, it will append "data:" after each line break with data encoders * For example, it will append "data:" after each line break with data encoders
* supporting it. * supporting it.
......
...@@ -38,6 +38,11 @@ import org.springframework.util.MimeType; ...@@ -38,6 +38,11 @@ import org.springframework.util.MimeType;
*/ */
public class AbstractJackson2Codec { public class AbstractJackson2Codec {
/**
* Hint key to use with a {@link Class} value specifying the JSON View to use to serialize
* or deserialize an object.
* @see <a href="http://wiki.fasterxml.com/JacksonJsonViews">Jackson JSON Views</a>
*/
public static final String JSON_VIEW_HINT = AbstractJackson2Codec.class.getName() + ".jsonView"; public static final String JSON_VIEW_HINT = AbstractJackson2Codec.class.getName() + ".jsonView";
protected static final List<MimeType> JSON_MIME_TYPES = Arrays.asList( protected static final List<MimeType> JSON_MIME_TYPES = Arrays.asList(
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册