Support RouterFunction in @EnableWebFlux

This commit removes the RouterFunctions.toHandlerMapping method, in
favor of native support for RouterFunctions in @EnableWebFlux
configuration classes.

In order to accomplish this, the HandlerStrategies components has been
repurposed to only be used for the "bare-bones" HttpHandler, while the
(newly introduced) RouterFunctionMapping uses the strategies as exposed
through WebFluxConfigurationSupport.

Furthermore, this commit also introduces support for testing
RouterFunctions without resorting to an application context.

Issue: SPR-15536
This commit is contained in:
Arjen Poutsma
2017-05-11 14:57:01 +02:00
parent 3d290165fb
commit 01e3561db9
21 changed files with 647 additions and 247 deletions

View File

@@ -23,6 +23,8 @@ import java.util.EnumSet;
import java.util.List;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Supplier;
import java.util.stream.Stream;
import org.junit.Test;
import org.reactivestreams.Publisher;
@@ -40,10 +42,12 @@ import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.codec.EncoderHttpMessageWriter;
import org.springframework.http.codec.HttpMessageWriter;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
import org.springframework.mock.http.server.reactive.test.MockServerWebExchange;
import org.springframework.web.reactive.function.BodyInserter;
import org.springframework.web.reactive.result.view.ViewResolver;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.Assert.*;
@@ -190,6 +194,17 @@ public class DefaultEntityResponseBuilderTests {
MockServerWebExchange exchange = MockServerHttpRequest.get("http://localhost").toExchange();
ServerResponse.Context context = new ServerResponse.Context() {
@Override
public Supplier<Stream<HttpMessageWriter<?>>> messageWriters() {
return Collections.<HttpMessageWriter<?>>singletonList(new EncoderHttpMessageWriter<>(CharSequenceEncoder.allMimeTypes()))::stream;
}
@Override
public Supplier<Stream<ViewResolver>> viewResolvers() {
return Collections.<ViewResolver>emptyList()::stream;
}
};
HandlerStrategies strategies = HandlerStrategies.empty()
.customCodecs(configurer -> configurer.writer(new EncoderHttpMessageWriter<>(CharSequenceEncoder.allMimeTypes())))
.build();
@@ -200,7 +215,7 @@ public class DefaultEntityResponseBuilderTests {
.expectNext(body)
.expectComplete()
.verify();
response.writeTo(exchange, strategies);
response.writeTo(exchange, context);
})
.expectComplete()
.verify();

View File

