Add support for Jetty Reactive Streams HTTP client

Leverage https://github.com/jetty-project/jetty-reactive-httpclient
to add support for Jetty in WebClient via JettyClientHttpConnector.

Implemented with buffer copy instead of optimized buffer wrapping
because the latter hangs since Callback#succeeded doesn't allow
releasing the buffer and requesting more data at different times
(required for Mono<DataBuffer> for example).
See https://github.com/eclipse/jetty.project/issues/2429.

Issue: SPR-15092
This commit is contained in:
sdeleuze
2018-04-11 10:42:36 +02:00
committed by Sebastien Deleuze
parent 3c9049d530
commit a87764f1fd
8 changed files with 465 additions and 3 deletions

View File

@@ -32,6 +32,8 @@ import org.hamcrest.Matchers;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
@@ -46,6 +48,9 @@ import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.http.client.reactive.JettyClientHttpConnector;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.http.codec.Pojo;
import static org.junit.Assert.*;
@@ -56,18 +61,35 @@ import static org.junit.Assert.*;
* @author Brian Clozel
* @author Rossen Stoyanchev
* @author Denys Ivano
* @author Sebastien Deleuze
*/
@RunWith(Parameterized.class)
public class WebClientIntegrationTests {
private MockWebServer server;
private WebClient webClient;
@Parameterized.Parameter(0)
public ClientHttpConnector connector;
@Parameterized.Parameters(name = "webClient [{0}]")
public static Object[][] arguments() {
return new Object[][] {
{new JettyClientHttpConnector()},
{new ReactorClientHttpConnector()}
};
}
@Before
public void setup() {
this.server = new MockWebServer();
this.webClient = WebClient.create(this.server.url("/").toString());
this.webClient = WebClient
.builder()
.clientConnector(this.connector)
.baseUrl(this.server.url("/").toString())
.build();
}
@After
@@ -124,6 +146,28 @@ public class WebClientIntegrationTests {
});
}
@Test
public void shouldReceivePlainTextFlux() throws Exception {
prepareResponse(response -> response.setBody("Hello Spring!"));
Flux<String> result = this.webClient.get()
.uri("/greeting?name=Spring")
.header("X-Test-Header", "testvalue")
.exchange()
.flatMapMany(response -> response.bodyToFlux(String.class));
StepVerifier.create(result)
.expectNext("Hello Spring!")
.expectComplete().verify(Duration.ofSeconds(3));
expectRequestCount(1);
expectRequest(request -> {
assertEquals("testvalue", request.getHeader("X-Test-Header"));
assertEquals("*/*", request.getHeader(HttpHeaders.ACCEPT));
assertEquals("/greeting?name=Spring", request.getPath());
});
}
@Test
public void shouldReceiveJsonAsString() {
String content = "{\"bar\":\"barbar\",\"foo\":\"foofoo\"}";

View File

@@ -16,12 +16,14 @@
package org.springframework.web.reactive.result.method.annotation;
import java.io.File;
import java.time.Duration;
import org.junit.Assume;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runners.Parameterized;
import reactor.core.publisher.Flux;
import reactor.core.publisher.MonoProcessor;
import reactor.test.StepVerifier;
@@ -30,11 +32,16 @@ import org.springframework.context.annotation.AnnotationConfigApplicationContext
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.http.client.reactive.JettyClientHttpConnector;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.http.server.reactive.AbstractHttpHandlerIntegrationTests;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.http.server.reactive.bootstrap.JettyHttpServer;
import org.springframework.http.server.reactive.bootstrap.ReactorHttpServer;
import org.springframework.http.server.reactive.bootstrap.TomcatHttpServer;
import org.springframework.http.server.reactive.bootstrap.UndertowHttpServer;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@@ -56,12 +63,33 @@ public class SseIntegrationTests extends AbstractHttpHandlerIntegrationTests {
private WebClient webClient;
@Parameterized.Parameter(1)
public ClientHttpConnector connector;
@Parameterized.Parameters(name = "server [{0}] webClient [{1}]")
public static Object[][] arguments() {
File base = new File(System.getProperty("java.io.tmpdir"));
return new Object[][] {
{new JettyHttpServer(), new ReactorClientHttpConnector()},
{new JettyHttpServer(), new JettyClientHttpConnector()},
{new ReactorHttpServer(), new ReactorClientHttpConnector()},
{new ReactorHttpServer(), new JettyClientHttpConnector()},
{new TomcatHttpServer(base.getAbsolutePath()), new ReactorClientHttpConnector()},
{new TomcatHttpServer(base.getAbsolutePath()), new JettyClientHttpConnector()},
{new UndertowHttpServer(), new ReactorClientHttpConnector()},
{new UndertowHttpServer(), new JettyClientHttpConnector()}
};
}
@Override
@Before
public void setup() throws Exception {
super.setup();
this.webClient = WebClient.create("http://localhost:" + this.port + "/sse");
this.webClient = WebClient
.builder()
.clientConnector(this.connector)
.baseUrl("http://localhost:" + this.port + "/sse")
.build();
}