Skip to content

Commit 948893f

Browse files
committed
Merge branch '2.6.x' into 2.7.x
Closes gh-33015
2 parents 06c83e5 + d4cc8fc commit 948893f

File tree

6 files changed

+156
-37
lines changed

6 files changed

+156
-37
lines changed

spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/ClassPathExclusions.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
* @since 1.5.0
3333
*/
3434
@Retention(RetentionPolicy.RUNTIME)
35-
@Target(ElementType.TYPE)
35+
@Target({ ElementType.TYPE, ElementType.METHOD })
3636
@Documented
3737
@ExtendWith(ModifiedClassPathExtension.class)
3838
public @interface ClassPathExclusions {

spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/ClassPathOverrides.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
* @since 1.5.0
3232
*/
3333
@Retention(RetentionPolicy.RUNTIME)
34-
@Target(ElementType.TYPE)
34+
@Target({ ElementType.TYPE, ElementType.METHOD })
3535
@Documented
3636
@ExtendWith(ModifiedClassPathExtension.class)
3737
public @interface ClassPathOverrides {

spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/ForkedClassPath.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,15 @@
2525
import org.junit.jupiter.api.extension.ExtendWith;
2626

2727
/**
28-
* Annotation used to fork the classpath. This can be helpful where neither
29-
* {@link ClassPathExclusions} or {@link ClassPathOverrides} are needed, but just a copy
30-
* of the classpath.
28+
* Annotation used to fork the classpath. This can be helpful when using annotations on
29+
* parameterized tests, or where neither {@link ClassPathExclusions} or
30+
* {@link ClassPathOverrides} are needed, but just a copy of the classpath.
3131
*
3232
* @author Christoph Dreis
3333
* @since 2.4.0
3434
*/
3535
@Retention(RetentionPolicy.RUNTIME)
36-
@Target(ElementType.TYPE)
36+
@Target({ ElementType.TYPE, ElementType.METHOD })
3737
@Documented
3838
@ExtendWith(ModifiedClassPathExtension.class)
3939
public @interface ForkedClassPath {

spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/ModifiedClassPathClassLoader.java

Lines changed: 63 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,22 @@
1818

1919
import java.io.File;
2020
import java.lang.management.ManagementFactory;
21+
import java.lang.reflect.AnnotatedElement;
22+
import java.lang.reflect.Method;
2123
import java.net.URISyntaxException;
2224
import java.net.URL;
2325
import java.net.URLClassLoader;
2426
import java.util.ArrayList;
2527
import java.util.Arrays;
26-
import java.util.Collections;
28+
import java.util.Collection;
29+
import java.util.LinkedHashSet;
2730
import java.util.List;
2831
import java.util.Map;
32+
import java.util.Set;
2933
import java.util.jar.Attributes;
3034
import java.util.jar.JarFile;
3135
import java.util.regex.Pattern;
36+
import java.util.stream.Collectors;
3237
import java.util.stream.Stream;
3338

3439
import org.apache.maven.repository.internal.MavenRepositorySystemUtils;
@@ -52,6 +57,7 @@
5257
import org.springframework.core.annotation.MergedAnnotations;
5358
import org.springframework.util.AntPathMatcher;
5459
import org.springframework.util.ConcurrentReferenceHashMap;
60+
import org.springframework.util.ObjectUtils;
5561
import org.springframework.util.StringUtils;
5662

5763
/**
@@ -62,7 +68,7 @@
6268
*/
6369
final class ModifiedClassPathClassLoader extends URLClassLoader {
6470

65-
private static final Map<Class<?>, ModifiedClassPathClassLoader> cache = new ConcurrentReferenceHashMap<>();
71+
private static final Map<List<AnnotatedElement>, ModifiedClassPathClassLoader> cache = new ConcurrentReferenceHashMap<>();
6672

6773
private static final Pattern INTELLIJ_CLASSPATH_JAR_PATTERN = Pattern.compile(".*classpath(\\d+)?\\.jar");
6874

@@ -84,19 +90,44 @@ public Class<?> loadClass(String name) throws ClassNotFoundException {
8490
return super.loadClass(name);
8591
}
8692

87-
static ModifiedClassPathClassLoader get(Class<?> testClass) {
88-
return cache.computeIfAbsent(testClass, ModifiedClassPathClassLoader::compute);
93+
static ModifiedClassPathClassLoader get(Class<?> testClass, Method testMethod, List<Object> arguments) {
94+
Set<AnnotatedElement> candidates = new LinkedHashSet<>();
95+
candidates.add(testClass);
96+
candidates.add(testMethod);
97+
candidates.addAll(getAnnotatedElements(arguments.toArray()));
98+
List<AnnotatedElement> annotatedElements = candidates.stream()
99+
.filter(ModifiedClassPathClassLoader::hasAnnotation).collect(Collectors.toList());
100+
if (annotatedElements.isEmpty()) {
101+
return null;
102+
}
103+
return cache.computeIfAbsent(annotatedElements, (key) -> compute(testClass.getClassLoader(), key));
89104
}
90105

91-
private static ModifiedClassPathClassLoader compute(Class<?> testClass) {
92-
ClassLoader classLoader = testClass.getClassLoader();
93-
MergedAnnotations annotations = MergedAnnotations.from(testClass,
94-
MergedAnnotations.SearchStrategy.TYPE_HIERARCHY);
95-
if (annotations.isPresent(ForkedClassPath.class) && (annotations.isPresent(ClassPathOverrides.class)
96-
|| annotations.isPresent(ClassPathExclusions.class))) {
97-
throw new IllegalStateException("@ForkedClassPath is redundant in combination with either "
98-
+ "@ClassPathOverrides or @ClassPathExclusions");
106+
private static Collection<AnnotatedElement> getAnnotatedElements(Object[] array) {
107+
Set<AnnotatedElement> result = new LinkedHashSet<>();
108+
for (Object item : array) {
109+
if (item instanceof AnnotatedElement) {
110+
result.add((AnnotatedElement) item);
111+
}
112+
else if (ObjectUtils.isArray(item)) {
113+
result.addAll(getAnnotatedElements(ObjectUtils.toObjectArray(item)));
114+
}
99115
}
116+
return result;
117+
}
118+
119+
private static boolean hasAnnotation(AnnotatedElement element) {
120+
MergedAnnotations annotations = MergedAnnotations.from(element,
121+
MergedAnnotations.SearchStrategy.TYPE_HIERARCHY);
122+
return annotations.isPresent(ForkedClassPath.class) || annotations.isPresent(ClassPathOverrides.class)
123+
|| annotations.isPresent(ClassPathExclusions.class);
124+
}
125+
126+
private static ModifiedClassPathClassLoader compute(ClassLoader classLoader,
127+
List<AnnotatedElement> annotatedClasses) {
128+
List<MergedAnnotations> annotations = annotatedClasses.stream()
129+
.map((source) -> MergedAnnotations.from(source, MergedAnnotations.SearchStrategy.TYPE_HIERARCHY))
130+
.collect(Collectors.toList());
100131
return new ModifiedClassPathClassLoader(processUrls(extractUrls(classLoader), annotations),
101132
classLoader.getParent(), classLoader);
102133
}
@@ -174,9 +205,9 @@ private static Attributes getManifestMainAttributesFromUrl(URL url) throws Excep
174205
}
175206
}
176207

177-
private static URL[] processUrls(URL[] urls, MergedAnnotations annotations) {
178-
ClassPathEntryFilter filter = new ClassPathEntryFilter(annotations.get(ClassPathExclusions.class));
179-
List<URL> additionalUrls = getAdditionalUrls(annotations.get(ClassPathOverrides.class));
208+
private static URL[] processUrls(URL[] urls, List<MergedAnnotations> annotations) {
209+
ClassPathEntryFilter filter = new ClassPathEntryFilter(annotations);
210+
List<URL> additionalUrls = getAdditionalUrls(annotations);
180211
List<URL> processedUrls = new ArrayList<>(additionalUrls);
181212
for (URL url : urls) {
182213
if (!filter.isExcluded(url)) {
@@ -186,11 +217,15 @@ private static URL[] processUrls(URL[] urls, MergedAnnotations annotations) {
186217
return processedUrls.toArray(new URL[0]);
187218
}
188219

189-
private static List<URL> getAdditionalUrls(MergedAnnotation<ClassPathOverrides> annotation) {
190-
if (!annotation.isPresent()) {
191-
return Collections.emptyList();
220+
private static List<URL> getAdditionalUrls(List<MergedAnnotations> annotations) {
221+
Set<URL> urls = new LinkedHashSet<>();
222+
for (MergedAnnotations candidate : annotations) {
223+
MergedAnnotation<ClassPathOverrides> annotation = candidate.get(ClassPathOverrides.class);
224+
if (annotation.isPresent()) {
225+
urls.addAll(resolveCoordinates(annotation.getStringArray(MergedAnnotation.VALUE)));
226+
}
192227
}
193-
return resolveCoordinates(annotation.getStringArray(MergedAnnotation.VALUE));
228+
return urls.stream().collect(Collectors.toList());
194229
}
195230

196231
private static List<URL> resolveCoordinates(String[] coordinates) {
@@ -241,9 +276,15 @@ private static final class ClassPathEntryFilter {
241276

242277
private final AntPathMatcher matcher = new AntPathMatcher();
243278

244-
private ClassPathEntryFilter(MergedAnnotation<ClassPathExclusions> annotation) {
245-
this.exclusions = annotation.getValue(MergedAnnotation.VALUE, String[].class).map(Arrays::asList)
246-
.orElse(Collections.emptyList());
279+
private ClassPathEntryFilter(List<MergedAnnotations> annotations) {
280+
Set<String> exclusions = new LinkedHashSet<>();
281+
for (MergedAnnotations candidate : annotations) {
282+
MergedAnnotation<ClassPathExclusions> annotation = candidate.get(ClassPathExclusions.class);
283+
if (annotation.isPresent()) {
284+
exclusions.addAll(Arrays.asList(annotation.getStringArray(MergedAnnotation.VALUE)));
285+
}
286+
}
287+
this.exclusions = exclusions.stream().collect(Collectors.toList());
247288
}
248289

249290
private boolean isExcluded(URL url) {

spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/ModifiedClassPathExtension.java

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2021 the original author or authors.
2+
* Copyright 2012-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -46,7 +46,7 @@
4646
*
4747
* @author Christoph Dreis
4848
*/
49-
class ModifiedClassPathExtension implements InvocationInterceptor {
49+
public class ModifiedClassPathExtension implements InvocationInterceptor {
5050

5151
@Override
5252
public void interceptBeforeAllMethod(Invocation<Void> invocation,
@@ -75,20 +75,31 @@ public void interceptAfterAllMethod(Invocation<Void> invocation,
7575
@Override
7676
public void interceptTestMethod(Invocation<Void> invocation, ReflectiveInvocationContext<Method> invocationContext,
7777
ExtensionContext extensionContext) throws Throwable {
78+
interceptMethod(invocation, invocationContext, extensionContext);
79+
}
80+
81+
@Override
82+
public void interceptTestTemplateMethod(Invocation<Void> invocation,
83+
ReflectiveInvocationContext<Method> invocationContext, ExtensionContext extensionContext) throws Throwable {
84+
interceptMethod(invocation, invocationContext, extensionContext);
85+
}
86+
87+
private void interceptMethod(Invocation<Void> invocation, ReflectiveInvocationContext<Method> invocationContext,
88+
ExtensionContext extensionContext) throws Throwable {
7889
if (isModifiedClassPathClassLoader(extensionContext)) {
7990
invocation.proceed();
8091
return;
8192
}
82-
invocation.skip();
83-
runTestWithModifiedClassPath(invocationContext, extensionContext);
84-
}
85-
86-
private void runTestWithModifiedClassPath(ReflectiveInvocationContext<Method> invocationContext,
87-
ExtensionContext extensionContext) throws Throwable {
8893
Class<?> testClass = extensionContext.getRequiredTestClass();
8994
Method testMethod = invocationContext.getExecutable();
95+
URLClassLoader modifiedClassLoader = ModifiedClassPathClassLoader.get(testClass, testMethod,
96+
invocationContext.getArguments());
97+
if (modifiedClassLoader == null) {
98+
invocation.proceed();
99+
return;
100+
}
101+
invocation.skip();
90102
ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader();
91-
URLClassLoader modifiedClassLoader = ModifiedClassPathClassLoader.get(testClass);
92103
Thread.currentThread().setContextClassLoader(modifiedClassLoader);
93104
try {
94105
runTest(modifiedClassLoader, testClass.getName(), testMethod.getName());
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
* Copyright 2012-2019 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+
17+
package org.springframework.boot.testsupport.classpath;
18+
19+
import java.util.stream.Stream;
20+
21+
import org.junit.jupiter.params.ParameterizedTest;
22+
import org.junit.jupiter.params.provider.Arguments;
23+
import org.junit.jupiter.params.provider.MethodSource;
24+
25+
import org.springframework.context.ApplicationContext;
26+
27+
import static org.assertj.core.api.Assertions.assertThat;
28+
29+
/**
30+
* Tests for {@link ModifiedClassPathExtension} overriding entries on the class path via
31+
* parameters.
32+
*
33+
* @author Phillip Webb
34+
*/
35+
class ModifiedClassPathExtensionOverridesParametrizedTests {
36+
37+
@ParameterizedTest
38+
@ForkedClassPath
39+
@MethodSource("parameter")
40+
void classesAreLoadedFromParameter(Class<?> type) {
41+
assertThat(ApplicationContext.class.getProtectionDomain().getCodeSource().getLocation().toString())
42+
.endsWith("spring-context-4.1.0.RELEASE.jar");
43+
}
44+
45+
static Class<?>[] parameter() {
46+
return new Class<?>[] { ClassWithOverride.class };
47+
}
48+
49+
@ParameterizedTest
50+
@ForkedClassPath
51+
@MethodSource("arrayParameter")
52+
void classesAreLoadedFromParameterInArray(Object[] types) {
53+
assertThat(ApplicationContext.class.getProtectionDomain().getCodeSource().getLocation().toString())
54+
.endsWith("spring-context-4.1.0.RELEASE.jar");
55+
}
56+
57+
static Stream<Arguments> arrayParameter() {
58+
Object[] types = new Object[] { ClassWithOverride.class };
59+
return Stream.of(Arguments.of(new Object[] { types }));
60+
}
61+
62+
@ClassPathOverrides("org.springframework:spring-context:4.1.0.RELEASE")
63+
static class ClassWithOverride {
64+
65+
}
66+
67+
}

0 commit comments

Comments
 (0)