Refactor HTTP client contracts

This commit refactors the `ClientHttpRequestFactory` into an
`ClientHttpConnector` abstraction, in order to reflect that
`ClientHttpRequest`s only "exist" once the client is connected
to the origin server.

This is why the HTTP client is now callback-based, containing all
interactions with the request within a
`Function<ClientHttpRequest,Mono<Void>>` that signals when it's done
writing to the request.

The `ClientHttpRequest` contract also adopts `setComplete()`
and promotes that method to the `ReactiveHttpOutputMessage` contract.

This commit also adapts all other APIs to that change and fixes a few
issues, including:

* use `HttpMessageConverter`s instead of `Encoders`/`Decoders`
* better handle type information about request content publishers
* support client cookies in HTTP requests
* temporarily remove the RxNetty client support
This commit is contained in:
Brian Clozel
2016-07-08 11:40:12 +02:00
parent b5bce1f017
commit 4892436efe
30 changed files with 942 additions and 1221 deletions

View File

@@ -18,15 +18,16 @@ package org.springframework.http.server.reactive;
import org.junit.Before;
import org.junit.Test;
import static org.springframework.web.client.reactive.HttpRequestBuilders.get;
import static org.springframework.web.client.reactive.WebResponseExtractors.bodyStream;
import static org.springframework.web.client.reactive.ClientWebRequestBuilders.get;
import static org.springframework.web.client.reactive.ResponseExtractors.bodyStream;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.test.TestSubscriber;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.FlushingDataBuffer;
import org.springframework.http.client.reactive.ReactorHttpClientRequestFactory;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.web.client.reactive.WebClient;
/**
@@ -39,7 +40,7 @@ public class FlushingIntegrationTests extends AbstractHttpHandlerIntegrationTest
@Before
public void setup() throws Exception {
super.setup();
this.webClient = new WebClient(new ReactorHttpClientRequestFactory());
this.webClient = new WebClient(new ReactorClientHttpConnector());
}
@Test

View File

@@ -16,33 +16,41 @@
package org.springframework.web.client.reactive;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.*;
import org.junit.Before;
import org.junit.Test;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.springframework.http.HttpMethod;
/**
*
* @author Rob Winch
*
*/
public class DefaultHttpRequestBuilderTests {
private DefaultHttpRequestBuilder builder;
public class DefaultWebRequestBuilderTests {
private DefaultClientWebRequestBuilder builder;
@Before
public void setup() {
builder = new DefaultHttpRequestBuilder(HttpMethod.GET, "https://example.com/foo");
builder = new DefaultClientWebRequestBuilder(HttpMethod.GET, "https://example.com/foo");
}
@Test
public void apply() {
RequestPostProcessor postProcessor = mock(RequestPostProcessor.class);
ClientWebRequestPostProcessor postProcessor = mock(ClientWebRequestPostProcessor.class);
when(postProcessor.postProcess(any(ClientWebRequest.class))).thenAnswer(new Answer<ClientWebRequest>() {
@Override
public ClientWebRequest answer(InvocationOnMock invocation) throws Throwable {
return (ClientWebRequest) invocation.getArguments()[0];
}
});
builder.apply(postProcessor);
ClientWebRequest webRequest = builder.apply(postProcessor).build();
verify(postProcessor).postProcess(builder);
verify(postProcessor).postProcess(webRequest);
}
@Test(expected = IllegalArgumentException.class)

View File

@@ -17,8 +17,8 @@
package org.springframework.web.client.reactive;
import static org.junit.Assert.*;
import static org.springframework.web.client.reactive.HttpRequestBuilders.*;
import static org.springframework.web.client.reactive.WebResponseExtractors.*;
import static org.springframework.web.client.reactive.ClientWebRequestBuilders.*;
import static org.springframework.web.client.reactive.ResponseExtractors.*;
import java.util.function.Consumer;
@@ -38,9 +38,11 @@ import org.springframework.http.codec.Pojo;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.reactive.ReactorHttpClientRequestFactory;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
/**
* {@link WebClient} integration tests with the {@code Flux} and {@code Mono} API.
*
* @author Brian Clozel
*/
public class WebClientIntegrationTests {
@@ -52,7 +54,7 @@ public class WebClientIntegrationTests {
@Before
public void setup() {
this.server = new MockWebServer();
this.webClient = new WebClient(new ReactorHttpClientRequestFactory());
this.webClient = new WebClient(new ReactorClientHttpConnector());
}
@Test
@@ -228,12 +230,14 @@ public class WebClientIntegrationTests {
public void shouldPostPojoAsJson() throws Exception {
HttpUrl baseUrl = server.url("/pojo/capitalize");
this.server.enqueue(new MockResponse().setBody("{\"bar\":\"BARBAR\",\"foo\":\"FOOFOO\"}"));
this.server.enqueue(new MockResponse()
.setHeader("Content-Type", "application/json")
.setBody("{\"bar\":\"BARBAR\",\"foo\":\"FOOFOO\"}"));
Pojo spring = new Pojo("foofoo", "barbar");
Mono<Pojo> result = this.webClient
.perform(post(baseUrl.toString())
.content(spring)
.body(spring)
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON))
.extract(body(Pojo.class));
@@ -252,6 +256,28 @@ public class WebClientIntegrationTests {
assertEquals("application/json", request.getHeader(HttpHeaders.CONTENT_TYPE));
}
@Test
public void shouldSendCookieHeader() throws Exception {
HttpUrl baseUrl = server.url("/test");
this.server.enqueue(new MockResponse()
.setHeader("Content-Type", "text/plain").setBody("test"));
Mono<String> result = this.webClient
.perform(get(baseUrl.toString())
.cookie("testkey", "testvalue"))
.extract(body(String.class));
TestSubscriber
.subscribe(result)
.awaitAndAssertNextValues("test")
.assertComplete();
RecordedRequest request = server.takeRequest();
assertEquals(1, server.getRequestCount());
assertEquals("/test", request.getPath());
assertEquals("testkey=testvalue", request.getHeader(HttpHeaders.COOKIE));
}
@Test
public void shouldGetErrorWhen404() throws Exception {
@@ -262,7 +288,6 @@ public class WebClientIntegrationTests {
.perform(get(baseUrl.toString()))
.extract(body(String.class));
// TODO: error message should be converted to a ClientException
TestSubscriber
.subscribe(result)
.await()

View File

@@ -16,7 +16,11 @@
package org.springframework.web.reactive.result.method.annotation;
import static org.springframework.web.client.reactive.ClientWebRequestBuilders.*;
import static org.springframework.web.client.reactive.ResponseExtractors.*;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -29,14 +33,16 @@ import reactor.core.test.TestSubscriber;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.codec.Encoder;
import org.springframework.core.codec.ByteBufferDecoder;
import org.springframework.core.codec.ByteBufferEncoder;
import org.springframework.core.codec.Encoder;
import org.springframework.core.codec.StringDecoder;
import org.springframework.core.codec.StringEncoder;
import org.springframework.http.MediaType;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.http.codec.SseEventEncoder;
import org.springframework.http.codec.json.JacksonJsonDecoder;
import org.springframework.http.codec.json.JacksonJsonEncoder;
import org.springframework.core.codec.StringDecoder;
import org.springframework.http.MediaType;
import org.springframework.http.client.reactive.ReactorHttpClientRequestFactory;
import org.springframework.http.codec.SseEventEncoder;
import org.springframework.http.converter.reactive.CodecHttpMessageConverter;
import org.springframework.http.converter.reactive.HttpMessageConverter;
import org.springframework.http.server.reactive.AbstractHttpHandlerIntegrationTests;
@@ -49,9 +55,6 @@ import org.springframework.web.reactive.config.WebReactiveConfiguration;
import org.springframework.web.reactive.sse.SseEvent;
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
import static org.springframework.web.client.reactive.HttpRequestBuilders.get;
import static org.springframework.web.client.reactive.WebResponseExtractors.bodyStream;
/**
* @author Sebastien Deleuze
*/
@@ -64,11 +67,12 @@ public class SseIntegrationTests extends AbstractHttpHandlerIntegrationTests {
@Before
public void setup() throws Exception {
super.setup();
this.webClient = new WebClient(new ReactorHttpClientRequestFactory());
this.webClient.setMessageDecoders(Arrays.asList(
new ByteBufferDecoder(),
new StringDecoder(false),
new JacksonJsonDecoder()));
this.webClient = new WebClient(new ReactorClientHttpConnector());
List<HttpMessageConverter<?>> converters = new ArrayList<>();
converters.add(new CodecHttpMessageConverter<>(new ByteBufferEncoder(), new ByteBufferDecoder()));
converters.add(new CodecHttpMessageConverter<>(new StringEncoder(), new StringDecoder(false)));
converters.add(new CodecHttpMessageConverter<>(new JacksonJsonEncoder(), new JacksonJsonDecoder()));
this.webClient.setMessageConverters(converters);
}
@Override