INT-4300: Add WebFlux Server Support

JIRA: https://jira.spring.io/browse/INT-4300

* Add `ReactiveHttpInboundEndpoint` based on the WebFlux foundation
* Extract `BaseHttpInboundEndpoint` for common
HTTP Inbound Channel Adapters options
* Make `spring-webmvc` and `spring-webflux` as `optional` dependencies
to let end-user to choose
* Refactor `HttpContextUtils` to include constants
for newly added WebFlux support
* Introduce `BaseHttpInboundEndpoint.setRequestPayloadTypeClass()`
for raw `Class<?>` and modify existing `setRequestPayloadType()`
for the `ResolvableType`
* Refactor existing MVC tests and XML components parsers to use
new `setRequestPayloadTypeClass()`
* Add `MessagingGatewaySupport.sendAndReceiveMessageReactive()` to get
a reply from downstream flow reactive back-pressure manner
* Add `IntegrationHandlerResultHandler` implementation to let WebFlux
infrastructure to handle the `Mono<Void>` from the `ReactiveHttpInboundEndpoint`
properly
* Fix `JdbcLockRegistryLeaderInitiatorTests` race condition to assert
the `initiator1` is elected eventually after yielding when the `initiator2`
is stopped

* Fix JavaDocs issue in the `HttpRequestHandlingMessagingGateway`
* Move all the "hard" logic in the `MessagingGatewaySupport#doSendAndReceiveMessageReactive`
to the `Mono` chain ensuring back-pressure when `sendAndReceiveMessageReactive()` is
called not from the Reactive Stream

Add test-case to demonstrate SSE

JIRA: https://jira.spring.io/browse/INT-3625

Some polishing and optimization for the
`MessagingGatewaySupport.doSendAndReceiveMessageReactive()`

More optimization for `MessagingGatewaySupport`

* Upgrade to Reactor 3.1 M3
* Document WebFlux-based components

Minor Doc Polishing
This commit is contained in:
Artem Bilan
2017-06-20 10:14:57 -04:00
committed by Gary Russell
parent e475d9c695
commit d4a99919ed
26 changed files with 2268 additions and 745 deletions

View File

