Skip to content

Commit b6ec133

Browse files
committed
Polish ModifiedClassPathClassLoader
Remove the `ModifiedClassPathClassLoaderFactory` in favor of factory methods on `ModifiedClassPathClassLoader`. See gh-17491
1 parent 4fe5e9e commit b6ec133

File tree

3 files changed

+210
-247
lines changed

3 files changed

+210
-247
lines changed

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

Lines changed: 209 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,52 @@
1616

1717
package org.springframework.boot.testsupport.runner.classpath;
1818

19+
import java.io.File;
20+
import java.lang.management.ManagementFactory;
21+
import java.net.URISyntaxException;
1922
import java.net.URL;
2023
import java.net.URLClassLoader;
24+
import java.util.ArrayList;
25+
import java.util.Arrays;
26+
import java.util.Collections;
27+
import java.util.List;
28+
import java.util.jar.Attributes;
29+
import java.util.jar.JarFile;
30+
import java.util.regex.Pattern;
31+
import java.util.stream.Stream;
32+
33+
import org.apache.maven.repository.internal.MavenRepositorySystemUtils;
34+
import org.eclipse.aether.DefaultRepositorySystemSession;
35+
import org.eclipse.aether.RepositorySystem;
36+
import org.eclipse.aether.artifact.DefaultArtifact;
37+
import org.eclipse.aether.collection.CollectRequest;
38+
import org.eclipse.aether.connector.basic.BasicRepositoryConnectorFactory;
39+
import org.eclipse.aether.graph.Dependency;
40+
import org.eclipse.aether.impl.DefaultServiceLocator;
41+
import org.eclipse.aether.repository.LocalRepository;
42+
import org.eclipse.aether.repository.RemoteRepository;
43+
import org.eclipse.aether.resolution.ArtifactResult;
44+
import org.eclipse.aether.resolution.DependencyRequest;
45+
import org.eclipse.aether.resolution.DependencyResult;
46+
import org.eclipse.aether.spi.connector.RepositoryConnectorFactory;
47+
import org.eclipse.aether.spi.connector.transport.TransporterFactory;
48+
import org.eclipse.aether.transport.http.HttpTransporterFactory;
49+
50+
import org.springframework.core.annotation.MergedAnnotation;
51+
import org.springframework.core.annotation.MergedAnnotations;
52+
import org.springframework.util.AntPathMatcher;
53+
import org.springframework.util.StringUtils;
2154

