#1223 - Provide API to configure RestTemplate with hypermedia.

Create a HypermediaRestTemplateConfigurer bean registered as a Spring HATEOAS bean.

Update reference documentation to show how to use it directly and with Spring Boot via RestTemplateCustomizer.
This commit is contained in:
Greg Turnquist
2020-03-23 16:50:44 -05:00
parent 4e697b899b
commit dea0001da2
6 changed files with 218 additions and 15 deletions

View File

@@ -0,0 +1,20 @@
package org.springframework.hateoas.client;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.hateoas.config.HypermediaRestTemplateConfigurer;
import org.springframework.web.client.RestTemplate;
// tag::code[]
@Configuration
public class HypermediaConfiguration {
/**
* Use the {@link HypermediaRestTemplateConfigurer} to configure a {@link RestTemplate}.
*/
@Bean
RestTemplate hypermediaRestTemplate(HypermediaRestTemplateConfigurer configurer) { // <1>
return configurer.registerHypermediaTypes(new RestTemplate()); // <2>
}
}
// end::code[]

View File

@@ -146,4 +146,77 @@ Again, you can use similar assertions as the earlier example.
There are many other ways to fashion test cases. `WebTestClient` can be bound to controllers, functions, and URLs. This section isn't meant to show all that. Instead, this gives you some examples to get started. The important thing is that by applying `HypermediaWebTestClientConfigurer`, any instance of `WebTestClient` can be altered to handle hypermedia.
[[client.rest-template]]
== Configuring RestTemplate instances
If you want to create your own copy of `RestTemplate`, configured to speak hypermedia, you can use the `HypermediaRestTemplateConfigurer`:
.Configuring a `RestTemplate` yourself
====
[source, java]
----
include::{base-dir}/src/docs/java/org/springframework/hateoas/client/HypermediaConfiguration.java[tag=code]
----
<1> Inside your `@Configuration` class, get a copy of the `HypermediaRestTemplateConfigurer` bean Spring HATEOAS registers.
<2> After creating a `RestTemplate`, use the configurer to apply hypermedia types.
====
You are free to apply this pattern to any instance of `RestTemplate` that you need, whether is to create a registered bean, or inside a service you define.
If you're using Spring Boot, there is another approach.
In general, Spring Boot has moved away from the concept of registering a `RestTemplate` bean in the application context.
* When talking to different services, you often need different credentials.
* When `RestTemplate` uses an underlying connection pool, you run into additional issues.
* Users often need different instances rather than a single bean.
To compensate for this, Spring Boot provides a `RestTemplateBuilder`. This autoconfigured bean lets you define various beans used to fashion
a `RestTemplate` instance. You ask for a `RestTemplateBuilder` bean, execute its `build()` method, and then apply final settings (like credentials, etc.).
To register hypermedia-based message converters, add the following to your code:
.Letting Spring Boot configure things
====
[source,java]
----
@Bean // <4>
RestTemplateCustomizer hypermediaRestTemplatCustomizer(HypermediaRestTemplateConfigurer configurer) { // <1>
return restTemplate -> { // <2>
configurer.registerHypermediaTypes(restTemplate); // <3>
};
}
----
<1> When creating a Spring bean, request a copy of Spring HATEOAS's `HypermediaRestTemplateConfigurer` bean.
<2> Use a Java 8 lambda expression to define a `RestTemplateCustomizer`.
<3> Inside the function call, apply the `registerHypermediaTypes` method.
<4> Return the whole thing as a Spring bean so Spring Boot can pick it up and apply it to its autoconfigured `RestTemplateBuilder`.
====
Assuming you added that `RestTemplateCustomizer` bean definition, this is all you must do to get a `RestTemplate` with hypermedia support:
.Injecting `RestTemplateBuilder` into your service
====
[source,java]
----
@Service
public class SampleService {
private RestTemplateBuilder restTemplateBuilder;
public SampleService(RestTemplateBuilder restTemplateBuilder) { // <1>
this.restTemplateBuilder = restTemplateBuilder;
}
void doSomething() {
RestTemplate template = restTemplateBuilder.build(); // <2>
// Your template is now configured to speak hypermedia!
}
}
----
<1> Use *constructor injection* to get a hold of Spring Boot's `RestTemplateBuilder`.
<2> When you need a `RestTemplate`, invoke `build()`. The instance will have tapped your `RestTemplateCustomizer` code and registered support.
====
That's all it takes to get `RestTemplate` hypermedia support.

View File

@@ -0,0 +1,30 @@
package org.springframework.hateoas.config;
import org.springframework.web.client.RestTemplate;
/**
* Assembles hypermedia-based message converters and applies them to an existing {@link RestTemplate}.
*
* @author Greg Turnquist
* @since 1.1
*/
public class HypermediaRestTemplateConfigurer {
private final WebConverters converters;
HypermediaRestTemplateConfigurer(WebConverters converters) {
this.converters = converters;
}
/**
* Insert hypermedia-aware message converters in front of any other existing message converters.
*
* @param template
* @return
*/
public RestTemplate registerHypermediaTypes(RestTemplate template) {
template.setMessageConverters(converters.and(template.getMessageConverters()));
return template;
}
}

