From de38b9e200b08505724a41d4bc5f926fd0aa0c15 Mon Sep 17 00:00:00 2001 From: Greg Turnquist Date: Thu, 2 Apr 2020 11:13:48 -0500 Subject: [PATCH] #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. --- ...HypermediaConfigurationImportSelector.java | 34 +++++- .../config/HypermediaWebClientConfigurer.java | 2 +- .../config/WebClientHateoasConfiguration.java | 8 -- .../config/WebTestHateoasConfiguration.java | 43 ++++++++ ...iaConfigurationImportSelectorUnitTest.java | 31 +++++- .../hateoas/support/ContextTester.java | 20 +++- .../hateoas/support/HidingClassLoader.java | 101 ++++++++++++++++++ 7 files changed, 220 insertions(+), 19 deletions(-) create mode 100644 src/main/java/org/springframework/hateoas/config/WebTestHateoasConfiguration.java create mode 100644 src/test/java/org/springframework/hateoas/support/HidingClassLoader.java diff --git a/src/main/java/org/springframework/hateoas/config/HypermediaConfigurationImportSelector.java b/src/main/java/org/springframework/hateoas/config/HypermediaConfigurationImportSelector.java index e7df7318..d868296d 100644 --- a/src/main/java/org/springframework/hateoas/config/HypermediaConfigurationImportSelector.java +++ b/src/main/java/org/springframework/hateoas/config/HypermediaConfigurationImportSelector.java @@ -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 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 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) { diff --git a/src/main/java/org/springframework/hateoas/config/HypermediaWebClientConfigurer.java b/src/main/java/org/springframework/hateoas/config/HypermediaWebClientConfigurer.java index 6c56b5c5..90bf557c 100644 --- a/src/main/java/org/springframework/hateoas/config/HypermediaWebClientConfigurer.java +++ b/src/main/java/org/springframework/hateoas/config/HypermediaWebClientConfigurer.java @@ -37,7 +37,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; */ public class HypermediaWebClientConfigurer { - Consumer configurer; + final Consumer configurer; /** * Creates a new {@link HypermediaWebClientConfigurer} for the given {@link ObjectMapper} and diff --git a/src/main/java/org/springframework/hateoas/config/WebClientHateoasConfiguration.java b/src/main/java/org/springframework/hateoas/config/WebClientHateoasConfiguration.java index 59aa366e..b5017073 100644 --- a/src/main/java/org/springframework/hateoas/config/WebClientHateoasConfiguration.java +++ b/src/main/java/org/springframework/hateoas/config/WebClientHateoasConfiguration.java @@ -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 mapper, - List hypermediaTypes) { - return new HypermediaWebTestClientConfigurer(mapper.getIfAvailable(ObjectMapper::new), hypermediaTypes); - } - @Bean static HypermediaWebClientBeanPostProcessor webClientBeanPostProcessor(HypermediaWebClientConfigurer configurer) { return new HypermediaWebClientBeanPostProcessor(configurer); diff --git a/src/main/java/org/springframework/hateoas/config/WebTestHateoasConfiguration.java b/src/main/java/org/springframework/hateoas/config/WebTestHateoasConfiguration.java new file mode 100644 index 00000000..19d7234e --- /dev/null +++ b/src/main/java/org/springframework/hateoas/config/WebTestHateoasConfiguration.java @@ -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 mapper, + List hypermediaTypes) { + return new HypermediaWebTestClientConfigurer(mapper.getIfAvailable(ObjectMapper::new), hypermediaTypes); + } +} diff --git a/src/test/java/org/springframework/hateoas/config/HypermediaConfigurationImportSelectorUnitTest.java b/src/test/java/org/springframework/hateoas/config/HypermediaConfigurationImportSelectorUnitTest.java index c970208f..0a38d0a5 100644 --- a/src/test/java/org/springframework/hateoas/config/HypermediaConfigurationImportSelectorUnitTest.java +++ b/src/test/java/org/springframework/hateoas/config/HypermediaConfigurationImportSelectorUnitTest.java @@ -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 { diff --git a/src/test/java/org/springframework/hateoas/support/ContextTester.java b/src/test/java/org/springframework/hateoas/support/ContextTester.java index acf1027a..04e8db83 100644 --- a/src/test/java/org/springframework/hateoas/support/ContextTester.java +++ b/src/test/java/org/springframework/hateoas/support/ContextTester.java @@ -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 void withContext(Class configuration, - ConsumerWithException consumer) throws E { + public static void withContext(Class configuration, // + ConsumerWithException 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 void withContext(Class configuration, + ConsumerWithException consumer) throws E { + withContext(configuration, consumer, null); + } + public static void withServletContext(Class configuration, ConsumerWithException consumer) throws E { diff --git a/src/test/java/org/springframework/hateoas/support/HidingClassLoader.java b/src/test/java/org/springframework/hateoas/support/HidingClassLoader.java new file mode 100644 index 00000000..42b24b7b --- /dev/null +++ b/src/test/java/org/springframework/hateoas/support/HidingClassLoader.java @@ -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: + *
    + *
  • shadowed: reloaded by this classloader no matter if they are loaded already by the SystemClassLoader
  • + *
  • 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}
  • + *
  • all other classes get loaded by the SystemClassLoader
  • + *
+ * + * @author Jens Schauder + * @author Oliver Gierke + */ +public class HidingClassLoader extends ShadowingClassLoader { + + private final Collection hidden; + + HidingClassLoader(Collection 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(); + } + } +}