diff --git a/pom.xml b/pom.xml
index 79f4bd910..91839a953 100644
--- a/pom.xml
+++ b/pom.xml
@@ -325,6 +325,11 @@
${jmolecules-integration}
true
+
+ org.junit.platform
+ junit-platform-launcher
+ test
+
diff --git a/src/main/java/org/springframework/data/web/aot/WebRuntimeHints.java b/src/main/java/org/springframework/data/web/aot/WebRuntimeHints.java
index 1230a2201..f791ce498 100644
--- a/src/main/java/org/springframework/data/web/aot/WebRuntimeHints.java
+++ b/src/main/java/org/springframework/data/web/aot/WebRuntimeHints.java
@@ -20,6 +20,7 @@ import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.aot.hint.TypeReference;
import org.springframework.data.web.config.SpringDataJacksonConfiguration.PageModule;
import org.springframework.lang.Nullable;
+import org.springframework.util.ClassUtils;
/**
* {@link RuntimeHintsRegistrar} providing hints for web usage.
@@ -32,7 +33,9 @@ class WebRuntimeHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) {
- hints.reflection().registerType(TypeReference.of("org.springframework.data.domain.Unpaged"),
- hint -> hint.onReachableType(PageModule.class));
+ if (ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader)) {
+ hints.reflection().registerType(TypeReference.of("org.springframework.data.domain.Unpaged"),
+ hint -> hint.onReachableType(PageModule.class));
+ }
}
}
diff --git a/src/test/java/org/springframework/data/test/util/ClassPathExclusions.java b/src/test/java/org/springframework/data/test/util/ClassPathExclusions.java
new file mode 100644
index 000000000..1f903be70
--- /dev/null
+++ b/src/test/java/org/springframework/data/test/util/ClassPathExclusions.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2024 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.data.test.util;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.junit.jupiter.api.extension.ExtendWith;
+
+/**
+ * Annotation used to exclude entries from the classpath.
+ * Simplified version of ClassPathExclusions.
+ *
+ * @author Christoph Strobl
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ ElementType.TYPE, ElementType.METHOD })
+@Documented
+@ExtendWith(ClassPathExclusionsExtension.class)
+public @interface ClassPathExclusions {
+
+ /**
+ * One or more packages that should be excluded from the classpath.
+ *
+ * @return the excluded packages
+ */
+ String[] packages();
+
+}
diff --git a/src/test/java/org/springframework/data/test/util/ClassPathExclusionsExtension.java b/src/test/java/org/springframework/data/test/util/ClassPathExclusionsExtension.java
new file mode 100644
index 000000000..295ae2d24
--- /dev/null
+++ b/src/test/java/org/springframework/data/test/util/ClassPathExclusionsExtension.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2024 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.data.test.util;
+
+import java.lang.reflect.Method;
+
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.api.extension.InvocationInterceptor;
+import org.junit.jupiter.api.extension.ReflectiveInvocationContext;
+import org.junit.platform.engine.discovery.DiscoverySelectors;
+import org.junit.platform.launcher.Launcher;
+import org.junit.platform.launcher.LauncherDiscoveryRequest;
+import org.junit.platform.launcher.TestPlan;
+import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
+import org.junit.platform.launcher.core.LauncherFactory;
+import org.junit.platform.launcher.listeners.SummaryGeneratingListener;
+import org.junit.platform.launcher.listeners.TestExecutionSummary;
+import org.springframework.util.CollectionUtils;
+
+/**
+ * Simplified version of ModifiedClassPathExtension.
+ *
+ * @author Christoph Strobl
+ */
+class ClassPathExclusionsExtension implements InvocationInterceptor {
+
+ @Override
+ public void interceptBeforeAllMethod(Invocation invocation,
+ ReflectiveInvocationContext invocationContext, ExtensionContext extensionContext) throws Throwable {
+ intercept(invocation, extensionContext);
+ }
+
+ @Override
+ public void interceptBeforeEachMethod(Invocation invocation,
+ ReflectiveInvocationContext invocationContext, ExtensionContext extensionContext) throws Throwable {
+ intercept(invocation, extensionContext);
+ }
+
+ @Override
+ public void interceptAfterEachMethod(Invocation invocation,
+ ReflectiveInvocationContext invocationContext, ExtensionContext extensionContext) throws Throwable {
+ intercept(invocation, extensionContext);
+ }
+
+ @Override
+ public void interceptAfterAllMethod(Invocation invocation,
+ ReflectiveInvocationContext invocationContext, ExtensionContext extensionContext) throws Throwable {
+ intercept(invocation, extensionContext);
+ }
+
+ @Override
+ public void interceptTestMethod(Invocation invocation, ReflectiveInvocationContext invocationContext,
+ ExtensionContext extensionContext) throws Throwable {
+ interceptMethod(invocation, invocationContext, extensionContext);
+ }
+
+ @Override
+ public void interceptTestTemplateMethod(Invocation invocation,
+ ReflectiveInvocationContext invocationContext, ExtensionContext extensionContext) throws Throwable {
+ interceptMethod(invocation, invocationContext, extensionContext);
+ }
+
+ private void interceptMethod(Invocation invocation, ReflectiveInvocationContext invocationContext,
+ ExtensionContext extensionContext) throws Throwable {
+
+ if (isModifiedClassPathClassLoader(extensionContext)) {
+ invocation.proceed();
+ return;
+ }
+
+ Class> testClass = extensionContext.getRequiredTestClass();
+ Method testMethod = invocationContext.getExecutable();
+ PackageExcludingClassLoader modifiedClassLoader = PackageExcludingClassLoader.get(testClass, testMethod);
+ if (modifiedClassLoader == null) {
+ invocation.proceed();
+ return;
+ }
+ invocation.skip();
+ ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader();
+ Thread.currentThread().setContextClassLoader(modifiedClassLoader);
+ try {
+ runTest(extensionContext.getUniqueId());
+ } finally {
+ Thread.currentThread().setContextClassLoader(originalClassLoader);
+ }
+ }
+
+ private void runTest(String testId) throws Throwable {
+
+ LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request()
+ .selectors(DiscoverySelectors.selectUniqueId(testId)).build();
+ Launcher launcher = LauncherFactory.create();
+ TestPlan testPlan = launcher.discover(request);
+ SummaryGeneratingListener listener = new SummaryGeneratingListener();
+ launcher.registerTestExecutionListeners(listener);
+ launcher.execute(testPlan);
+ TestExecutionSummary summary = listener.getSummary();
+ if (!CollectionUtils.isEmpty(summary.getFailures())) {
+ throw summary.getFailures().get(0).getException();
+ }
+ }
+
+ private void intercept(Invocation invocation, ExtensionContext extensionContext) throws Throwable {
+ if (isModifiedClassPathClassLoader(extensionContext)) {
+ invocation.proceed();
+ return;
+ }
+ invocation.skip();
+ }
+
+ private boolean isModifiedClassPathClassLoader(ExtensionContext extensionContext) {
+ Class> testClass = extensionContext.getRequiredTestClass();
+ ClassLoader classLoader = testClass.getClassLoader();
+ return classLoader.getClass().getName().equals(PackageExcludingClassLoader.class.getName());
+ }
+}
diff --git a/src/test/java/org/springframework/data/test/util/PackageExcludingClassLoader.java b/src/test/java/org/springframework/data/test/util/PackageExcludingClassLoader.java
new file mode 100644
index 000000000..b389354b3
--- /dev/null
+++ b/src/test/java/org/springframework/data/test/util/PackageExcludingClassLoader.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2024 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.data.test.util;
+
+import java.io.File;
+import java.lang.management.ManagementFactory;
+import java.lang.reflect.Method;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.BiConsumer;
+import java.util.function.BinaryOperator;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.stream.Collector;
+import java.util.stream.Stream;
+
+import org.springframework.core.annotation.AnnotatedElementUtils;
+import org.springframework.util.ClassUtils;
+
+/**
+ * Simplified version of ModifiedClassPathClassLoader.
+ *
+ * @author Christoph Strobl
+ */
+class PackageExcludingClassLoader extends URLClassLoader {
+
+ private final Set excludedPackages;
+ private final ClassLoader junitLoader;
+
+ PackageExcludingClassLoader(URL[] urls, ClassLoader parent, Collection excludedPackages,
+ ClassLoader junitClassLoader) {
+
+ super(urls, parent);
+ this.excludedPackages = Set.copyOf(excludedPackages);
+ this.junitLoader = junitClassLoader;
+ }
+
+ @Override
+ public Class> loadClass(String name) throws ClassNotFoundException {
+
+ if (name.startsWith("org.junit") || name.startsWith("org.hamcrest")) {
+ return Class.forName(name, false, this.junitLoader);
+ }
+
+ String packageName = ClassUtils.getPackageName(name);
+ if (this.excludedPackages.contains(packageName)) {
+ throw new ClassNotFoundException(name);
+ }
+ return super.loadClass(name);
+ }
+
+ static PackageExcludingClassLoader get(Class> testClass, Method testMethod) {
+
+ List excludedPackages = readExcludedPackages(testClass, testMethod);
+
+ if (excludedPackages.isEmpty()) {
+ return null;
+ }
+
+ ClassLoader testClassClassLoader = testClass.getClassLoader();
+ Stream urls = null;
+ if (testClassClassLoader instanceof URLClassLoader urlClassLoader) {
+ urls = Stream.of(urlClassLoader.getURLs());
+ } else {
+ urls = Stream.of(ManagementFactory.getRuntimeMXBean().getClassPath().split(File.pathSeparator))
+ .map(PackageExcludingClassLoader::toURL);
+ }
+
+ return new PackageExcludingClassLoader(urls.toArray(URL[]::new), testClassClassLoader.getParent(), excludedPackages,
+ testClassClassLoader);
+ }
+
+ private static List readExcludedPackages(Class> testClass, Method testMethod) {
+
+ return Stream.of( //
+ AnnotatedElementUtils.findMergedAnnotation(testClass, ClassPathExclusions.class),
+ AnnotatedElementUtils.findMergedAnnotation(testMethod, ClassPathExclusions.class) //
+ ).filter(Objects::nonNull) //
+ .map(ClassPathExclusions::packages) //
+ .collect(new CombingArrayCollector());
+ }
+
+ private static URL toURL(String entry) {
+ try {
+ return new File(entry).toURI().toURL();
+ } catch (Exception ex) {
+ throw new IllegalArgumentException(ex);
+ }
+ }
+
+ private static class CombingArrayCollector implements Collector, List> {
+
+ @Override
+ public Supplier> supplier() {
+ return ArrayList::new;
+ }
+
+ @Override
+ public BiConsumer, T[]> accumulator() {
+ return (target, values) -> target.addAll(Arrays.asList(values));
+ }
+
+ @Override
+ public BinaryOperator> combiner() {
+ return (r1, r2) -> {
+ r1.addAll(r2);
+ return r1;
+ };
+ }
+
+ @Override
+ public Function, List> finisher() {
+ return i -> (List) i;
+ }
+
+ @Override
+ public Set characteristics() {
+ return Collections.unmodifiableSet(EnumSet.of(Characteristics.IDENTITY_FINISH));
+ }
+ }
+}
diff --git a/src/test/java/org/springframework/data/web/aot/WebRuntimeHintsUnitTests.java b/src/test/java/org/springframework/data/web/aot/WebRuntimeHintsUnitTests.java
new file mode 100644
index 000000000..4cb0ffc37
--- /dev/null
+++ b/src/test/java/org/springframework/data/web/aot/WebRuntimeHintsUnitTests.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2024 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.data.web.aot;
+
+import static org.assertj.core.api.Assertions.*;
+
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+import org.springframework.aot.hint.ReflectionHints;
+import org.springframework.aot.hint.RuntimeHints;
+import org.springframework.aot.hint.TypeReference;
+import org.springframework.aot.hint.predicate.RuntimeHintsPredicates;
+import org.springframework.data.test.util.ClassPathExclusions;
+
+/**
+ * @author Christoph Strobl
+ */
+class WebRuntimeHintsUnitTests {
+
+ @Test // GH-3033
+ void shouldRegisterRuntimeHintWhenJacksonPresent() {
+
+ ReflectionHints reflectionHints = new ReflectionHints();
+ RuntimeHints runtimeHints = Mockito.mock(RuntimeHints.class);
+ Mockito.when(runtimeHints.reflection()).thenReturn(reflectionHints);
+
+ new WebRuntimeHints().registerHints(runtimeHints, this.getClass().getClassLoader());
+
+ assertThat(runtimeHints).matches(
+ RuntimeHintsPredicates.reflection().onType(TypeReference.of("org.springframework.data.domain.Unpaged")));
+ }
+
+ @Test // GH-3033
+ @ClassPathExclusions(packages = { "com.fasterxml.jackson.databind" })
+ void shouldRegisterRuntimeHintWithTypeNameWhenJacksonNotPresent() {
+
+ ReflectionHints reflectionHints = new ReflectionHints();
+ RuntimeHints runtimeHints = Mockito.mock(RuntimeHints.class);
+ Mockito.when(runtimeHints.reflection()).thenReturn(reflectionHints);
+
+ new WebRuntimeHints().registerHints(runtimeHints, this.getClass().getClassLoader());
+
+ assertThat(runtimeHints.reflection().typeHints()).isEmpty();
+ }
+}
diff --git a/src/test/java/org/springframework/data/web/config/EnableSpringDataWebSupportIntegrationTests.java b/src/test/java/org/springframework/data/web/config/EnableSpringDataWebSupportIntegrationTests.java
index 98b8f22bb..bf2087beb 100755
--- a/src/test/java/org/springframework/data/web/config/EnableSpringDataWebSupportIntegrationTests.java
+++ b/src/test/java/org/springframework/data/web/config/EnableSpringDataWebSupportIntegrationTests.java
@@ -26,27 +26,24 @@ import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.ConversionService;
-import org.springframework.data.classloadersupport.HidingClassLoader;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.Point;
import org.springframework.data.querydsl.EntityPathResolver;
import org.springframework.data.querydsl.SimpleEntityPathResolver;
import org.springframework.data.querydsl.binding.QuerydslBindingsFactory;
+import org.springframework.data.test.util.ClassPathExclusions;
import org.springframework.data.web.OffsetScrollPositionHandlerMethodArgumentResolver;
import org.springframework.data.web.PageableHandlerMethodArgumentResolver;
import org.springframework.data.web.PagedResourcesAssemblerArgumentResolver;
import org.springframework.data.web.ProxyingHandlerMethodArgumentResolver;
import org.springframework.data.web.SortHandlerMethodArgumentResolver;
import org.springframework.data.web.WebTestUtils;
-import org.springframework.hateoas.Link;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
import org.springframework.web.util.UriComponentsBuilder;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
/**
* Integration tests for {@link EnableSpringDataWebSupport}.
*
@@ -54,6 +51,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
* @author Jens Schauder
* @author Vedran Pavic
* @author Yanming Zhou
+ * @author Christoph Strobl
*/
class EnableSpringDataWebSupportIntegrationTests {
@@ -136,11 +134,10 @@ class EnableSpringDataWebSupportIntegrationTests {
}
@Test // DATACMNS-330
+ @ClassPathExclusions(packages = { "org.springframework.hateoas" })
void doesNotRegisterHateoasSpecificComponentsIfHateoasNotPresent() throws Exception {
- var classLoader = HidingClassLoader.hide(Link.class);
-
- ApplicationContext context = WebTestUtils.createApplicationContext(classLoader, SampleConfig.class);
+ ApplicationContext context = WebTestUtils.createApplicationContext(SampleConfig.class);
var names = Arrays.asList(context.getBeanDefinitionNames());
@@ -158,10 +155,10 @@ class EnableSpringDataWebSupportIntegrationTests {
}
@Test // DATACMNS-475
+ @ClassPathExclusions(packages = { "com.fasterxml.jackson.databind" })
void doesNotRegisterJacksonSpecificComponentsIfJacksonNotPresent() throws Exception {
- ApplicationContext context = WebTestUtils.createApplicationContext(HidingClassLoader.hide(ObjectMapper.class),
- SampleConfig.class);
+ ApplicationContext context = WebTestUtils.createApplicationContext(SampleConfig.class);
var names = Arrays.asList(context.getBeanDefinitionNames());