View File

@@ -16,9 +16,7 @@
package org.springframework.hateoas.config;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -36,8 +34,13 @@ class RestTemplateHateoasConfiguration {
@Bean
static HypermediaRestTemplateBeanPostProcessor hypermediaRestTemplateBeanPostProcessor(
ObjectProvider<WebConverters> converters) {
return new HypermediaRestTemplateBeanPostProcessor(converters);
HypermediaRestTemplateConfigurer configurer) {
return new HypermediaRestTemplateBeanPostProcessor(configurer);
}
@Bean
HypermediaRestTemplateConfigurer hypermediaRestTemplateConfigurer(WebConverters converters) {
return new HypermediaRestTemplateConfigurer(converters);
}
/**
@@ -50,7 +53,7 @@ class RestTemplateHateoasConfiguration {
@RequiredArgsConstructor
static class HypermediaRestTemplateBeanPostProcessor implements BeanPostProcessor {
private final ObjectProvider<WebConverters> converters;
private final HypermediaRestTemplateConfigurer configurer;
/*
* (non-Javadoc)
@@ -64,10 +67,7 @@ class RestTemplateHateoasConfiguration {
return bean;
}
RestTemplate template = (RestTemplate) bean;
template.setMessageConverters(converters.getObject().and(template.getMessageConverters()));
return template;
return this.configurer.registerHypermediaTypes((RestTemplate) bean);
}
}
}

View File

@@ -15,18 +15,17 @@
*/
package org.springframework.hateoas.config;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.hateoas.RepresentationModel;
import org.springframework.hateoas.server.mvc.TypeConstrainedMappingJackson2HttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter;
import org.springframework.util.Assert;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
/**
* Value type to handle registration of hypermedia related {@link HttpMessageConverter}s.

View File

@@ -0,0 +1,81 @@
package org.springframework.hateoas.config;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Bean;
import org.springframework.hateoas.MediaTypes;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import java.util.Collections;
import static org.assertj.core.api.Assertions.*;
import static org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType.*;
import static org.springframework.hateoas.support.ContextTester.*;
public class HypermediaRestTemplateConfigurerTest {
private static MediaType FRODO_JSON = MediaType.parseMediaType("application/frodo+json");
@Test // #1223
void webConvertersShouldAddHypermediaMessageConverters() {
withContext(HalConfig.class, context -> {
HypermediaRestTemplateConfigurer configurer = context.getBean(HypermediaRestTemplateConfigurer.class);
RestTemplate restTemplate = configurer.registerHypermediaTypes(new RestTemplate());
assertThat(restTemplate.getMessageConverters()).flatExtracting(HttpMessageConverter::getSupportedMediaTypes)
.contains(MediaTypes.HAL_JSON) //
.doesNotContain(MediaTypes.HAL_FORMS_JSON, MediaTypes.COLLECTION_JSON, MediaTypes.UBER_JSON);
});
}
@Test // #1223
void webConvertersShouldAddAllHypermediaMessageConverters() {
withContext(AllHypermediaConfig.class, context -> {
HypermediaRestTemplateConfigurer configurer = context.getBean(HypermediaRestTemplateConfigurer.class);
RestTemplate restTemplate = configurer.registerHypermediaTypes(new RestTemplate());
assertThat(restTemplate.getMessageConverters()).flatExtracting(HttpMessageConverter::getSupportedMediaTypes)
.contains(MediaTypes.HAL_JSON, MediaTypes.HAL_FORMS_JSON, MediaTypes.COLLECTION_JSON, MediaTypes.UBER_JSON);
});
}
@Test // #1223
void webConvertersShouldSupportCustomHypermediaTypes() {
withContext(CustomHypermediaConfig.class, context -> {
HypermediaRestTemplateConfigurer configurer = context.getBean(HypermediaRestTemplateConfigurer.class);
RestTemplate restTemplate = configurer.registerHypermediaTypes(new RestTemplate());
assertThat(restTemplate.getMessageConverters()).flatExtracting(HttpMessageConverter::getSupportedMediaTypes)
.contains(MediaTypes.HAL_JSON, FRODO_JSON)
.doesNotContain(MediaTypes.HAL_FORMS_JSON, MediaTypes.COLLECTION_JSON, MediaTypes.UBER_JSON);
});
}
@EnableHypermediaSupport(type = HAL)
static class HalConfig {
}
@EnableHypermediaSupport(type = { HAL, HAL_FORMS, COLLECTION_JSON, UBER })
static class AllHypermediaConfig {
}
static class CustomHypermediaConfig extends HalConfig {
@Bean
HypermediaMappingInformation hypermediaMappingInformation() {
return () -> Collections.singletonList(FRODO_JSON);
}
}
}