-
Notifications
You must be signed in to change notification settings - Fork 299
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Some `ClassLoader`s that work with repackaged JAR files return custom resource URIs to indicate custom class loading locations. For example, the `ClassLoader` in a packaged Spring Boot app returns the following URI for source package named example: `jar:file:/Path/to/my.jar!/BOOT-INF/classes!/example/`. Note the second "!/" to indicate a classpath root. Prior to this commit, `JarFileLocation` was splitting paths to a resource at the first "!/" assuming the remainder of the string would depict the actual resource path. That remainder potentially containing a further "!/" would prevent the JAR entry matching in `FromJar.classFilesBeneath(…)` as the entries themselves do not contain the exclamation mark. This changes the treatment of the URI in `JarFileLocation` to rather use the *last* "!/" as splitting point so that the remainder is a proper path within the `ClassLoader` and the matching in `FromJar.classFilesBeneath(…)` works properly. Note that in composition with custom `ClassLoader`s frameworks like Spring Boot can also install custom URL handling. In this case ArchUnit can read a class file from a nested archive URL like `jar:file:/some/file.jar!/BOOT-INF/classes!/...` using the standard `URL#openStream()` method in a completely transparent way (compare setup in `SpringLocationsTest`).
- Loading branch information
Showing
11 changed files
with
379 additions
and
36 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,16 @@ | ||
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.junit_dataprovider | ||
testImplementation dependency.assertj | ||
} |
96 changes: 96 additions & 0 deletions
96
...-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,96 @@ | ||
package com.tngtech.archunit.core.importer; | ||
|
||
import java.io.File; | ||
import java.io.IOException; | ||
import java.net.URL; | ||
import java.util.Arrays; | ||
import java.util.Iterator; | ||
import java.util.function.Function; | ||
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 com.tngtech.java.junit.dataprovider.DataProvider; | ||
import com.tngtech.java.junit.dataprovider.DataProviderRunner; | ||
import com.tngtech.java.junit.dataprovider.UseDataProvider; | ||
import org.junit.Rule; | ||
import org.junit.Test; | ||
import org.junit.runner.RunWith; | ||
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.java.junit.dataprovider.DataProviders.testForEach; | ||
import static org.assertj.core.api.Assertions.assertThat; | ||
|
||
@RunWith(DataProviderRunner.class) | ||
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(); | ||
|
||
@DataProvider | ||
public static Object[][] springBootJars() { | ||
Function<Function<TestJarFile, TestJarFile>, TestJarFile> createSpringBootJar = setUpJarFile -> setUpJarFile.apply(new TestJarFile()) | ||
.withNestedClassFilesDirectory("BOOT-INF/classes") | ||
.withEntry(classFileEntry(SomeEnum.class).toAbsolutePath()); | ||
|
||
return testForEach( | ||
createSpringBootJar.apply(TestJarFile::withDirectoryEntries), | ||
createSpringBootJar.apply(TestJarFile::withoutDirectoryEntries) | ||
); | ||
} | ||
|
||
@Test | ||
@UseDataProvider("springBootJars") | ||
public void finds_locations_of_packages_from_Spring_Boot_ClassLoader_for_JARs(TestJarFile jarFileToTest) throws Exception { | ||
try (JarFile jarFile = jarFileToTest.create()) { | ||
|
||
configureSpringBootContextClassLoaderKnowingOnly(jarFile); | ||
|
||
String jarUri = new File(jarFile.getName()).toURI().toString(); | ||
Location location = Locations.ofPackage(SomeEnum.class.getPackage().getName()).stream() | ||
.filter(it -> it.contains(jarUri)) | ||
.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(); | ||
} | ||
} | ||
|
||
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); | ||
} | ||
} |
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
Oops, something went wrong.