diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/ClassPathExclusions.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/ClassPathExclusions.java index f7c809f947..8f124bcd02 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/ClassPathExclusions.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/ClassPathExclusions.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -25,6 +25,8 @@ import java.lang.annotation.Target; import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.core.annotation.AliasFor; + /** * Annotation used to exclude entries from the classpath. * @@ -37,13 +39,34 @@ import org.junit.jupiter.api.extension.ExtendWith; @ExtendWith(ModifiedClassPathExtension.class) public @interface ClassPathExclusions { + /** + * Alias for {@code files}. + *

+ * One or more Ant-style patterns that identify entries to be excluded from the class + * path. Matching is performed against an entry's {@link File#getName() file name}. + * For example, to exclude Hibernate Validator from the classpath, + * {@code "hibernate-validator-*.jar"} can be used. + * @return the exclusion patterns + */ + @AliasFor("files") + String[] value() default {}; + /** * One or more Ant-style patterns that identify entries to be excluded from the class * path. Matching is performed against an entry's {@link File#getName() file name}. * For example, to exclude Hibernate Validator from the classpath, * {@code "hibernate-validator-*.jar"} can be used. * @return the exclusion patterns + * @since 3.2.0 + */ + @AliasFor("value") + String[] files() default {}; + + /** + * One or more packages that should be excluded from the classpath. + * @return the excluded packages + * @since 3.2.0 */ - String[] value(); + String[] packages() default {}; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/ModifiedClassPathClassLoader.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/ModifiedClassPathClassLoader.java index fdd5f0c915..acb91585be 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/ModifiedClassPathClassLoader.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/ModifiedClassPathClassLoader.java @@ -26,6 +26,7 @@ import java.net.URLClassLoader; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -56,6 +57,7 @@ import org.eclipse.aether.transport.http.HttpTransporterFactory; import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotations; import org.springframework.util.AntPathMatcher; +import org.springframework.util.ClassUtils; import org.springframework.util.ConcurrentReferenceHashMap; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -74,10 +76,14 @@ final class ModifiedClassPathClassLoader extends URLClassLoader { private static final int MAX_RESOLUTION_ATTEMPTS = 5; + private final Set excludedPackages; + private final ClassLoader junitLoader; - ModifiedClassPathClassLoader(URL[] urls, ClassLoader parent, ClassLoader junitLoader) { + ModifiedClassPathClassLoader(URL[] urls, Set excludedPackages, ClassLoader parent, + ClassLoader junitLoader) { super(urls, parent); + this.excludedPackages = excludedPackages; this.junitLoader = junitLoader; } @@ -87,6 +93,10 @@ final class ModifiedClassPathClassLoader extends URLClassLoader { || name.startsWith("io.netty.internal.tcnative")) { return Class.forName(name, false, this.junitLoader); } + String packageName = ClassUtils.getPackageName(name); + if (this.excludedPackages.contains(packageName)) { + throw new ClassNotFoundException(); + } return super.loadClass(name); } @@ -130,7 +140,7 @@ final class ModifiedClassPathClassLoader extends URLClassLoader { .map((source) -> MergedAnnotations.from(source, MergedAnnotations.SearchStrategy.TYPE_HIERARCHY)) .toList(); return new ModifiedClassPathClassLoader(processUrls(extractUrls(classLoader), annotations), - classLoader.getParent(), classLoader); + excludedPackages(annotations), classLoader.getParent(), classLoader); } private static URL[] extractUrls(ClassLoader classLoader) { @@ -269,6 +279,17 @@ final class ModifiedClassPathClassLoader extends URLClassLoader { return dependencies; } + private static Set excludedPackages(List annotations) { + Set excludedPackages = new HashSet<>(); + for (MergedAnnotations candidate : annotations) { + MergedAnnotation annotation = candidate.get(ClassPathExclusions.class); + if (annotation.isPresent()) { + excludedPackages.addAll(Arrays.asList(annotation.getStringArray("packages"))); + } + } + return excludedPackages; + } + /** * Filter for class path entries. */ diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/classpath/ModifiedClassPathExtensionExclusionsTests.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/classpath/ModifiedClassPathExtensionExclusionsTests.java index 5cfdd53c0a..0e0741bd2a 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/classpath/ModifiedClassPathExtensionExclusionsTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/classpath/ModifiedClassPathExtensionExclusionsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2023 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. @@ -19,6 +19,8 @@ package org.springframework.boot.testsupport.classpath; import org.hamcrest.Matcher; import org.junit.jupiter.api.Test; +import org.springframework.util.ClassUtils; + import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.isA; @@ -26,22 +28,34 @@ import static org.hamcrest.Matchers.isA; * Tests for {@link ModifiedClassPathExtension} excluding entries from the class path. * * @author Christoph Dreis + * @author Andy Wilkinson */ -@ClassPathExclusions("hibernate-validator-*.jar") +@ClassPathExclusions(files = "hibernate-validator-*.jar", packages = "java.net.http") class ModifiedClassPathExtensionExclusionsTests { private static final String EXCLUDED_RESOURCE = "META-INF/services/jakarta.validation.spi.ValidationProvider"; @Test - void entriesAreFilteredFromTestClassClassLoader() { + void fileExclusionsAreFilteredFromTestClassClassLoader() { assertThat(getClass().getClassLoader().getResource(EXCLUDED_RESOURCE)).isNull(); } @Test - void entriesAreFilteredFromThreadContextClassLoader() { + void fileExclusionsAreFilteredFromThreadContextClassLoader() { assertThat(Thread.currentThread().getContextClassLoader().getResource(EXCLUDED_RESOURCE)).isNull(); } + @Test + void packageExclusionsAreFilteredFromTestClassClassLoader() { + assertThat(ClassUtils.isPresent("java.net.http.HttpClient", getClass().getClassLoader())).isFalse(); + } + + @Test + void packageExclusionsAreFilteredFromThreadContextClassLoader() { + assertThat(ClassUtils.isPresent("java.net.http.HttpClient", Thread.currentThread().getContextClassLoader())) + .isFalse(); + } + @Test void testsThatUseHamcrestWorkCorrectly() { Matcher matcher = isA(IllegalStateException.class);