@@ -16,6 +16,9 @@
package org.springframework.integration.http.dsl;
import static org.hamcrest.Matchers.instanceOf;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
@@ -27,11 +30,13 @@ import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.reactivestreams.Publisher;
import org.springframework.beans.DirectFieldAccessor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.ResolvableType;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
@@ -44,7 +49,9 @@ import org.springframework.integration.http.outbound.HttpRequestExecutingMessage
import org.springframework.integration.http.outbound.ReactiveHttpRequestExecutingMessageHandler;
import org.springframework.integration.security.channel.ChannelSecurityInterceptor;
import org.springframework.integration.security.channel.SecuredChannel;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.PollableChannel;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.vote.AffirmativeBased;
import org.springframework.security.access.vote.RoleVoter;
@@ -57,16 +64,19 @@ import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.client.MockMvcClientHttpRequestFactory;
import org.springframework.test.web.reactive.server.HttpHandlerConnector;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.reactive.config.EnableWebFlux;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
/**
* @author Artem Bilan
@@ -90,12 +100,18 @@ public class HttpDslTests {
private MockMvc mockMvc;
private WebTestClient webTestClient;
@Before
public void setup() {
this.mockMvc =
MockMvcBuilders.webAppContextSetup(this.wac)
.apply(springSecurity())
.build();
this.webTestClient =
WebTestClient.bindToApplicationContext(this.wac)
.build();
}
@@ -140,9 +156,45 @@ public class HttpDslTests {
.string("FOO"));
}
@Autowired
private PollableChannel storeChannel;
@Test
@SuppressWarnings("unchecked")
public void testHttpReactivePost() {
this.webTestClient.post().uri("/reactivePost")
.body(Flux.just("foo", "bar", "baz"), String.class)
.exchange()
.expectStatus().isAccepted();
Message<?> store = this.storeChannel.receive(10_000);
assertNotNull(store);
assertThat(store.getPayload(), instanceOf(Flux.class));
StepVerifier
.create((Publisher<String>) store.getPayload())
.expectNext("foo", "bar", "baz")
.verifyComplete();
}
@Test
public void testSse() {
Flux<String> responseBody =
this.webTestClient.get().uri("/sse")
.exchange()
.returnResult(String.class)
.getResponseBody();
StepVerifier
.create(responseBody)
.expectNext("foo", "bar", "baz")
.verifyComplete();
}
@Configuration
@EnableWebMvc
@EnableWebFlux
@EnableWebSecurity
@EnableIntegration
public static class ContextConfiguration extends WebSecurityConfigurerAdapter {
@@ -211,6 +263,26 @@ public class HttpDslTests {
.get();
}
@Bean
public IntegrationFlow httpReactiveInboundChannelAdapterFlow() {
return IntegrationFlows
.from(Http.inboundReactiveChannelAdapter("/reactivePost")
.requestMapping(m -> m.methods(HttpMethod.POST))
.requestPayloadType(ResolvableType.forClassWithGenerics(Flux.class, String.class))
.statusCodeFunction(m -> HttpStatus.ACCEPTED))
.channel(c -> c.queue("storeChannel"))
.get();
}
@Bean
public IntegrationFlow sseFlow() {
return IntegrationFlows
.from(Http.inboundReactiveGateway("/sse")
.requestMapping(m -> m.produces(MediaType.TEXT_EVENT_STREAM_VALUE)))
.handle((p, h) -> Flux.just("foo", "bar", "baz"))
.get();
}
@Bean
public AccessDecisionManager accessDecisionManager() {
return new AffirmativeBased(Collections.singletonList(new RoleVoter()));

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.
@@ -64,6 +64,7 @@ import org.springframework.util.SerializationUtils;
* @author Gunnar Hillert
* @author Artem Bilan
* @author Biju Kunjummen
*
* @since 2.0
*/
public class HttpRequestHandlingMessagingGatewayTests extends AbstractHttpInboundTests {
@@ -96,7 +97,7 @@ public class HttpRequestHandlingMessagingGatewayTests extends AbstractHttpInboun
QueueChannel requestChannel = new QueueChannel();
HttpRequestHandlingMessagingGateway gateway = new HttpRequestHandlingMessagingGateway(false);
gateway.setBeanFactory(mock(BeanFactory.class));
gateway.setRequestPayloadType(String.class);
gateway.setRequestPayloadTypeClass(String.class);
gateway.setRequestChannel(requestChannel);
gateway.afterPropertiesSet();
gateway.start();
@@ -134,7 +135,7 @@ public class HttpRequestHandlingMessagingGatewayTests extends AbstractHttpInboun
HttpRequestHandlingMessagingGateway gateway = new HttpRequestHandlingMessagingGateway(true);
gateway.setStatusCodeExpression(new LiteralExpression("foo"));
gateway.setBeanFactory(mock(BeanFactory.class));
gateway.setRequestPayloadType(String.class);
gateway.setRequestPayloadTypeClass(String.class);
gateway.setRequestChannel(requestChannel);
gateway.afterPropertiesSet();
gateway.start();
@@ -162,7 +163,7 @@ public class HttpRequestHandlingMessagingGatewayTests extends AbstractHttpInboun
});
HttpRequestHandlingMessagingGateway gateway = new HttpRequestHandlingMessagingGateway(true);
gateway.setBeanFactory(mock(BeanFactory.class));
gateway.setRequestPayloadType(String.class);
gateway.setRequestPayloadTypeClass(String.class);
gateway.setRequestChannel(requestChannel);
gateway.afterPropertiesSet();
gateway.start();
@@ -238,7 +239,7 @@ public class HttpRequestHandlingMessagingGatewayTests extends AbstractHttpInboun
QueueChannel channel = new QueueChannel();
HttpRequestHandlingMessagingGateway gateway = new HttpRequestHandlingMessagingGateway(false);
gateway.setBeanFactory(mock(BeanFactory.class));
gateway.setRequestPayloadType(TestBean.class);
gateway.setRequestPayloadTypeClass(TestBean.class);
gateway.setRequestChannel(channel);
List<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>();