2255
/**
2356
* Custom {@link URLClassLoader} that modifies the class path.
2457
*
2558
* @author Andy Wilkinson
2659
* @author Christoph Dreis
27-
* @see ModifiedClassPathClassLoaderFactory
2860
*/
2961
final class ModifiedClassPathClassLoader extends URLClassLoader {
3062

63+
private static final Pattern INTELLIJ_CLASSPATH_JAR_PATTERN = Pattern.compile(".*classpath(\\d+)?\\.jar");
64+
3165
private final ClassLoader junitLoader;
3266

3367
ModifiedClassPathClassLoader(URL[] urls, ClassLoader parent, ClassLoader junitLoader) {
@@ -43,4 +77,178 @@ public Class<?> loadClass(String name) throws ClassNotFoundException {
4377
return super.loadClass(name);
4478
}
4579

80+
static ModifiedClassPathClassLoader get(Class<?> testClass) {
81+
ClassLoader classLoader = testClass.getClassLoader();
82+
return new ModifiedClassPathClassLoader(processUrls(extractUrls(classLoader), testClass),
83+
classLoader.getParent(), classLoader);
84+
}
85+
86+
private static URL[] extractUrls(ClassLoader classLoader) {
87+
List<URL> extractedUrls = new ArrayList<>();
88+
doExtractUrls(classLoader).forEach((URL url) -> {
89+
if (isManifestOnlyJar(url)) {
90+
extractedUrls.addAll(extractUrlsFromManifestClassPath(url));
91+
}
92+
else {
93+
extractedUrls.add(url);
94+
}
95+
});
96+
return extractedUrls.toArray(new URL[0]);
97+
}
98+
99+
private static Stream<URL> doExtractUrls(ClassLoader classLoader) {
100+
if (classLoader instanceof URLClassLoader) {
101+
return Stream.of(((URLClassLoader) classLoader).getURLs());
102+
}
103+
return Stream.of(ManagementFactory.getRuntimeMXBean().getClassPath().split(File.pathSeparator))
104+
.map(ModifiedClassPathClassLoader::toURL);
105+
}
106+
107+
private static URL toURL(String entry) {
108+
try {
109+
return new File(entry).toURI().toURL();
110+
}
111+
catch (Exception ex) {
112+
throw new IllegalArgumentException(ex);
113+
}
114+
}
115+
116+
private static boolean isManifestOnlyJar(URL url) {
117+
return isSurefireBooterJar(url) || isShortenedIntelliJJar(url);
118+
}
119+
120+
private static boolean isSurefireBooterJar(URL url) {
121+
return url.getPath().contains("surefirebooter");
122+
}
123+
124+
private static boolean isShortenedIntelliJJar(URL url) {
125+
String urlPath = url.getPath();
126+
boolean isCandidate = INTELLIJ_CLASSPATH_JAR_PATTERN.matcher(urlPath).matches();
127+
if (isCandidate) {
128+
try {
129+
Attributes attributes = getManifestMainAttributesFromUrl(url);
130+
String createdBy = attributes.getValue("Created-By");
131+
return createdBy != null && createdBy.contains("IntelliJ");
132+
}
133+
catch (Exception ex) {
134+
}
135+
}
136+
return false;
137+
}
138+
139+
private static List<URL> extractUrlsFromManifestClassPath(URL booterJar) {
140+
List<URL> urls = new ArrayList<>();
141+
try {
142+
for (String entry : getClassPath(booterJar)) {
143+
urls.add(new URL(entry));
144+
}
145+
}
146+
catch (Exception ex) {
147+
throw new RuntimeException(ex);
148+
}
149+
return urls;
150+
}
151+
152+
private static String[] getClassPath(URL booterJar) throws Exception {
153+
Attributes attributes = getManifestMainAttributesFromUrl(booterJar);
154+
return StringUtils.delimitedListToStringArray(attributes.getValue(Attributes.Name.CLASS_PATH), " ");
155+
}
156+
157+
private static Attributes getManifestMainAttributesFromUrl(URL url) throws Exception {
158+
try (JarFile jarFile = new JarFile(new File(url.toURI()))) {
159+
return jarFile.getManifest().getMainAttributes();
160+
}
161+
}
162+
163+
private static URL[] processUrls(URL[] urls, Class<?> testClass) {
164+
MergedAnnotations annotations = MergedAnnotations.from(testClass, MergedAnnotations.SearchStrategy.EXHAUSTIVE);
165+
ClassPathEntryFilter filter = new ClassPathEntryFilter(annotations.get(ClassPathExclusions.class));
166+
List<URL> processedUrls = new ArrayList<>();
167+
List<URL> additionalUrls = getAdditionalUrls(annotations.get(ClassPathOverrides.class));
168+
processedUrls.addAll(additionalUrls);
169+
for (URL url : urls) {
170+
if (!filter.isExcluded(url)) {
171+
processedUrls.add(url);
172+
}
173+
}
174+
return processedUrls.toArray(new URL[0]);
175+
}
176+
177+
private static List<URL> getAdditionalUrls(MergedAnnotation<ClassPathOverrides> annotation) {
178+
if (!annotation.isPresent()) {
179+
return Collections.emptyList();
180+
}
181+
return resolveCoordinates(annotation.getStringArray(MergedAnnotation.VALUE));
182+
}
183+
184+
private static List<URL> resolveCoordinates(String[] coordinates) {
185+
DefaultServiceLocator serviceLocator = MavenRepositorySystemUtils.newServiceLocator();
186+
serviceLocator.addService(RepositoryConnectorFactory.class, BasicRepositoryConnectorFactory.class);
187+
serviceLocator.addService(TransporterFactory.class, HttpTransporterFactory.class);
188+
RepositorySystem repositorySystem = serviceLocator.getService(RepositorySystem.class);
189+
DefaultRepositorySystemSession session = MavenRepositorySystemUtils.newSession();
190+
LocalRepository localRepository = new LocalRepository(System.getProperty("user.home") + "/.m2/repository");
191+
session.setLocalRepositoryManager(repositorySystem.newLocalRepositoryManager(session, localRepository));
192+
CollectRequest collectRequest = new CollectRequest(null, Arrays.asList(
193+
new RemoteRepository.Builder("central", "default", "https://repo.maven.apache.org/maven2").build()));
194+
195+
collectRequest.setDependencies(createDependencies(coordinates));
196+
DependencyRequest dependencyRequest = new DependencyRequest(collectRequest, null);
197+
try {
198+
DependencyResult result = repositorySystem.resolveDependencies(session, dependencyRequest);
199+
List<URL> resolvedArtifacts = new ArrayList<>();
200+
for (ArtifactResult artifact : result.getArtifactResults()) {
201+
resolvedArtifacts.add(artifact.getArtifact().getFile().toURI().toURL());
202+
}
203+
return resolvedArtifacts;
204+
}
205+
catch (Exception ignored) {
206+
return Collections.emptyList();
207+
208+
}
209+
}
210+
211+
private static List<Dependency> createDependencies(String[] allCoordinates) {
212+
List<Dependency> dependencies = new ArrayList<>();
213+
for (String coordinate : allCoordinates) {
214+
dependencies.add(new Dependency(new DefaultArtifact(coordinate), null));
215+
}
216+
return dependencies;
217+
}
218+
219+
/**
220+
* Filter for class path entries.
221+
*/
222+
private static final class ClassPathEntryFilter {
223+
224+
private final List<String> exclusions;
225+
226+
private final AntPathMatcher matcher = new AntPathMatcher();
227+
228+
private ClassPathEntryFilter(MergedAnnotation<ClassPathExclusions> annotation) {
229+
this.exclusions = new ArrayList<>();
230+
this.exclusions.add("log4j-*.jar");
231+
if (annotation.isPresent()) {
232+
this.exclusions.addAll(Arrays.asList(annotation.getStringArray(MergedAnnotation.VALUE)));
233+
}
234+
}
235+
236+
private boolean isExcluded(URL url) {
237+
if ("file".equals(url.getProtocol())) {
238+
try {
239+
String name = new File(url.toURI()).getName();
240+
for (String exclusion : this.exclusions) {
241+
if (this.matcher.match(exclusion, name)) {
242+
return true;
243+
}
244+
}
245+
}
246+
catch (URISyntaxException ex) {
247+
}
248+
}
249+
return false;
250+
}
251+
252+
}
253+
46254
}

0 commit comments

Comments
 (0)