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:
committed by
Gary Russell
parent
e475d9c695
commit
d4a99919ed
@@ -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()));
|
||||
|
||||
@@ -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<?>>();
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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 + "']";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user