View File

@@ -47,6 +47,7 @@ import org.springframework.web.context.request.RequestContextHolder;
/**
* @author Gary Russell
* @author Artem Bilan
*
* @since 4.2
*/
public class MultipartAsRawByteArrayTests {
@@ -61,7 +62,7 @@ public class MultipartAsRawByteArrayTests {
QueueChannel requestChannel = new QueueChannel();
gw.setRequestChannel(requestChannel);
gw.setBeanFactory(mock(BeanFactory.class));
gw.setRequestPayloadType(byte[].class);
gw.setRequestPayloadTypeClass(byte[].class);
gw.afterPropertiesSet();
gw.start();

View File

@@ -0,0 +1,170 @@
/*
* Copyright 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.integration.http.inbound;
import java.util.Objects;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.integration.channel.FluxMessageChannel;
import org.springframework.integration.config.EnableIntegration;
import org.springframework.messaging.MessageChannel;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.reactive.config.EnableWebFlux;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import reactor.core.publisher.Flux;
/**
* @author Artem Bilan
*
* @since 5.0
*/
@RunWith(SpringRunner.class)
@DirtiesContext
public class ReactiveHttpInboundEndpointTests {
@Autowired
private WebTestClient webTestClient;
@Autowired
private ReactiveHttpInboundEndpoint simpleInboundEndpoint;
@Test
public void testSimpleGet() {
this.webTestClient.get().uri("/test")
.exchange()
.expectStatus().isOk()
.expectBody(String.class).isEqualTo("It works!");
this.simpleInboundEndpoint.stop();
this.webTestClient.get().uri("/test")
.exchange()
.expectStatus().isEqualTo(HttpStatus.SERVICE_UNAVAILABLE)
.expectBody(String.class).isEqualTo("Endpoint is stopped");
}
@Test
public void testJsonResult() {
this.webTestClient.get().uri("/persons")
.accept(MediaType.APPLICATION_JSON_UTF8)
.exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$[0].name").isEqualTo("Jane")
.jsonPath("$[1].name").isEqualTo("Jason")
.jsonPath("$[2].name").isEqualTo("John");
}
@Configuration
@EnableWebFlux
@EnableIntegration
public static class ContextConfiguration {
@Bean
public WebTestClient webTestClient(ApplicationContext applicationContext) {
return WebTestClient.bindToApplicationContext(applicationContext).build();
}
@Bean
public ReactiveHttpInboundEndpoint simpleInboundEndpoint() {
ReactiveHttpInboundEndpoint endpoint = new ReactiveHttpInboundEndpoint();
RequestMapping requestMapping = new RequestMapping();
requestMapping.setPathPatterns("/test");
endpoint.setRequestMapping(requestMapping);
endpoint.setRequestChannelName("serviceChannel");
return endpoint;
}
@ServiceActivator(inputChannel = "serviceChannel")
String service() {
return "It works!";
}
@Bean
public ReactiveHttpInboundEndpoint jsonInboundEndpoint() {
ReactiveHttpInboundEndpoint endpoint = new ReactiveHttpInboundEndpoint();
RequestMapping requestMapping = new RequestMapping();
requestMapping.setPathPatterns("/persons");
endpoint.setRequestMapping(requestMapping);
endpoint.setRequestChannel(fluxResultChannel());
return endpoint;
}
@Bean
public MessageChannel fluxResultChannel() {
return new FluxMessageChannel();
}
@ServiceActivator(inputChannel = "fluxResultChannel")
Flux<Person> getPersons() {
return Flux.just(new Person("Jane"), new Person("Jason"), new Person("John"));
}
}
static class Person {
private final String name;
@JsonCreator
Person(@JsonProperty("name") String name) {
this.name = name;
}
public String getName() {
return this.name;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Person person = (Person) o;
return Objects.equals(this.name, person.name);
}
@Override
public int hashCode() {
return getName().hashCode();
}
@Override
public String toString() {
return "Person[name='" + this.name + "']";
}
}
}