#1224 - Provide API for users to easily configure WebClient instances.

Introduce `HypermediaWebClientConfigurer` with a simple API that registers hypermedia types via `WebClient.Builder`.

Deprecate `WebClientConfigurer`, leveraging the new solution.

Update reference documentation showing how to use it, with and without Spring Boot.
This commit is contained in:
Greg Turnquist
2020-03-25 16:03:00 -05:00
parent dea0001da2
commit 904a03a241
17 changed files with 327 additions and 186 deletions

View File

@@ -68,6 +68,7 @@ class ArchitectureTest {
.and().resideOutsideOfPackages("..reactive") //
.and().haveSimpleNameNotStartingWith("WebFlux") //
.and().haveSimpleNameNotStartingWith("WebClient") //
.and().haveSimpleNameNotStartingWith("HypermediaWebClient") //
.and().haveSimpleNameNotStartingWith("HypermediaWebTestClient") //
.should().dependOnClassesThat(areSpringFrameworkClassesWithReactiveDependency) //
.check(classes);

View File

@@ -52,7 +52,7 @@ class CustomHypermediaWebFluxTest {
ctx.register(TestConfig.class);
ctx.refresh();
WebClientConfigurer webClientConfigurer = ctx.getBean(WebClientConfigurer.class);
HypermediaWebClientConfigurer webClientConfigurer = ctx.getBean(HypermediaWebClientConfigurer.class);
this.testClient = WebTestClient.bindToApplicationContext(ctx).build() //
.mutate() //

View File

@@ -0,0 +1,101 @@
package org.springframework.hateoas.config;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Bean;
import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType;
import org.springframework.http.MediaType;
import org.springframework.http.codec.HttpMessageReader;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.reactive.function.client.ExchangeStrategies;
import org.springframework.web.reactive.function.client.WebClient;
import java.util.Collections;
import static org.assertj.core.api.Assertions.*;
import static org.springframework.hateoas.MediaTypes.*;
import static org.springframework.hateoas.support.ContextTester.*;
public class HypermediaWebClientConfigurerTest {
private static MediaType FRODO_JSON = MediaType.parseMediaType("application/frodo+json");
@Test // #1224
void webClientConfigurerHandlesSingleHypermediaType() {
withContext(HalConfig.class, context -> {
HypermediaWebClientConfigurer configurer = context.getBean(HypermediaWebClientConfigurer.class);
WebClient webClient = configurer.registerHypermediaTypes(WebClient.builder()).build();
assertThat(exchangeStrategies(webClient).messageReaders())
.flatExtracting(HttpMessageReader::getReadableMediaTypes) //
.contains(HAL_JSON) //
.doesNotContain(HAL_FORMS_JSON, COLLECTION_JSON, UBER_JSON);
});
}
@Test // #1224
void webClientConfigurerHandlesMultipleHypermediaTypes() {
withContext(AllHypermediaConfig.class, context -> {
HypermediaWebClientConfigurer configurer = context.getBean(HypermediaWebClientConfigurer.class);
WebClient webClient = configurer.registerHypermediaTypes(WebClient.builder()).build();
assertThat(exchangeStrategies(webClient).messageReaders())
.flatExtracting(HttpMessageReader::getReadableMediaTypes) //
.contains(HAL_JSON, HAL_FORMS_JSON, COLLECTION_JSON, UBER_JSON);
});
}
@Test // #1224
void webClientConfigurerHandlesCustomHypermediaTypes() {
withContext(CustomHypermediaConfig.class, context -> {
HypermediaWebClientConfigurer configurer = context.getBean(HypermediaWebClientConfigurer.class);
WebClient webClient = configurer.registerHypermediaTypes(WebClient.builder()).build();
assertThat(exchangeStrategies(webClient).messageReaders())
.flatExtracting(HttpMessageReader::getReadableMediaTypes) //
.contains(HAL_JSON, FRODO_JSON) //
.doesNotContain(HAL_FORMS_JSON, COLLECTION_JSON, UBER_JSON);
});
}
/**
* Extract the {@link ExchangeStrategies} from a {@link WebTestClient} to assert it has the proper message readers and
* writers.
*
* @param webClient
* @return
*/
private static ExchangeStrategies exchangeStrategies(WebClient webClient) {
return (ExchangeStrategies) ReflectionTestUtils
.getField(ReflectionTestUtils.getField(webClient, "exchangeFunction"), "strategies");
}
@EnableHypermediaSupport(type = HypermediaType.HAL)
static class HalConfig {
}
@EnableHypermediaSupport(
type = { HypermediaType.HAL, HypermediaType.HAL_FORMS, HypermediaType.COLLECTION_JSON, HypermediaType.UBER })
static class AllHypermediaConfig {
}
static class CustomHypermediaConfig extends HalConfig {
@Bean
HypermediaMappingInformation hypermediaMappingInformation() {
return () -> Collections.singletonList(FRODO_JSON);
}
}
}

View File

@@ -15,18 +15,6 @@
*/
package org.springframework.hateoas.config;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
import static org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
@@ -50,6 +38,17 @@ import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.config.EnableWebFlux;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
import static org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType.*;
/**
* @author Greg Turnquist
@@ -67,10 +66,9 @@ class HypermediaWebFluxConfigurerTest {
ctx.register(context);
ctx.refresh();
WebClientConfigurer webClientConfigurer = ctx.getBean(WebClientConfigurer.class);
HypermediaWebTestClientConfigurer configurer = ctx.getBean(HypermediaWebTestClientConfigurer.class);
this.testClient = WebTestClient.bindToApplicationContext(ctx).build().mutate()
.exchangeStrategies(webClientConfigurer.hypermediaExchangeStrategies()).build();
this.testClient = WebTestClient.bindToApplicationContext(ctx).build().mutateWith(configurer);
}
/**

View File

@@ -97,7 +97,7 @@ public class HypermediaWebTestClientConfigurerTest {
WebTestClient client = WebTestClient.bindToApplicationContext(context).build().mutateWith(configurer); // <2>
// Exercise the controller.
client.get().uri("http://localhost/employees") //
client.get().uri("http://localhost/employees").accept(HAL_JSON) //
.exchange() //
.expectStatus().isOk() //
.expectBody(new TypeReferences.CollectionModelType<EntityModel<Employee>>() {}) // <3>

View File

@@ -15,11 +15,6 @@
*/
package org.springframework.hateoas.mediatype.alps;
import static org.assertj.core.api.Assertions.*;
import static org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType.*;
import reactor.test.StepVerifier;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
@@ -28,12 +23,16 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.hateoas.MediaTypes;
import org.springframework.hateoas.config.EnableHypermediaSupport;
import org.springframework.hateoas.config.WebClientConfigurer;
import org.springframework.hateoas.config.HypermediaWebTestClientConfigurer;
import org.springframework.hateoas.support.WebFluxEmployeeController;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.reactive.config.EnableWebFlux;
import reactor.test.StepVerifier;
import static org.assertj.core.api.Assertions.*;
import static org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType.*;
/**
* @author Greg Turnquist
@@ -97,12 +96,8 @@ public class AlpsWebFluxIntegrationTest {
}
@Bean
WebTestClient webTestClient(WebClientConfigurer webClientConfigurer, ApplicationContext ctx) {
return WebTestClient.bindToApplicationContext(ctx).build() //
.mutate() //
.exchangeStrategies(webClientConfigurer.hypermediaExchangeStrategies()) //
.build();
WebTestClient webTestClient(HypermediaWebTestClientConfigurer configurer, ApplicationContext ctx) {
return WebTestClient.bindToApplicationContext(ctx).build().mutateWith(configurer);
}
}

View File

@@ -15,11 +15,6 @@
*/
package org.springframework.hateoas.mediatype.collectionjson;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.collection.IsCollectionWithSize.*;
import static org.springframework.hateoas.support.JsonPathUtils.*;
import static org.springframework.hateoas.support.MappingUtils.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -31,7 +26,7 @@ import org.springframework.core.io.ClassPathResource;
import org.springframework.hateoas.MediaTypes;
import org.springframework.hateoas.config.EnableHypermediaSupport;
import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType;
import org.springframework.hateoas.config.WebClientConfigurer;
import org.springframework.hateoas.config.HypermediaWebTestClientConfigurer;
import org.springframework.hateoas.support.WebFluxEmployeeController;
import org.springframework.http.HttpHeaders;
import org.springframework.test.context.ContextConfiguration;
@@ -40,6 +35,11 @@ import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.reactive.config.EnableWebFlux;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.collection.IsCollectionWithSize.*;
import static org.springframework.hateoas.support.JsonPathUtils.*;
import static org.springframework.hateoas.support.MappingUtils.*;
/**
* @author Greg Turnquist
*/
@@ -189,12 +189,8 @@ class CollectionJsonWebFluxIntegrationTest {
}
@Bean
WebTestClient webTestClient(WebClientConfigurer webClientConfigurer, ApplicationContext ctx) {
return WebTestClient.bindToApplicationContext(ctx).build() //
.mutate() //
.exchangeStrategies(webClientConfigurer.hypermediaExchangeStrategies()) //
.build();
WebTestClient webTestClient(HypermediaWebTestClientConfigurer configurer, ApplicationContext ctx) {
return WebTestClient.bindToApplicationContext(ctx).build().mutateWith(configurer);
}
}
}

View File

@@ -15,13 +15,6 @@
*/
package org.springframework.hateoas.mediatype.hal.forms;
import static org.assertj.core.api.Assertions.*;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.collection.IsCollectionWithSize.*;
import static org.springframework.hateoas.support.JsonPathUtils.*;
import java.net.URI;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -33,7 +26,7 @@ import org.springframework.core.io.ClassPathResource;
import org.springframework.hateoas.MediaTypes;
import org.springframework.hateoas.config.EnableHypermediaSupport;
import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType;
import org.springframework.hateoas.config.WebClientConfigurer;
import org.springframework.hateoas.config.HypermediaWebTestClientConfigurer;
import org.springframework.hateoas.mediatype.problem.Problem;
import org.springframework.hateoas.support.MappingUtils;
import org.springframework.hateoas.support.WebFluxEmployeeController;
@@ -45,6 +38,13 @@ import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.reactive.config.EnableWebFlux;
import java.net.URI;
import static org.assertj.core.api.Assertions.*;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.collection.IsCollectionWithSize.*;
import static org.springframework.hateoas.support.JsonPathUtils.*;
/**
* @author Greg Turnquist
*/
@@ -136,7 +136,8 @@ class HalFormsWebFluxIntegrationTest {
@Test // #786
void problemReturningControllerMethod() {
Problem problem = this.testClient.get().uri("http://localhost/employees/problem").accept(MediaTypes.HTTP_PROBLEM_DETAILS_JSON) //
Problem problem = this.testClient.get().uri("http://localhost/employees/problem")
.accept(MediaTypes.HTTP_PROBLEM_DETAILS_JSON) //
.exchange() //
.expectStatus().isBadRequest() //
.expectHeader().contentType(MediaTypes.HTTP_PROBLEM_DETAILS_JSON) //
@@ -161,10 +162,8 @@ class HalFormsWebFluxIntegrationTest {
}
@Bean
WebTestClient webTestClient(WebClientConfigurer webClientConfigurer, ApplicationContext ctx) {
return WebTestClient.bindToApplicationContext(ctx).build().mutate()
.exchangeStrategies(webClientConfigurer.hypermediaExchangeStrategies()).build();
WebTestClient webTestClient(HypermediaWebTestClientConfigurer configurer, ApplicationContext ctx) {
return WebTestClient.bindToApplicationContext(ctx).build().mutateWith(configurer);
}
}
}

View File

@@ -15,11 +15,6 @@
*/
package org.springframework.hateoas.mediatype.uber;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.collection.IsCollectionWithSize.*;
import static org.springframework.hateoas.support.JsonPathUtils.*;
import static org.springframework.hateoas.support.MappingUtils.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -31,7 +26,7 @@ import org.springframework.core.io.ClassPathResource;
import org.springframework.hateoas.MediaTypes;
import org.springframework.hateoas.config.EnableHypermediaSupport;
import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType;
import org.springframework.hateoas.config.WebClientConfigurer;
import org.springframework.hateoas.config.HypermediaWebTestClientConfigurer;
import org.springframework.hateoas.support.WebFluxEmployeeController;
import org.springframework.http.HttpHeaders;
import org.springframework.test.context.ContextConfiguration;
@@ -40,6 +35,11 @@ import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.reactive.config.EnableWebFlux;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.collection.IsCollectionWithSize.*;
import static org.springframework.hateoas.support.JsonPathUtils.*;
import static org.springframework.hateoas.support.MappingUtils.*;
/**
* @author Greg Turnquist
*/
@@ -247,12 +247,8 @@ class UberWebFluxIntegrationTest {
}
@Bean
WebTestClient webTestClient(WebClientConfigurer webClientConfigurer, ApplicationContext ctx) {
return WebTestClient.bindToApplicationContext(ctx).build() //
.mutate() //
.exchangeStrategies(webClientConfigurer.hypermediaExchangeStrategies()) //
.build();
WebTestClient webTestClient(HypermediaWebTestClientConfigurer configurer, ApplicationContext ctx) {
return WebTestClient.bindToApplicationContext(ctx).build().mutateWith(configurer);
}
}
}

View File

@@ -15,13 +15,6 @@
*/
package org.springframework.hateoas.server.reactive;
import static org.assertj.core.api.AssertionsForInterfaceTypes.*;
import static org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType.*;
import static org.springframework.hateoas.server.reactive.WebFluxLinkBuilder.*;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
@@ -32,12 +25,18 @@ import org.springframework.hateoas.Link;
import org.springframework.hateoas.MediaTypes;
import org.springframework.hateoas.RepresentationModel;
import org.springframework.hateoas.config.EnableHypermediaSupport;
import org.springframework.hateoas.config.WebClientConfigurer;
import org.springframework.hateoas.config.HypermediaWebTestClientConfigurer;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.config.EnableWebFlux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import static org.assertj.core.api.AssertionsForInterfaceTypes.*;
import static org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType.*;
import static org.springframework.hateoas.server.reactive.WebFluxLinkBuilder.*;
/**
* Verify WebFlux properly activates the {@link org.springframework.web.filter.reactive.ServerWebExchangeContextFilter}.
@@ -55,10 +54,9 @@ class HypermediaWebFilterTest {
ctx.register(WebFluxConfig.class);
ctx.refresh();
WebClientConfigurer webClientConfigurer = ctx.getBean(WebClientConfigurer.class);
HypermediaWebTestClientConfigurer configurer = ctx.getBean(HypermediaWebTestClientConfigurer.class);
this.testClient = WebTestClient.bindToApplicationContext(ctx).build().mutate()
.exchangeStrategies(webClientConfigurer.hypermediaExchangeStrategies()).build();
this.testClient = WebTestClient.bindToApplicationContext(ctx).build().mutateWith(configurer);
}
/**
@@ -67,7 +65,15 @@ class HypermediaWebFilterTest {
@Test
void webFilterShouldEmbedExchangeIntoContext() {
this.testClient.get().uri("https://example.com/api") //
String results = this.testClient.get().uri("https://example.com/api") //
.exchange() //
.expectStatus().isOk() //
.expectBody(String.class) //
.returnResult().getResponseBody();
System.out.println("results = " + results);
this.testClient.get().uri("https://example.com/api") //
.accept(MediaTypes.HAL_JSON) //
.exchange() //
.expectStatus().isOk() //
@@ -81,7 +87,8 @@ class HypermediaWebFilterTest {
.containsExactly(Link.of("https://example.com/api", IanaLinkRelations.SELF));
return true;
}).verifyComplete();
}) //
.verifyComplete();
}
@RestController

View File

@@ -15,11 +15,11 @@
*/
package org.springframework.hateoas.support;
import java.util.function.Function;
import org.springframework.mock.web.MockServletContext;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import java.util.function.Function;
/**
* @author Greg Turnquist
*/