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 index f47ae15daa7..f4743aa0eab 100644 --- 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 @@ -16,18 +16,52 @@ 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; /** * Custom {@link URLClassLoader} that modifies the class path. * * @author Andy Wilkinson * @author Christoph Dreis - * @see ModifiedClassPathClassLoaderFactory */ final class ModifiedClassPathClassLoader extends URLClassLoader { + private static final Pattern INTELLIJ_CLASSPATH_JAR_PATTERN = Pattern.compile(".*classpath(\\d+)?\\.jar"); + private final ClassLoader junitLoader; ModifiedClassPathClassLoader(URL[] urls, ClassLoader parent, ClassLoader junitLoader) { @@ -43,4 +77,178 @@ final class ModifiedClassPathClassLoader extends URLClassLoader { return super.loadClass(name); } + static ModifiedClassPathClassLoader get(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(ModifiedClassPathClassLoader::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())) { + try { + String name = new File(url.toURI()).getName(); + for (String exclusion : this.exclusions) { + if (this.matcher.match(exclusion, name)) { + return true; + } + } + } + catch (URISyntaxException ex) { + } + } + return false; + } + + } + } 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 deleted file mode 100644 index 203de78aaa6..00000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/runner/classpath/ModifiedClassPathClassLoaderFactory.java +++ /dev/null @@ -1,245 +0,0 @@ -/* - * 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())) { - try { - String name = new File(url.toURI()).getName(); - for (String exclusion : this.exclusions) { - if (this.matcher.match(exclusion, name)) { - return true; - } - } - } - catch (URISyntaxException ex) { - } - } - return false; - } - - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/runner/classpath/ModifiedClassPathExtension.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/runner/classpath/ModifiedClassPathExtension.java index 16783150a91..dae47219bb9 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/runner/classpath/ModifiedClassPathExtension.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/runner/classpath/ModifiedClassPathExtension.java @@ -85,7 +85,7 @@ public class ModifiedClassPathExtension implements InvocationInterceptor { Class testClass = extensionContext.getRequiredTestClass(); Method testMethod = invocationContext.getExecutable(); ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader(); - URLClassLoader modifiedClassLoader = ModifiedClassPathClassLoaderFactory.createTestClassLoader(testClass); + URLClassLoader modifiedClassLoader = ModifiedClassPathClassLoader.get(testClass); Thread.currentThread().setContextClassLoader(modifiedClassLoader); try { runTest(modifiedClassLoader, testClass.getName(), testMethod.getName());