diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/runner/classpath/ModifiedClassPathClassLoader.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/runner/classpath/ModifiedClassPathClassLoader.java new file mode 100644 index 00000000000..f47ae15daa7 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/runner/classpath/ModifiedClassPathClassLoader.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012-2019 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.boot.testsupport.runner.classpath; + +import java.net.URL; +import java.net.URLClassLoader; + +/** + * Custom {@link URLClassLoader} that modifies the class path. + * + * @author Andy Wilkinson + * @author Christoph Dreis + * @see ModifiedClassPathClassLoaderFactory + */ +final class ModifiedClassPathClassLoader extends URLClassLoader { + + private final ClassLoader junitLoader; + + ModifiedClassPathClassLoader(URL[] urls, ClassLoader parent, ClassLoader junitLoader) { + super(urls, parent); + this.junitLoader = junitLoader; + } + + @Override + public Class loadClass(String name) throws ClassNotFoundException { + if (name.startsWith("org.junit") || name.startsWith("org.hamcrest")) { + return this.junitLoader.loadClass(name); + } + return super.loadClass(name); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/runner/classpath/ModifiedClassPathClassLoaderFactory.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/runner/classpath/ModifiedClassPathClassLoaderFactory.java new file mode 100644 index 00000000000..5b5d08d5631 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/runner/classpath/ModifiedClassPathClassLoaderFactory.java @@ -0,0 +1,248 @@ +/* + * Copyright 2012-2019 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.boot.testsupport.runner.classpath; + +import java.io.File; +import java.lang.management.ManagementFactory; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.jar.Attributes; +import java.util.jar.JarFile; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +import org.apache.maven.repository.internal.MavenRepositorySystemUtils; +import org.eclipse.aether.DefaultRepositorySystemSession; +import org.eclipse.aether.RepositorySystem; +import org.eclipse.aether.artifact.DefaultArtifact; +import org.eclipse.aether.collection.CollectRequest; +import org.eclipse.aether.connector.basic.BasicRepositoryConnectorFactory; +import org.eclipse.aether.graph.Dependency; +import org.eclipse.aether.impl.DefaultServiceLocator; +import org.eclipse.aether.repository.LocalRepository; +import org.eclipse.aether.repository.RemoteRepository; +import org.eclipse.aether.resolution.ArtifactResult; +import org.eclipse.aether.resolution.DependencyRequest; +import org.eclipse.aether.resolution.DependencyResult; +import org.eclipse.aether.spi.connector.RepositoryConnectorFactory; +import org.eclipse.aether.spi.connector.transport.TransporterFactory; +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.StringUtils; + +/** + * A factory that creates a custom class loader with a modified class path that is used to + * both load the test class and as the thread context class loader while the test is being + * run. + * + * @author Andy Wilkinson + * @author Christoph Dreis + * @see ModifiedClassPathClassLoader + */ +final class ModifiedClassPathClassLoaderFactory { + + private static final Pattern INTELLIJ_CLASSPATH_JAR_PATTERN = Pattern.compile(".*classpath(\\d+)?\\.jar"); + + private ModifiedClassPathClassLoaderFactory() { + } + + static URLClassLoader createTestClassLoader(Class testClass) { + ClassLoader classLoader = testClass.getClassLoader(); + return new ModifiedClassPathClassLoader(processUrls(extractUrls(classLoader), testClass), + classLoader.getParent(), classLoader); + } + + private static URL[] extractUrls(ClassLoader classLoader) { + List extractedUrls = new ArrayList<>(); + doExtractUrls(classLoader).forEach((URL url) -> { + if (isManifestOnlyJar(url)) { + extractedUrls.addAll(extractUrlsFromManifestClassPath(url)); + } + else { + extractedUrls.add(url); + } + }); + return extractedUrls.toArray(new URL[0]); + } + + private static Stream doExtractUrls(ClassLoader classLoader) { + if (classLoader instanceof URLClassLoader) { + return Stream.of(((URLClassLoader) classLoader).getURLs()); + } + return Stream.of(ManagementFactory.getRuntimeMXBean().getClassPath().split(File.pathSeparator)) + .map(ModifiedClassPathClassLoaderFactory::toURL); + } + + private static URL toURL(String entry) { + try { + return new File(entry).toURI().toURL(); + } + catch (Exception ex) { + throw new IllegalArgumentException(ex); + } + } + + private static boolean isManifestOnlyJar(URL url) { + return isSurefireBooterJar(url) || isShortenedIntelliJJar(url); + } + + private static boolean isSurefireBooterJar(URL url) { + return url.getPath().contains("surefirebooter"); + } + + private static boolean isShortenedIntelliJJar(URL url) { + String urlPath = url.getPath(); + boolean isCandidate = INTELLIJ_CLASSPATH_JAR_PATTERN.matcher(urlPath).matches(); + if (isCandidate) { + try { + Attributes attributes = getManifestMainAttributesFromUrl(url); + String createdBy = attributes.getValue("Created-By"); + return createdBy != null && createdBy.contains("IntelliJ"); + } + catch (Exception ex) { + } + } + return false; + } + + private static List extractUrlsFromManifestClassPath(URL booterJar) { + List urls = new ArrayList<>(); + try { + for (String entry : getClassPath(booterJar)) { + urls.add(new URL(entry)); + } + } + catch (Exception ex) { + throw new RuntimeException(ex); + } + return urls; + } + + private static String[] getClassPath(URL booterJar) throws Exception { + Attributes attributes = getManifestMainAttributesFromUrl(booterJar); + return StringUtils.delimitedListToStringArray(attributes.getValue(Attributes.Name.CLASS_PATH), " "); + } + + private static Attributes getManifestMainAttributesFromUrl(URL url) throws Exception { + try (JarFile jarFile = new JarFile(new File(url.toURI()))) { + return jarFile.getManifest().getMainAttributes(); + } + } + + private static URL[] processUrls(URL[] urls, Class testClass) { + MergedAnnotations annotations = MergedAnnotations.from(testClass, MergedAnnotations.SearchStrategy.EXHAUSTIVE); + ClassPathEntryFilter filter = new ClassPathEntryFilter(annotations.get(ClassPathExclusions.class)); + List processedUrls = new ArrayList<>(); + List additionalUrls = getAdditionalUrls(annotations.get(ClassPathOverrides.class)); + processedUrls.addAll(additionalUrls); + for (URL url : urls) { + if (!filter.isExcluded(url)) { + processedUrls.add(url); + } + } + return processedUrls.toArray(new URL[0]); + } + + private static List getAdditionalUrls(MergedAnnotation annotation) { + if (!annotation.isPresent()) { + return Collections.emptyList(); + } + return resolveCoordinates(annotation.getStringArray(MergedAnnotation.VALUE)); + } + + private static List resolveCoordinates(String[] coordinates) { + DefaultServiceLocator serviceLocator = MavenRepositorySystemUtils.newServiceLocator(); + serviceLocator.addService(RepositoryConnectorFactory.class, BasicRepositoryConnectorFactory.class); + serviceLocator.addService(TransporterFactory.class, HttpTransporterFactory.class); + RepositorySystem repositorySystem = serviceLocator.getService(RepositorySystem.class); + DefaultRepositorySystemSession session = MavenRepositorySystemUtils.newSession(); + LocalRepository localRepository = new LocalRepository(System.getProperty("user.home") + "/.m2/repository"); + session.setLocalRepositoryManager(repositorySystem.newLocalRepositoryManager(session, localRepository)); + CollectRequest collectRequest = new CollectRequest(null, Arrays.asList( + new RemoteRepository.Builder("central", "default", "https://repo.maven.apache.org/maven2").build())); + + collectRequest.setDependencies(createDependencies(coordinates)); + DependencyRequest dependencyRequest = new DependencyRequest(collectRequest, null); + try { + DependencyResult result = repositorySystem.resolveDependencies(session, dependencyRequest); + List resolvedArtifacts = new ArrayList<>(); + for (ArtifactResult artifact : result.getArtifactResults()) { + resolvedArtifacts.add(artifact.getArtifact().getFile().toURI().toURL()); + } + return resolvedArtifacts; + } + catch (Exception ignored) { + return Collections.emptyList(); + + } + } + + private static List createDependencies(String[] allCoordinates) { + List dependencies = new ArrayList<>(); + for (String coordinate : allCoordinates) { + dependencies.add(new Dependency(new DefaultArtifact(coordinate), null)); + } + return dependencies; + } + + /** + * Filter for class path entries. + */ + private static final class ClassPathEntryFilter { + + private final List exclusions; + + private final AntPathMatcher matcher = new AntPathMatcher(); + + private ClassPathEntryFilter(MergedAnnotation annotation) { + this.exclusions = new ArrayList<>(); + this.exclusions.add("log4j-*.jar"); + if (annotation.isPresent()) { + this.exclusions.addAll(Arrays.asList(annotation.getStringArray(MergedAnnotation.VALUE))); + } + } + + private boolean isExcluded(URL url) { + if (!"file".equals(url.getProtocol())) { + return false; + } + String name; + try { + name = new File(url.toURI()).getName(); + } + catch (URISyntaxException ex) { + return false; + } + for (String exclusion : this.exclusions) { + if (this.matcher.match(exclusion, name)) { + return true; + } + } + return false; + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/runner/classpath/ModifiedClassPathRunner.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/runner/classpath/ModifiedClassPathRunner.java index c66f06e8af4..e013f604bef 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/runner/classpath/ModifiedClassPathRunner.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/runner/classpath/ModifiedClassPathRunner.java @@ -16,48 +16,16 @@ package org.springframework.boot.testsupport.runner.classpath; -import java.io.File; import java.lang.annotation.Annotation; -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.Collections; import java.util.List; -import java.util.jar.Attributes; -import java.util.jar.JarFile; -import java.util.regex.Pattern; -import java.util.stream.Stream; -import org.apache.maven.repository.internal.MavenRepositorySystemUtils; -import org.eclipse.aether.DefaultRepositorySystemSession; -import org.eclipse.aether.RepositorySystem; -import org.eclipse.aether.artifact.DefaultArtifact; -import org.eclipse.aether.collection.CollectRequest; -import org.eclipse.aether.connector.basic.BasicRepositoryConnectorFactory; -import org.eclipse.aether.graph.Dependency; -import org.eclipse.aether.impl.DefaultServiceLocator; -import org.eclipse.aether.repository.LocalRepository; -import org.eclipse.aether.repository.RemoteRepository; -import org.eclipse.aether.resolution.ArtifactResult; -import org.eclipse.aether.resolution.DependencyRequest; -import org.eclipse.aether.resolution.DependencyResult; -import org.eclipse.aether.spi.connector.RepositoryConnectorFactory; -import org.eclipse.aether.spi.connector.transport.TransporterFactory; -import org.eclipse.aether.transport.http.HttpTransporterFactory; import org.junit.runners.BlockJUnit4ClassRunner; import org.junit.runners.model.FrameworkMethod; import org.junit.runners.model.InitializationError; import org.junit.runners.model.TestClass; -import org.springframework.core.annotation.MergedAnnotation; -import org.springframework.core.annotation.MergedAnnotations; -import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; -import org.springframework.util.AntPathMatcher; -import org.springframework.util.StringUtils; - /** * A custom {@link BlockJUnit4ClassRunner} that runs tests using a modified class path. * Entries are excluded from the class path using @@ -71,8 +39,6 @@ import org.springframework.util.StringUtils; */ public class ModifiedClassPathRunner extends BlockJUnit4ClassRunner { - private static final Pattern INTELLIJ_CLASSPATH_JAR_PATTERN = Pattern.compile(".*classpath(\\d+)?\\.jar"); - public ModifiedClassPathRunner(Class testClass) throws InitializationError { super(testClass); } @@ -80,7 +46,7 @@ public class ModifiedClassPathRunner extends BlockJUnit4ClassRunner { @Override protected TestClass createTestClass(Class testClass) { try { - ClassLoader classLoader = createTestClassLoader(testClass); + ClassLoader classLoader = ModifiedClassPathClassLoaderFactory.createTestClassLoader(testClass); return new ModifiedClassPathTestClass(classLoader, testClass.getName()); } catch (Exception ex) { @@ -95,171 +61,6 @@ public class ModifiedClassPathRunner extends BlockJUnit4ClassRunner { .doWithModifiedClassPathThreadContextClassLoader(() -> ModifiedClassPathRunner.super.createTest()); } - private URLClassLoader createTestClassLoader(Class testClass) throws Exception { - ClassLoader classLoader = this.getClass().getClassLoader(); - return new ModifiedClassPathClassLoader(processUrls(extractUrls(classLoader), testClass), - classLoader.getParent(), classLoader); - } - - private URL[] extractUrls(ClassLoader classLoader) throws Exception { - List extractedUrls = new ArrayList<>(); - doExtractUrls(classLoader).forEach((URL url) -> { - if (isManifestOnlyJar(url)) { - extractedUrls.addAll(extractUrlsFromManifestClassPath(url)); - } - else { - extractedUrls.add(url); - } - }); - return extractedUrls.toArray(new URL[0]); - } - - private Stream doExtractUrls(ClassLoader classLoader) throws Exception { - if (classLoader instanceof URLClassLoader) { - return Stream.of(((URLClassLoader) classLoader).getURLs()); - } - return Stream.of(ManagementFactory.getRuntimeMXBean().getClassPath().split(File.pathSeparator)) - .map(this::toURL); - } - - private URL toURL(String entry) { - try { - return new File(entry).toURI().toURL(); - } - catch (Exception ex) { - throw new IllegalArgumentException(ex); - } - } - - private boolean isManifestOnlyJar(URL url) { - return isSurefireBooterJar(url) || isShortenedIntelliJJar(url); - } - - private boolean isSurefireBooterJar(URL url) { - return url.getPath().contains("surefirebooter"); - } - - private boolean isShortenedIntelliJJar(URL url) { - String urlPath = url.getPath(); - boolean isCandidate = INTELLIJ_CLASSPATH_JAR_PATTERN.matcher(urlPath).matches(); - if (isCandidate) { - try { - Attributes attributes = getManifestMainAttributesFromUrl(url); - String createdBy = attributes.getValue("Created-By"); - return createdBy != null && createdBy.contains("IntelliJ"); - } - catch (Exception ex) { - } - } - return false; - } - - private List extractUrlsFromManifestClassPath(URL booterJar) { - List urls = new ArrayList<>(); - try { - for (String entry : getClassPath(booterJar)) { - urls.add(new URL(entry)); - } - } - catch (Exception ex) { - throw new RuntimeException(ex); - } - return urls; - } - - private String[] getClassPath(URL booterJar) throws Exception { - Attributes attributes = getManifestMainAttributesFromUrl(booterJar); - return StringUtils.delimitedListToStringArray(attributes.getValue(Attributes.Name.CLASS_PATH), " "); - } - - private Attributes getManifestMainAttributesFromUrl(URL url) throws Exception { - try (JarFile jarFile = new JarFile(new File(url.toURI()))) { - return jarFile.getManifest().getMainAttributes(); - } - } - - private URL[] processUrls(URL[] urls, Class testClass) throws Exception { - MergedAnnotations annotations = MergedAnnotations.from(testClass, SearchStrategy.EXHAUSTIVE); - ClassPathEntryFilter filter = new ClassPathEntryFilter(annotations.get(ClassPathExclusions.class)); - List processedUrls = new ArrayList<>(); - List additionalUrls = getAdditionalUrls(annotations.get(ClassPathOverrides.class)); - processedUrls.addAll(additionalUrls); - for (URL url : urls) { - if (!filter.isExcluded(url)) { - processedUrls.add(url); - } - } - return processedUrls.toArray(new URL[0]); - } - - private List getAdditionalUrls(MergedAnnotation annotation) throws Exception { - if (!annotation.isPresent()) { - return Collections.emptyList(); - } - return resolveCoordinates(annotation.getStringArray(MergedAnnotation.VALUE)); - } - - private List resolveCoordinates(String[] coordinates) throws Exception { - DefaultServiceLocator serviceLocator = MavenRepositorySystemUtils.newServiceLocator(); - serviceLocator.addService(RepositoryConnectorFactory.class, BasicRepositoryConnectorFactory.class); - serviceLocator.addService(TransporterFactory.class, HttpTransporterFactory.class); - RepositorySystem repositorySystem = serviceLocator.getService(RepositorySystem.class); - DefaultRepositorySystemSession session = MavenRepositorySystemUtils.newSession(); - LocalRepository localRepository = new LocalRepository(System.getProperty("user.home") + "/.m2/repository"); - session.setLocalRepositoryManager(repositorySystem.newLocalRepositoryManager(session, localRepository)); - CollectRequest collectRequest = new CollectRequest(null, Arrays.asList( - new RemoteRepository.Builder("central", "default", "https://repo.maven.apache.org/maven2").build())); - - collectRequest.setDependencies(createDependencies(coordinates)); - DependencyRequest dependencyRequest = new DependencyRequest(collectRequest, null); - DependencyResult result = repositorySystem.resolveDependencies(session, dependencyRequest); - List resolvedArtifacts = new ArrayList<>(); - for (ArtifactResult artifact : result.getArtifactResults()) { - resolvedArtifacts.add(artifact.getArtifact().getFile().toURI().toURL()); - } - return resolvedArtifacts; - } - - private List createDependencies(String[] allCoordinates) { - List dependencies = new ArrayList<>(); - for (String coordinate : allCoordinates) { - dependencies.add(new Dependency(new DefaultArtifact(coordinate), null)); - } - return dependencies; - } - - /** - * Filter for class path entries. - */ - private static final class ClassPathEntryFilter { - - private final List exclusions; - - private final AntPathMatcher matcher = new AntPathMatcher(); - - private ClassPathEntryFilter(MergedAnnotation annotation) throws Exception { - this.exclusions = new ArrayList<>(); - this.exclusions.add("log4j-*.jar"); - if (annotation.isPresent()) { - this.exclusions.addAll(Arrays.asList(annotation.getStringArray(MergedAnnotation.VALUE))); - } - } - - private boolean isExcluded(URL url) throws Exception { - if (!"file".equals(url.getProtocol())) { - return false; - } - String name = new File(url.toURI()).getName(); - for (String exclusion : this.exclusions) { - if (this.matcher.match(exclusion, name)) { - return true; - } - } - return false; - } - - } - /** * Custom {@link TestClass} that uses a modified class path. */ @@ -340,26 +141,4 @@ public class ModifiedClassPathRunner extends BlockJUnit4ClassRunner { } - /** - * Custom {@link URLClassLoader} that modifies the class path. - */ - private static final class ModifiedClassPathClassLoader extends URLClassLoader { - - private final ClassLoader junitLoader; - - ModifiedClassPathClassLoader(URL[] urls, ClassLoader parent, ClassLoader junitLoader) { - super(urls, parent); - this.junitLoader = junitLoader; - } - - @Override - public Class loadClass(String name) throws ClassNotFoundException { - if (name.startsWith("org.junit") || name.startsWith("org.hamcrest")) { - return this.junitLoader.loadClass(name); - } - return super.loadClass(name); - } - - } - }