@@ -270,9 +270,9 @@ public class DefaultServerResponseBuilderTests {
ServerWebExchange exchange = mock(ServerWebExchange.class);
MockServerHttpResponse response = new MockServerHttpResponse();
when(exchange.getResponse()).thenReturn(response);
HandlerStrategies strategies = mock(HandlerStrategies.class);
ServerResponse.Context context = mock(ServerResponse.Context.class);
result.flatMap(res -> res.writeTo(exchange, strategies)).block();
result.flatMap(res -> res.writeTo(exchange, context)).block();
assertEquals(HttpStatus.CREATED, response.getStatusCode());
assertEquals("MyValue", response.getHeaders().getFirst("MyKey"));
@@ -287,9 +287,9 @@ public class DefaultServerResponseBuilderTests {
ServerWebExchange exchange = mock(ServerWebExchange.class);
MockServerHttpResponse response = new MockServerHttpResponse();
when(exchange.getResponse()).thenReturn(response);
HandlerStrategies strategies = mock(HandlerStrategies.class);
ServerResponse.Context context = mock(ServerResponse.Context.class);
result.flatMap(res -> res.writeTo(exchange, strategies)).block();
result.flatMap(res -> res.writeTo(exchange, context)).block();
StepVerifier.create(response.getBody()).expectComplete().verify();
}

View File

@@ -36,16 +36,10 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.reactive.DispatcherHandler;
import org.springframework.web.reactive.HandlerAdapter;
import org.springframework.web.reactive.HandlerMapping;
import org.springframework.web.reactive.config.EnableWebFlux;
import org.springframework.web.reactive.config.WebFluxConfigurationSupport;
import org.springframework.web.reactive.function.server.support.HandlerFunctionAdapter;
import org.springframework.web.reactive.function.server.support.ServerResponseResultHandler;
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
import static org.junit.Assert.*;
import static org.springframework.web.reactive.function.BodyInserters.fromObject;
import static org.springframework.web.reactive.function.BodyInserters.fromPublisher;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
@@ -108,8 +102,9 @@ public class DispatcherHandlerIntegrationTests extends AbstractHttpHandlerIntegr
}
@EnableWebFlux
@Configuration
static class TestConfiguration extends WebFluxConfigurationSupport {
static class TestConfiguration {
@Bean
public PersonHandler personHandler() {
@@ -122,31 +117,23 @@ public class DispatcherHandlerIntegrationTests extends AbstractHttpHandlerIntegr
}
@Bean
public HandlerAdapter handlerAdapter() {
return new HandlerFunctionAdapter();
public RouterFunction<EntityResponse<Person>> monoRouterFunction(PersonHandler personHandler) {
return route(RequestPredicates.GET("/mono"), personHandler::mono);
}
@Bean
public HandlerMapping handlerMapping() {
PersonHandler personHandler = personHandler();
return RouterFunctions.toHandlerMapping(
route(RequestPredicates.GET("/mono"), personHandler::mono)
.and(route(RequestPredicates.GET("/flux"), personHandler::flux)));
public RouterFunction<ServerResponse> fluxRouterFunction(PersonHandler personHandler) {
return route(RequestPredicates.GET("/flux"), personHandler::flux);
}
@Bean
public ServerResponseResultHandler responseResultHandler() {
return new ServerResponseResultHandler();
}
}
private static class PersonHandler {
public Mono<ServerResponse> mono(ServerRequest request) {
public Mono<EntityResponse<Person>> mono(ServerRequest request) {
Person person = new Person("John");
return ServerResponse.ok().body(fromObject(person));
return EntityResponse.fromObject(person).build();
}
public Mono<ServerResponse> flux(ServerRequest request) {

View File

@@ -33,7 +33,6 @@ public class HandlerStrategiesTests {
assertEquals(Optional.empty(), strategies.messageReaders().get().findFirst());
assertEquals(Optional.empty(), strategies.messageWriters().get().findFirst());
assertEquals(Optional.empty(), strategies.viewResolvers().get().findFirst());
assertNull(strategies.localeResolver().get());
}
@Test
@@ -42,7 +41,6 @@ public class HandlerStrategiesTests {
assertNotEquals(Optional.empty(), strategies.messageReaders().get().findFirst());
assertNotEquals(Optional.empty(), strategies.messageWriters().get().findFirst());
assertEquals(Optional.empty(), strategies.viewResolvers().get().findFirst());
assertNotNull(strategies.localeResolver().get());
}
}

View File

@@ -19,7 +19,10 @@ package org.springframework.web.reactive.function.server;
import java.io.IOException;
import java.nio.file.Files;
import java.util.EnumSet;
import java.util.function.Supplier;
import java.util.stream.Stream;
import org.junit.Before;
import org.junit.Test;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
@@ -29,9 +32,11 @@ import org.springframework.core.io.Resource;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.codec.HttpMessageWriter;
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse;
import org.springframework.mock.http.server.reactive.test.MockServerWebExchange;
import org.springframework.web.reactive.result.view.ViewResolver;
import static org.junit.Assert.*;
@@ -44,6 +49,25 @@ public class ResourceHandlerFunctionTests {
private final ResourceHandlerFunction handlerFunction = new ResourceHandlerFunction(this.resource);
private ServerResponse.Context context;
@Before
public void createContext() {
HandlerStrategies strategies = HandlerStrategies.withDefaults();
context = new ServerResponse.Context() {
@Override
public Supplier<Stream<HttpMessageWriter<?>>> messageWriters() {
return strategies.messageWriters();
}
@Override
public Supplier<Stream<ViewResolver>> viewResolvers() {
return strategies.viewResolvers();
}
};
}
@Test
public void get() throws IOException {
@@ -59,7 +83,7 @@ public class ResourceHandlerFunctionTests {
assertTrue(response instanceof EntityResponse);
EntityResponse<Resource> entityResponse = (EntityResponse<Resource>) response;
assertEquals(this.resource, entityResponse.entity());
return response.writeTo(exchange, HandlerStrategies.withDefaults());
return response.writeTo(exchange, context);
});
StepVerifier.create(result)
@@ -94,7 +118,7 @@ public class ResourceHandlerFunctionTests {
assertTrue(response instanceof EntityResponse);
EntityResponse<Resource> entityResponse = (EntityResponse<Resource>) response;
assertEquals(this.resource.getFilename(), entityResponse.entity().getFilename());
return response.writeTo(exchange, HandlerStrategies.withDefaults());
return response.writeTo(exchange, context);
});
StepVerifier.create(result).expectComplete().verify();
@@ -116,7 +140,7 @@ public class ResourceHandlerFunctionTests {
assertEquals(HttpStatus.OK, response.statusCode());
assertEquals(EnumSet.of(HttpMethod.GET, HttpMethod.HEAD, HttpMethod.OPTIONS),
response.headers().getAllow());
return response.writeTo(exchange, HandlerStrategies.withDefaults());
return response.writeTo(exchange, context);
});

View File

@@ -199,7 +199,7 @@ public class RouterFunctionsTests {
@Override
public Mono<Void> writeTo(ServerWebExchange exchange,
HandlerStrategies strategies) {
Context context) {
return Mono.error(new ResponseStatusException(HttpStatus.NOT_FOUND, "Not found"));
}
});
@@ -232,7 +232,7 @@ public class RouterFunctionsTests {
@Override
public Mono<Void> writeTo(ServerWebExchange exchange,
HandlerStrategies strategies) {
Context context) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Not found");
}
});

View File

@@ -0,0 +1,108 @@
/*
* 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.web.reactive.function.server.support;
import java.util.Collections;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.codec.ByteBufferDecoder;
import org.springframework.http.codec.DecoderHttpMessageReader;
import org.springframework.http.codec.HttpMessageReader;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
import org.springframework.mock.http.server.reactive.test.MockServerWebExchange;
import org.springframework.web.reactive.config.EnableWebFlux;
import org.springframework.web.reactive.function.server.HandlerFunction;
import org.springframework.web.reactive.function.server.RequestPredicates;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.ServerWebExchange;
/**
* @author Arjen Poutsma
*/
public class RouterFunctionMappingTests {
private List<HttpMessageReader<?>> messageReaders;
private ServerWebExchange exchange;
private ServerCodecConfigurer codecConfigurer;
@Before
public void setUp() {
this.messageReaders =
Collections.singletonList(new DecoderHttpMessageReader<>(new ByteBufferDecoder()));
this.exchange = new MockServerWebExchange(MockServerHttpRequest.get("http://example.com/match").build());
codecConfigurer = ServerCodecConfigurer.create();
}
@Test
public void normal() {
HandlerFunction<ServerResponse> handlerFunction = request -> ServerResponse.ok().build();
RouterFunction<ServerResponse> routerFunction = request -> Mono.just(handlerFunction);
RouterFunctionMapping mapping = new RouterFunctionMapping(routerFunction);
mapping.setMessageCodecConfigurer(this.codecConfigurer);
Mono<Object> result = mapping.getHandler(this.exchange);
StepVerifier.create(result)
.expectNext(handlerFunction)
.expectComplete()
.verify();
}
@Test
public void noMatch() {
RouterFunction<ServerResponse> routerFunction = request -> Mono.empty();
RouterFunctionMapping mapping = new RouterFunctionMapping(routerFunction);
mapping.setMessageCodecConfigurer(this.codecConfigurer);
Mono<Object> result = mapping.getHandler(this.exchange);
StepVerifier.create(result)
.expectComplete()
.verify();
}
@Configuration
@EnableWebFlux
private static class TestConfig {
public RouterFunction<ServerResponse> match() {
HandlerFunction<ServerResponse> handlerFunction = request -> ServerResponse.ok().build();
return RouterFunctions.route(RequestPredicates.GET("/match"), handlerFunction);
}
public RouterFunction<ServerResponse> noMatch() {
HandlerFunction<ServerResponse> handlerFunction = request -> ServerResponse.ok().build();
RouterFunction<ServerResponse> routerFunction = request -> Mono.empty();
return RouterFunctions.route(RequestPredicates.GET("/no-match"), handlerFunction);
}
}
}