-
Notifications
You must be signed in to change notification settings - Fork 288
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fixup! Let JarFileLocation work with custom ClassLoader URIs.
- Loading branch information
1 parent
a5cf00c
commit 34009ca
Showing
7 changed files
with
252 additions
and
37 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
plugins { | ||
id 'archunit.java-conventions' | ||
} | ||
|
||
ext.moduleName = 'com.tngtech.archunit.thirdpartytest' | ||
|
||
dependencies { | ||
testImplementation project(path: ':archunit', configuration: 'shadow') | ||
testImplementation project(path: ':archunit', configuration: 'tests') | ||
testImplementation dependency.springBootLoader | ||
dependency.addGuava { dependencyNotation, config -> testImplementation(dependencyNotation, config) } | ||
testImplementation dependency.log4j_slf4j | ||
testImplementation dependency.junit4 | ||
testImplementation dependency.assertj | ||
} |
122 changes: 122 additions & 0 deletions
122
...-3rd-party-test/src/test/java/com/tngtech/archunit/core/importer/SpringLocationsTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
package com.tngtech.archunit.core.importer; | ||
|
||
import java.io.File; | ||
import java.io.IOException; | ||
import java.net.URI; | ||
import java.net.URL; | ||
import java.util.Arrays; | ||
import java.util.Iterator; | ||
import java.util.jar.JarFile; | ||
import java.util.stream.Stream; | ||
|
||
import com.tngtech.archunit.core.importer.testexamples.SomeEnum; | ||
import com.tngtech.archunit.testutil.SystemPropertiesRule; | ||
import org.junit.Rule; | ||
import org.junit.Test; | ||
import org.springframework.boot.loader.LaunchedURLClassLoader; | ||
import org.springframework.boot.loader.archive.Archive; | ||
import org.springframework.boot.loader.archive.JarFileArchive; | ||
|
||
import static com.google.common.collect.Iterators.getOnlyElement; | ||
import static com.google.common.collect.MoreCollectors.onlyElement; | ||
import static com.google.common.collect.Streams.stream; | ||
import static com.google.common.io.ByteStreams.toByteArray; | ||
import static com.tngtech.archunit.core.importer.LocationTest.classFileEntry; | ||
import static com.tngtech.archunit.core.importer.LocationTest.urlOfClass; | ||
import static com.tngtech.archunit.core.importer.LocationsTest.unchecked; | ||
import static com.tngtech.archunit.core.importer.UrlSourceTest.JAVA_CLASS_PATH_PROP; | ||
import static org.assertj.core.api.Assertions.assertThat; | ||
|
||
public class SpringLocationsTest { | ||
/** | ||
* Spring Boot configures some system properties that we want to reset afterward (e.g. custom URL stream handler) | ||
*/ | ||
@Rule | ||
public final SystemPropertiesRule systemPropertiesRule = new SystemPropertiesRule(); | ||
|
||
@Test | ||
public void finds_locations_of_packages_from_Spring_Boot_ClassLoader_for_JARs_with_directory_entries() throws Exception { | ||
try (JarFile jarFile = new TestJarFile() | ||
.withDirectoryEntries() | ||
.withNestedClassFilesDirectory("BOOT-INF/classes") | ||
.withEntry(classFileEntry(SomeEnum.class).toAbsolutePath()) | ||
.create()) { | ||
|
||
configureSpringBootContextClassLoaderKnowingOnly(jarFile); | ||
|
||
Location location = Locations.ofPackage(SomeEnum.class.getPackage().getName()).stream() | ||
.filter(it -> it.contains(jarFile.getName())) | ||
.collect(onlyElement()); | ||
|
||
byte[] expectedClassContent = toByteArray(urlOfClass(SomeEnum.class).openStream()); | ||
Stream<byte[]> actualClassContents = stream(location.asClassFileSource(new ImportOptions())) | ||
.map(it -> unchecked(() -> toByteArray(it.openStream()))); | ||
|
||
boolean containsExpectedContent = actualClassContents.anyMatch(it -> Arrays.equals(it, expectedClassContent)); | ||
assertThat(containsExpectedContent) | ||
.as("one of the found class files has the expected class file content") | ||
.isTrue(); | ||
} | ||
} | ||
|
||
/** | ||
* This is not "desired" behavior, but just to document the state as it is right now. I.e. if there was a | ||
* Spring Boot JAR that only has a {@code BOOT-INF/classes/} directory entry, but no further directory entries | ||
* for the packages contained, then we wouldn't find the classes in this package right now. | ||
* Since we don't know if this really happens out there in the wild we keep it like this for now and see if | ||
* this problem will ever occur (because to solve it is likely not trivial at all and might in the end require | ||
* a custom extension point for frameworks). | ||
* Note that we don't need to test the case where {@code BOOT-INF/classes/} has no directory entry, | ||
* because in that case deriving the nested archive already fails with an exception. So it's a valid assumption | ||
* that this directory entry always exists out in the wild. | ||
*/ | ||
@Test | ||
public void does_not_find_locations_of_packages_from_Spring_Boot_ClassLoader_for_JARs_without_directory_entries() throws Exception { | ||
try (JarFile jarFile = new TestJarFile() | ||
.withoutDirectoryEntries() | ||
.withNestedClassFilesDirectory("BOOT-INF/classes") | ||
.withEntry(classFileEntry(SomeEnum.class).toAbsolutePath()) | ||
.create()) { | ||
|
||
configureSpringBootContextClassLoaderKnowingOnly(jarFile); | ||
|
||
// Also configure classpath to match, so we principally would have a chance to find the class file there | ||
System.setProperty(JAVA_CLASS_PATH_PROP, uriOf(jarFile)); | ||
|
||
// We still don't find any class files, because reading the JAR from the classpath gives us | ||
// resource entries of the form `BOOT-INF/classes/...` which don't match the expected package name | ||
// `com.tngtech...`. Without telling ArchUnit somehow that there is a nested archive | ||
// within `BOOT-INF/classes` there doesn't seem to be any robust solution for this | ||
// (unless the classpath would contain a URL of the form `jar:file:/.../some.jar!/BOOT-INF/classes!/` | ||
// with two separators, then the customized URL handling would kick in. But realistic usage AFAIS just adds | ||
// the base JAR URL and then lets the Spring Boot `JarLauncher` handle the nested archive logic). | ||
Stream<Location> locationsInMatchingJarFile = Locations.ofPackage(SomeEnum.class.getPackage().getName()).stream() | ||
.filter(it -> it.contains(jarFile.getName())); | ||
assertThat(locationsInMatchingJarFile).isEmpty(); | ||
} | ||
} | ||
|
||
private static void configureSpringBootContextClassLoaderKnowingOnly(JarFile jarFile) throws IOException { | ||
// This hooks in Spring Boot's own JAR URL protocol handler which knows how to handle URLs with | ||
// multiple separators (e.g. "jar:file:/dir/some.jar!/BOOT-INF/classes!/pkg/some.class") | ||
org.springframework.boot.loader.jar.JarFile.registerUrlProtocolHandler(); | ||
|
||
try (JarFileArchive jarFileArchive = new JarFileArchive(new File(jarFile.getName()))) { | ||
JarFileArchive bootInfClassArchive = getNestedJarFileArchive(jarFileArchive, "BOOT-INF/classes/"); | ||
|
||
Thread.currentThread().setContextClassLoader( | ||
new LaunchedURLClassLoader(false, bootInfClassArchive, new URL[]{bootInfClassArchive.getUrl()}, null) | ||
); | ||
} | ||
} | ||
|
||
@SuppressWarnings("SameParameterValue") | ||
private static JarFileArchive getNestedJarFileArchive(JarFileArchive jarFileArchive, String path) throws IOException { | ||
Iterator<Archive> archiveCandidates = jarFileArchive.getNestedArchives(entry -> entry.getName().equals(path), entry -> true); | ||
return (JarFileArchive) getOnlyElement(archiveCandidates); | ||
} | ||
|
||
private static String uriOf(JarFile jarFile) { | ||
return URI.create("jar:" + new File(jarFile.getName()).toURI()).toString(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters