#1119 - Avoid reference to spring-webmvc in configuration for RestTemplate.

RestTemplateHateoasConfiguration previously referred to HypermediaWebMvcConfigurer which is turn depending on WebMvcConfigurer, a type living in spring-webmvc. The very former would still get active in WebFlux as the web stack is selected based on the presence of either DispatcherServlet or DispatcherHandler. This is now resolved by using a WebMvcConverter indrection to handle the configuration tweaks avoiding the reference to interfaces from spring-webmvc.

Introduced an ArchUnit based test that Spring HATEOAS code only depends on Spring types containing references to reactive types from either the ….reactive package or classes starting with WebFlux.
This commit is contained in:
Oliver Drotbohm
2019-11-09 09:04:55 -06:00
parent 5444e14017
commit 5f016d6c9e
5 changed files with 250 additions and 38 deletions

View File

@@ -18,11 +18,9 @@ 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;
import org.springframework.hateoas.config.WebMvcHateoasConfiguration.HypermediaWebMvcConfigurer;
import org.springframework.lang.NonNull;
import org.springframework.web.client.RestTemplate;
@@ -37,10 +35,10 @@ class RestTemplateHateoasConfiguration {
@Bean
static HypermediaRestTemplateBeanPostProcessor hypermediaRestTemplateBeanPostProcessor(
ObjectProvider<HypermediaWebMvcConfigurer> configurer) {
return new HypermediaRestTemplateBeanPostProcessor(configurer);
WebMvcConverters converters) {
return new HypermediaRestTemplateBeanPostProcessor(converters);
}
/**
* {@link BeanPostProcessor} to register hypermedia support with {@link RestTemplate} instances found in the
* application context.
@@ -51,7 +49,7 @@ class RestTemplateHateoasConfiguration {
@RequiredArgsConstructor
static class HypermediaRestTemplateBeanPostProcessor implements BeanPostProcessor {
private final ObjectProvider<HypermediaWebMvcConfigurer> configurer;
private final WebMvcConverters converters;
/*
* (non-Javadoc)
@@ -65,9 +63,10 @@ class RestTemplateHateoasConfiguration {
return bean;
}
configurer.getObject().extendMessageConverters(((RestTemplate) bean).getMessageConverters());
RestTemplate template = (RestTemplate) bean;
template.setMessageConverters(converters.and(template.getMessageConverters()));
return bean;
return template;
}
}
}

View File

@@ -17,12 +17,17 @@ package org.springframework.hateoas.config;
import lombok.RequiredArgsConstructor;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.codec.Decoder;
import org.springframework.core.codec.Encoder;
import org.springframework.http.MediaType;
import org.springframework.http.codec.CodecConfigurer.CustomCodecs;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.http.codec.json.Jackson2JsonDecoder;
import org.springframework.http.codec.json.Jackson2JsonEncoder;
@@ -36,16 +41,25 @@ import com.fasterxml.jackson.databind.ObjectMapper;
* Spring WebFlux HATEOAS configuration.
*
* @author Greg Turnquist
* @since 1.0 TODO: Inspect ApplicationContext -> WebApplicationContext -> WebMVC
* @author Oliver Drotbohm
* @since 1.0
*/
@Configuration
class WebFluxHateoasConfiguration {
@Bean
HypermediaWebFluxConfigurer hypermediaWebFluxConfigurer(ObjectProvider<ObjectMapper> mapper,
List<HypermediaMappingInformation> hypermediaTypes) {
WebFluxCodecs hypermediaConverters(ObjectProvider<ObjectMapper> mapper,
List<HypermediaMappingInformation> mappingInformation) {
return new WebFluxCodecs(mapper.getIfAvailable(ObjectMapper::new), mappingInformation);
}
return new HypermediaWebFluxConfigurer(mapper.getIfAvailable(ObjectMapper::new), hypermediaTypes);
@Bean
HypermediaWebFluxConfigurer hypermediaWebFluxConfigurer(ObjectProvider<ObjectMapper> mapper,
List<HypermediaMappingInformation> mappingInformation) {
WebFluxCodecs codecs = new WebFluxCodecs(mapper.getIfAvailable(ObjectMapper::new), mappingInformation);
return new HypermediaWebFluxConfigurer(codecs);
}
@Bean
@@ -64,27 +78,53 @@ class WebFluxHateoasConfiguration {
@RequiredArgsConstructor
static class HypermediaWebFluxConfigurer implements WebFluxConfigurer {
private final ObjectMapper mapper;
private final List<HypermediaMappingInformation> hypermediaTypes;
private final WebFluxCodecs codecs;
/**
* Configure custom HTTP message readers and writers or override built-in ones.
* <p>
* The configured readers and writers will be used for both annotated controllers and functional endpoints.
*
* @param configurer the configurer to use
* @param configurer the configurer to use, must not be {@literal null}.
*/
@Override
public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
codecs.registerCodecs(configurer.customCodecs());
}
}
this.hypermediaTypes.forEach(hypermedia -> {
private static class WebFluxCodecs {
ObjectMapper objectMapper = hypermedia.configureObjectMapper(this.mapper.copy());
MimeType[] mimeTypes = hypermedia.getMediaTypes().toArray(new MimeType[0]);
configurer.customCodecs().encoder(new Jackson2JsonEncoder(objectMapper, mimeTypes));
configurer.customCodecs().decoder(new Jackson2JsonDecoder(objectMapper, mimeTypes));
});
private final List<Decoder<?>> decoders;
private final List<Encoder<?>> encoders;
private WebFluxCodecs(ObjectMapper mapper, List<HypermediaMappingInformation> mappingInformation) {
this.decoders = new ArrayList<>();
this.encoders = new ArrayList<>();
for (HypermediaMappingInformation information : mappingInformation) {
ObjectMapper objectMapper = information.configureObjectMapper(mapper.copy());
List<MediaType> mediaTypes = information.getMediaTypes();
this.decoders.add(getDecoder(objectMapper, mediaTypes));
this.encoders.add(getEncoder(objectMapper, mediaTypes));
}
}
public void registerCodecs(CustomCodecs codecs) {
decoders.forEach(codecs::decoder);
encoders.forEach(codecs::encoder);
}
private static Decoder<?> getDecoder(ObjectMapper mapper, List<MediaType> mediaTypes) {
return new Jackson2JsonDecoder(mapper, mediaTypes.toArray(new MimeType[0]));
}
private static Encoder<?> getEncoder(ObjectMapper mapper, List<MediaType> mediaTypes) {
return new Jackson2JsonEncoder(mapper, mediaTypes.toArray(new MimeType[0]));
}
}
}

View File

@@ -0,0 +1,112 @@
/*
* Copyright 2019 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
*
* https://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.hateoas.config;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
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;
/**
* Value type to handle registration of hypermedia related {@link HttpMessageConverter}s.
*
* @author Oliver Drotbohm
*/
class WebMvcConverters {
private final List<HttpMessageConverter<?>> converters;
/**
* Creates a new {@link WebMvcConverters} from the given {@link ObjectMapper} and
* {@link HypermediaMappingInformation}s.
*
* @param mapper must not be {@literal null}.
* @param mappingInformation must not be {@literal null}.
*/
private WebMvcConverters(ObjectMapper mapper, List<HypermediaMappingInformation> mappingInformation) {
this.converters = mappingInformation.stream() //
.map(it -> createMessageConverter(it, it.configureObjectMapper(mapper.copy()))) //
.collect(Collectors.toList());
}
/**
* Creates a new {@link WebMvcConverters} from the given {@link ObjectMapper} and
* {@link HypermediaMappingInformation}s.
*
* @param mapper must not be {@literal null}.
* @param mappingInformations must not be {@literal null}.
* @return
*/
public static WebMvcConverters of(ObjectMapper mapper, List<HypermediaMappingInformation> mappingInformations) {
Assert.notNull(mapper, "ObjectMapper must not be null!");
Assert.notNull(mappingInformations, "Mapping information must not be null!");
return new WebMvcConverters(mapper, mappingInformations);
}
/**
* Augments the given {@link List} of {@link HttpMessageConverter}s with the hypermedia enabled ones.
*
* @param converters must not be {@literal null}.
*/
public void augment(List<HttpMessageConverter<?>> converters) {
Assert.notNull(converters, "HttpMessageConverters must not be null!");
this.converters.forEach(it -> converters.add(0, it));
}
/**
* Returns a new {@link List} of {@link HttpMessageConverter}s consisting of both the hypermedia based ones as well as
* the given ones.
*
* @param converters must not be {@literal null}.
*/
public List<HttpMessageConverter<?>> and(Collection<HttpMessageConverter<?>> converters) {
Assert.notNull(converters, "HttpMessageConverters must not be null!");
List<HttpMessageConverter<?>> result = new ArrayList<>(this.converters);
result.addAll(converters);
return result;
}
/**
* Creates a new {@link TypeConstrainedMappingJackson2HttpMessageConverter} to handle {@link RepresentationModel} for
* the given {@link HypermediaMappingInformation} using a copy of the given {@link ObjectMapper}.
*
* @param type must not be {@literal null}.
* @param mapper must not be {@literal null}.
* @return
*/
private static AbstractJackson2HttpMessageConverter createMessageConverter(HypermediaMappingInformation type,
ObjectMapper mapper) {
return new TypeConstrainedMappingJackson2HttpMessageConverter(RepresentationModel.class, type.getMediaTypes(),
type.configureObjectMapper(mapper));
}
}

View File

@@ -27,11 +27,9 @@ import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.hateoas.RepresentationModel;
import org.springframework.hateoas.server.RepresentationModelProcessor;
import org.springframework.hateoas.server.mvc.RepresentationModelProcessorHandlerMethodReturnValueHandler;
import org.springframework.hateoas.server.mvc.RepresentationModelProcessorInvoker;
import org.springframework.hateoas.server.mvc.TypeConstrainedMappingJackson2HttpMessageConverter;
import org.springframework.hateoas.server.mvc.UriComponentsContributor;
import org.springframework.hateoas.server.mvc.WebMvcLinkBuilderFactory;
import org.springframework.http.converter.HttpMessageConverter;
@@ -53,10 +51,14 @@ import com.fasterxml.jackson.databind.ObjectMapper;
class WebMvcHateoasConfiguration {
@Bean
HypermediaWebMvcConfigurer hypermediaWebMvcConfigurer(ObjectProvider<ObjectMapper> mapper,
List<HypermediaMappingInformation> hypermediaTypes) {
WebMvcConverters hypermediaWebMvcConverters(ObjectProvider<ObjectMapper> mapper,
List<HypermediaMappingInformation> information) {
return WebMvcConverters.of(mapper.getIfUnique(ObjectMapper::new), information);
}
return new HypermediaWebMvcConfigurer(mapper.getIfAvailable(ObjectMapper::new), hypermediaTypes);
@Bean
HypermediaWebMvcConfigurer hypermediaWebMvcConfigurer(WebMvcConverters converters) {
return new HypermediaWebMvcConfigurer(converters);
}
@Bean
@@ -88,8 +90,7 @@ class WebMvcHateoasConfiguration {
@RequiredArgsConstructor
static class HypermediaWebMvcConfigurer implements WebMvcConfigurer {
private final ObjectMapper mapper;
private final List<HypermediaMappingInformation> hypermediaTypes;
private final @NonNull WebMvcConverters hypermediaConverters;
/*
* (non-Javadoc)
@@ -97,12 +98,7 @@ class WebMvcHateoasConfiguration {
*/
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
this.hypermediaTypes.forEach(hypermedia -> {
converters.add(0, new TypeConstrainedMappingJackson2HttpMessageConverter(RepresentationModel.class,
hypermedia.getMediaTypes(), hypermedia.configureObjectMapper(mapper.copy())));
});
hypermediaConverters.augment(converters);
}
}