Skip to content

Commit 18386c8

Browse files
christophstroblmp911de
authored andcommitted
Guard AOT registration of PageModule.
This commit makes sure to only register runtime hints for PageModule if Jackson is present. Use newly introduced ClassPathExclusions instead of manually creating the ClassLoader. Closes #3033 Original pull request: #3034
1 parent 36d6a78 commit 18386c8

File tree

7 files changed

+390
-10
lines changed

7 files changed

+390
-10
lines changed

pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,11 @@
325325
<version>${jmolecules-integration}</version>
326326
<optional>true</optional>
327327
</dependency>
328+
<dependency>
329+
<groupId>org.junit.platform</groupId>
330+
<artifactId>junit-platform-launcher</artifactId>
331+
<scope>test</scope>
332+
</dependency>
328333

329334
</dependencies>
330335

src/main/java/org/springframework/data/web/aot/WebRuntimeHints.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import org.springframework.aot.hint.TypeReference;
2121
import org.springframework.data.web.config.SpringDataJacksonConfiguration.PageModule;
2222
import org.springframework.lang.Nullable;
23+
import org.springframework.util.ClassUtils;
2324

2425
/**
2526
* {@link RuntimeHintsRegistrar} providing hints for web usage.
@@ -32,7 +33,9 @@ class WebRuntimeHints implements RuntimeHintsRegistrar {
3233
@Override
3334
public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) {
3435

35-
hints.reflection().registerType(TypeReference.of("org.springframework.data.domain.Unpaged"),
36-
hint -> hint.onReachableType(PageModule.class));
36+
if (ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader)) {
37+
hints.reflection().registerType(TypeReference.of("org.springframework.data.domain.Unpaged"),
38+
hint -> hint.onReachableType(PageModule.class));
39+
}
3740
}
3841
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Copyright 2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.test.util;
17+
18+
import java.lang.annotation.Documented;
19+
import java.lang.annotation.ElementType;
20+
import java.lang.annotation.Retention;
21+
import java.lang.annotation.RetentionPolicy;
22+
import java.lang.annotation.Target;
23+
24+
import org.junit.jupiter.api.extension.ExtendWith;
25+
26+
/**
27+
* Annotation used to exclude entries from the classpath.
28+
* Simplified version of <a href="https://github.com/spring-projects/spring-boot/blob/main/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/ClassPathExclusions.java">ClassPathExclusions</a>.
29+
*
30+
* @author Christoph Strobl
31+
*/
32+
@Retention(RetentionPolicy.RUNTIME)
33+
@Target({ ElementType.TYPE, ElementType.METHOD })
34+
@Documented
35+
@ExtendWith(ClassPathExclusionsExtension.class)
36+
public @interface ClassPathExclusions {
37+
38+
/**
39+
* One or more packages that should be excluded from the classpath.
40+
*
41+
* @return the excluded packages
42+
*/
43+
String[] packages();
44+
45+
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/*
2+
* Copyright 2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.test.util;
17+
18+
import java.lang.reflect.Method;
19+
20+
import org.junit.jupiter.api.extension.ExtensionContext;
21+
import org.junit.jupiter.api.extension.InvocationInterceptor;
22+
import org.junit.jupiter.api.extension.ReflectiveInvocationContext;
23+
import org.junit.platform.engine.discovery.DiscoverySelectors;
24+
import org.junit.platform.launcher.Launcher;
25+
import org.junit.platform.launcher.LauncherDiscoveryRequest;
26+
import org.junit.platform.launcher.TestPlan;
27+
import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
28+
import org.junit.platform.launcher.core.LauncherFactory;
29+
import org.junit.platform.launcher.listeners.SummaryGeneratingListener;
30+
import org.junit.platform.launcher.listeners.TestExecutionSummary;
31+
import org.springframework.util.CollectionUtils;
32+
33+
/**
34+
* Simplified version of <a href="https://github.com/spring-projects/spring-boot/blob/main/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/ModifiedClassPathClassLoader.java">ModifiedClassPathExtension</a>.
35+
*
36+
* @author Christoph Strobl
37+
*/
38+
class ClassPathExclusionsExtension implements InvocationInterceptor {
39+
40+
@Override
41+
public void interceptBeforeAllMethod(Invocation<Void> invocation,
42+
ReflectiveInvocationContext<Method> invocationContext, ExtensionContext extensionContext) throws Throwable {
43+
intercept(invocation, extensionContext);
44+
}
45+
46+
@Override
47+
public void interceptBeforeEachMethod(Invocation<Void> invocation,
48+
ReflectiveInvocationContext<Method> invocationContext, ExtensionContext extensionContext) throws Throwable {
49+
intercept(invocation, extensionContext);
50+
}
51+
52+
@Override
53+
public void interceptAfterEachMethod(Invocation<Void> invocation,
54+
ReflectiveInvocationContext<Method> invocationContext, ExtensionContext extensionContext) throws Throwable {
55+
intercept(invocation, extensionContext);
56+
}
57+
58+
@Override
59+
public void interceptAfterAllMethod(Invocation<Void> invocation,
60+
ReflectiveInvocationContext<Method> invocationContext, ExtensionContext extensionContext) throws Throwable {
61+
intercept(invocation, extensionContext);
62+
}
63+
64+
@Override
65+
public void interceptTestMethod(Invocation<Void> invocation, ReflectiveInvocationContext<Method> invocationContext,
66+
ExtensionContext extensionContext) throws Throwable {
67+
interceptMethod(invocation, invocationContext, extensionContext);
68+
}
69+
70+
@Override
71+
public void interceptTestTemplateMethod(Invocation<Void> invocation,
72+
ReflectiveInvocationContext<Method> invocationContext, ExtensionContext extensionContext) throws Throwable {
73+
interceptMethod(invocation, invocationContext, extensionContext);
74+
}
75+
76+
private void interceptMethod(Invocation<Void> invocation, ReflectiveInvocationContext<Method> invocationContext,
77+
ExtensionContext extensionContext) throws Throwable {
78+
79+
if (isModifiedClassPathClassLoader(extensionContext)) {
80+
invocation.proceed();
81+
return;
82+
}
83+
84+
Class<?> testClass = extensionContext.getRequiredTestClass();
85+
Method testMethod = invocationContext.getExecutable();
86+
PackageExcludingClassLoader modifiedClassLoader = PackageExcludingClassLoader.get(testClass, testMethod);
87+
if (modifiedClassLoader == null) {
88+
invocation.proceed();
89+
return;
90+
}
91+
invocation.skip();
92+
ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader();
93+
Thread.currentThread().setContextClassLoader(modifiedClassLoader);
94+
try {
95+
runTest(extensionContext.getUniqueId());
96+
} finally {
97+
Thread.currentThread().setContextClassLoader(originalClassLoader);
98+
}
99+
}
100+
101+
private void runTest(String testId) throws Throwable {
102+
103+
LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request()
104+
.selectors(DiscoverySelectors.selectUniqueId(testId)).build();
105+
Launcher launcher = LauncherFactory.create();
106+
TestPlan testPlan = launcher.discover(request);
107+
SummaryGeneratingListener listener = new SummaryGeneratingListener();
108+
launcher.registerTestExecutionListeners(listener);
109+
launcher.execute(testPlan);
110+
TestExecutionSummary summary = listener.getSummary();
111+
if (!CollectionUtils.isEmpty(summary.getFailures())) {
112+
throw summary.getFailures().get(0).getException();
113+
}
114+
}
115+
116+
private void intercept(Invocation<Void> invocation, ExtensionContext extensionContext) throws Throwable {
117+
if (isModifiedClassPathClassLoader(extensionContext)) {
118+
invocation.proceed();
119+
return;
120+
}
121+
invocation.skip();
122+
}
123+
124+
private boolean isModifiedClassPathClassLoader(ExtensionContext extensionContext) {
125+
Class<?> testClass = extensionContext.getRequiredTestClass();
126+
ClassLoader classLoader = testClass.getClassLoader();
127+
return classLoader.getClass().getName().equals(PackageExcludingClassLoader.class.getName());
128+
}
129+
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/*
2+
* Copyright 2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.test.util;
17+
18+
import java.io.File;
19+
import java.lang.management.ManagementFactory;
20+
import java.lang.reflect.Method;
21+
import java.net.URL;
22+
import java.net.URLClassLoader;
23+
import java.util.ArrayList;
24+
import java.util.Arrays;
25+
import java.util.Collection;
26+
import java.util.Collections;
27+
import java.util.EnumSet;
28+
import java.util.List;
29+
import java.util.Objects;
30+
import java.util.Set;
31+
import java.util.function.BiConsumer;
32+
import java.util.function.BinaryOperator;
33+
import java.util.function.Function;
34+
import java.util.function.Supplier;
35+
import java.util.stream.Collector;
36+
import java.util.stream.Stream;
37+
38+
import org.springframework.core.annotation.AnnotatedElementUtils;
39+
import org.springframework.util.ClassUtils;
40+
41+
/**
42+
* Simplified version of <a href="https://github.com/spring-projects/spring-boot/blob/main/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/ModifiedClassPathClassLoader.java">ModifiedClassPathClassLoader</a>.
43+
*
44+
* @author Christoph Strobl
45+
*/
46+
class PackageExcludingClassLoader extends URLClassLoader {
47+
48+
private final Set<String> excludedPackages;
49+
private final ClassLoader junitLoader;
50+
51+
PackageExcludingClassLoader(URL[] urls, ClassLoader parent, Collection<String> excludedPackages,
52+
ClassLoader junitClassLoader) {
53+
54+
super(urls, parent);
55+
this.excludedPackages = Set.copyOf(excludedPackages);
56+
this.junitLoader = junitClassLoader;
57+
}
58+
59+
@Override
60+
public Class<?> loadClass(String name) throws ClassNotFoundException {
61+
62+
if (name.startsWith("org.junit") || name.startsWith("org.hamcrest")) {
63+
return Class.forName(name, false, this.junitLoader);
64+
}
65+
66+
String packageName = ClassUtils.getPackageName(name);
67+
if (this.excludedPackages.contains(packageName)) {
68+
throw new ClassNotFoundException(name);
69+
}
70+
return super.loadClass(name);
71+
}
72+
73+
static PackageExcludingClassLoader get(Class<?> testClass, Method testMethod) {
74+
75+
List<String> excludedPackages = readExcludedPackages(testClass, testMethod);
76+
77+
if (excludedPackages.isEmpty()) {
78+
return null;
79+
}
80+
81+
ClassLoader testClassClassLoader = testClass.getClassLoader();
82+
Stream<URL> urls = null;
83+
if (testClassClassLoader instanceof URLClassLoader urlClassLoader) {
84+
urls = Stream.of(urlClassLoader.getURLs());
85+
} else {
86+
urls = Stream.of(ManagementFactory.getRuntimeMXBean().getClassPath().split(File.pathSeparator))
87+
.map(PackageExcludingClassLoader::toURL);
88+
}
89+
90+
return new PackageExcludingClassLoader(urls.toArray(URL[]::new), testClassClassLoader.getParent(), excludedPackages,
91+
testClassClassLoader);
92+
}
93+
94+
private static List<String> readExcludedPackages(Class<?> testClass, Method testMethod) {
95+
96+
return Stream.of( //
97+
AnnotatedElementUtils.findMergedAnnotation(testClass, ClassPathExclusions.class),
98+
AnnotatedElementUtils.findMergedAnnotation(testMethod, ClassPathExclusions.class) //
99+
).filter(Objects::nonNull) //
100+
.map(ClassPathExclusions::packages) //
101+
.collect(new CombingArrayCollector<String>());
102+
}
103+
104+
private static URL toURL(String entry) {
105+
try {
106+
return new File(entry).toURI().toURL();
107+
} catch (Exception ex) {
108+
throw new IllegalArgumentException(ex);
109+
}
110+
}
111+
112+
private static class CombingArrayCollector<T> implements Collector<T[], List<T>, List<T>> {
113+
114+
@Override
115+
public Supplier<List<T>> supplier() {
116+
return ArrayList::new;
117+
}
118+
119+
@Override
120+
public BiConsumer<List<T>, T[]> accumulator() {
121+
return (target, values) -> target.addAll(Arrays.asList(values));
122+
}
123+
124+
@Override
125+
public BinaryOperator<List<T>> combiner() {
126+
return (r1, r2) -> {
127+
r1.addAll(r2);
128+
return r1;
129+
};
130+
}
131+
132+
@Override
133+
public Function<List<T>, List<T>> finisher() {
134+
return i -> (List<T>) i;
135+
}
136+
137+
@Override
138+
public Set<Characteristics> characteristics() {
139+
return Collections.unmodifiableSet(EnumSet.of(Characteristics.IDENTITY_FINISH));
140+
}
141+
}
142+
}

0 commit comments

Comments
 (0)