#1252 - Make sure HypermediaWebTestClientConfigurer is only included with spring-test on the classpath.

HypermediaConfigurationImportSelector is now ResourceLoaderAware to make sure it uses the class loader used to bootstrap the application context. This is important for both testing as well as custom classloader arrangements like Boot's DevTools. We now only include WebTestHateoasConfiguration if WebTestClient is on the classpath.

The configuration class has been extracted to not leak references to code tied to spring-test into production deployments. Added test cases to check both the inclusion and exclusion of the configuration classes based on the classpath arrangement.

Original pull request: #1251.
This commit is contained in:
Greg Turnquist
2020-04-02 11:13:48 -05:00
committed by Oliver Drotbohm
parent 599a366c85
commit de38b9e200
7 changed files with 220 additions and 19 deletions

View File

@@ -20,26 +20,44 @@ import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType;
import org.springframework.http.MediaType;
import org.springframework.util.ClassUtils;
/**
* {@link ImportSelector} that looks up configuration classes from all {@link MediaTypeConfigurationProvider}
* implementations listed in {@code META-INF/spring.factories}.
*
* @author Oliver Drotbohm
* @author Greg Turnquist
*/
class HypermediaConfigurationImportSelector implements ImportSelector {
class HypermediaConfigurationImportSelector implements ImportSelector, ResourceLoaderAware {
public static final String SPRING_TEST = "org.springframework.test.web.reactive.server.WebTestClient";
private ResourceLoader resourceLoader;
/*
* (non-Javadoc)
* @see org.springframework.context.annotation.ImportSelector#selectImports(org.springframework.core.type.AnnotationMetadata)
* @see org.springframework.context.ResourceLoaderAware#setResourceLoader(org.springframework.core.io.ResourceLoader)
*/
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
/*
* (non-Javadoc)
* @see org.springframework.context.annotation.ImportSelector#selectImports(org.springframework.core.type.AnnotationMetadata)
*/
@Override
public String[] selectImports(AnnotationMetadata metadata) {
Map<String, Object> attributes = metadata.getAnnotationAttributes(EnableHypermediaSupport.class.getName());
@@ -54,11 +72,17 @@ class HypermediaConfigurationImportSelector implements ImportSelector {
MediaTypeConfigurationProvider.class, HypermediaConfigurationImportSelector.class.getClassLoader());
// Filter the ones supporting the given media types
return configurationProviders.stream() //
Stream<String> imports = configurationProviders.stream() //
.filter(it -> it.supportsAny(types)) //
.map(MediaTypeConfigurationProvider::getConfiguration) //
.map(Class::getName) //
.toArray(String[]::new);
.map(Class::getName);
// Conditionally apply other configurations
if (ClassUtils.isPresent(SPRING_TEST, resourceLoader.getClassLoader())) {
imports = Stream.concat(imports, Stream.of(WebTestHateoasConfiguration.class.getName()));
}
return imports.toArray(String[]::new);
}
public String[] selectImports(List<MediaType> mediaType) {

View File

@@ -37,7 +37,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
*/
public class HypermediaWebClientConfigurer {
Consumer<ClientCodecConfigurer> configurer;
final Consumer<ClientCodecConfigurer> configurer;
/**
* Creates a new {@link HypermediaWebClientConfigurer} for the given {@link ObjectMapper} and

View File

@@ -24,7 +24,6 @@ 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.context.annotation.Lazy;
import org.springframework.lang.NonNull;
import org.springframework.web.reactive.function.client.WebClient;
@@ -45,13 +44,6 @@ class WebClientHateoasConfiguration {
return new HypermediaWebClientConfigurer(mapper.getIfAvailable(ObjectMapper::new), hypermediaTypes);
}
@Bean
@Lazy
HypermediaWebTestClientConfigurer webTestClientConfigurer(ObjectProvider<ObjectMapper> mapper,
List<HypermediaMappingInformation> hypermediaTypes) {
return new HypermediaWebTestClientConfigurer(mapper.getIfAvailable(ObjectMapper::new), hypermediaTypes);
}
@Bean
static HypermediaWebClientBeanPostProcessor webClientBeanPostProcessor(HypermediaWebClientConfigurer configurer) {
return new HypermediaWebClientBeanPostProcessor(configurer);

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2020 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.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 com.fasterxml.jackson.databind.ObjectMapper;
/**
* Configuration for Spring TEST-specific things
*
* @author Greg Turnquist
* @author Oliver Drotbohm
* @since 1.1
*/
@Configuration(proxyBeanMethods = false)
class WebTestHateoasConfiguration {
@Bean
@Lazy
HypermediaWebTestClientConfigurer webTestClientConfigurer(ObjectProvider<ObjectMapper> mapper,
List<HypermediaMappingInformation> hypermediaTypes) {
return new HypermediaWebTestClientConfigurer(mapper.getIfAvailable(ObjectMapper::new), hypermediaTypes);
}
}

View File

@@ -15,18 +15,24 @@
*/
package org.springframework.hateoas.config;
import static org.assertj.core.api.AssertionsForInterfaceTypes.*;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
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 static org.springframework.hateoas.support.ContextTester.*;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.hateoas.client.LinkDiscoverer;
import org.springframework.hateoas.mediatype.collectionjson.CollectionJsonLinkDiscoverer;
import org.springframework.hateoas.mediatype.hal.HalLinkDiscoverer;
import org.springframework.hateoas.mediatype.hal.forms.HalFormsLinkDiscoverer;
import org.springframework.hateoas.mediatype.uber.UberLinkDiscoverer;
import org.springframework.hateoas.support.HidingClassLoader;
import org.springframework.instrument.classloading.ShadowingClassLoader;
import org.springframework.test.web.reactive.server.WebTestClientConfigurer;
/**
* Unit tests for {@link HypermediaConfigurationImportSelector}.
@@ -89,6 +95,29 @@ class HypermediaConfigurationImportSelectorUnitTest {
});
}
@Test // #1252
void includesWebTestHateoasConfigurationIfWebTestClientIsOnTheClasspath() {
withContext(HalConfig.class, context -> {
assertThat(context.getBean(WebTestHateoasConfiguration.class)).isNotNull();
});
}
@Test // #1252
void doesNotIncludeWebTestHateoasConfigurationIfWebTestClientIsNotOnTheClasspath() {
HidingClassLoader delegate = HidingClassLoader.hide(WebTestClientConfigurer.class);
ShadowingClassLoader loader = new ShadowingClassLoader(delegate);
loader.excludePackage("org.springframework");
withContext(HalConfig.class, context -> {
assertThatExceptionOfType(NoSuchBeanDefinitionException.class)
.isThrownBy(() -> context.getBean(WebTestHateoasConfiguration.class));
}, loader);
}
@EnableHypermediaSupport(type = HAL)
static class HalConfig {

View File

@@ -15,21 +15,28 @@
*/
package org.springframework.hateoas.support;
import java.util.function.Function;
import org.springframework.lang.Nullable;
import org.springframework.mock.web.MockServletContext;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import java.util.function.Function;
/**
* @author Greg Turnquist
* @author Oliver Drotbohm
*/
public class ContextTester {
public static <E extends Exception> void withContext(Class<?> configuration,
ConsumerWithException<AnnotationConfigWebApplicationContext, E> consumer) throws E {
public static <E extends Exception> void withContext(Class<?> configuration, //
ConsumerWithException<AnnotationConfigWebApplicationContext, E> consumer, //
@Nullable ClassLoader classLoader) throws E {
try (AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext()) {
if (classLoader != null) {
context.setClassLoader(classLoader);
}
context.register(configuration);
context.refresh();
@@ -37,6 +44,11 @@ public class ContextTester {
}
}
public static <E extends Exception> void withContext(Class<?> configuration,
ConsumerWithException<AnnotationConfigWebApplicationContext, E> consumer) throws E {
withContext(configuration, consumer, null);
}
public static <E extends Exception> void withServletContext(Class<?> configuration,
ConsumerWithException<AnnotationConfigWebApplicationContext, E> consumer) throws E {

View File

@@ -0,0 +1,101 @@
/*
* Copyright 2017-2020 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.support;
import java.net.URLClassLoader;
import java.util.Arrays;
import java.util.Collection;
import java.util.stream.Collectors;
import org.springframework.instrument.classloading.ShadowingClassLoader;
import org.springframework.util.Assert;
/**
* is intended for testing code that depends on the presence/absence of certain classes. Classes can be:
* <ul>
* <li>shadowed: reloaded by this classloader no matter if they are loaded already by the SystemClassLoader</li>
* <li>hidden: not loaded by this classloader no matter if they are loaded already by the SystemClassLoader. Trying to
* load these classes results in a {@link ClassNotFoundException}</li>
* <li>all other classes get loaded by the SystemClassLoader</li>
* </ul>
*
* @author Jens Schauder
* @author Oliver Gierke
*/
public class HidingClassLoader extends ShadowingClassLoader {
private final Collection<String> hidden;
HidingClassLoader(Collection<String> hidden) {
super(URLClassLoader.getSystemClassLoader(), false);
this.hidden = hidden;
}
/**
* Creates a new {@link HidingClassLoader} with the packages of the given classes hidden.
*
* @param packages must not be {@literal null}.
* @return
*/
public static HidingClassLoader hide(Class<?>... packages) {
Assert.notNull(packages, "Packages must not be null!");
return new HidingClassLoader(Arrays.stream(packages)//
.map(it -> it.getPackage().getName())//
.collect(Collectors.toList()));
}
/*
* (non-Javadoc)
* @see org.springframework.data.classloadersupport.ShadowingClassLoader#loadClass(java.lang.String)
*/
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
checkIfHidden(name);
return super.loadClass(name);
}
/*
* (non-Javadoc)
* @see org.springframework.data.classloadersupport.ShadowingClassLoader#isEligibleForShadowing(java.lang.String)
*/
@Override
protected boolean isEligibleForShadowing(String className) {
return isExcluded(className);
}
/*
* (non-Javadoc)
* @see java.lang.ClassLoader#findClass(java.lang.String)
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
checkIfHidden(name);
return super.findClass(name);
}
private void checkIfHidden(String name) throws ClassNotFoundException {
if (hidden.stream().anyMatch(it -> name.startsWith(it))) {
throw new ClassNotFoundException();
}
}
}