TNG / ArchUnit

A Java architecture test library, to specify and assert architecture rules in plain Java
http://archunit.org
Apache License 2.0
3.18k stars 288 forks source link

fails with Spring Boot Nested Jars #1224

Closed wolframhaussig closed 6 months ago

wolframhaussig commented 8 months ago

I am using Spring Modulith which internally uses ArchUnit. When starting the application I get the following error:

07:05:08.787 [main] WARN com.tngtech.archunit.core.importer.ClassFileImporter -- Couldn't derive ClassFileSource from Location{uri=jar:nested:... com.tngtech.archunit.base.ArchUnitException$UnsupportedUriSchemeException: The scheme of the following URI is not (yet) supported: nested:...

Can you please support the nested format? I need the information at runtime to get a list of all modules.

Java: Zulu 21 ArchUnit: 1.1.0

Update: Example project: demo.zip

The error only occurs when starting the jar file

odrotbohm commented 7 months ago

We just spoke at OOP, I'll have a look at the reproducer.

araragao commented 7 months ago

+1

Base image: eclipse-temurin:21.0.2_13-jdk-jammy ArchUnit: 1.1.0

2024-02-09T19:28:56.320Z  INFO 1 --- [cTaskExecutor-1] com.tngtech.archunit.core.PluginLoader   : Detected Java version 21.0.2
2024-02-09T19:28:57.193Z  WARN 1 --- [cTaskExecutor-1] c.t.a.core.importer.ClassFileImporter    : Couldn't derive ClassFileSource from Location{uri=jar:nested:/{work_dir}/{jar_name}.jar/!BOOT-INF/classes/!/com/my/project/}
           at com.tngtech.archunit.core.importer.Location.of(Location.java:195) ~[archunit-1.1.0.jar!/:1.1.0]
           at com.tngtech.archunit.core.importer.Location.of(Location.java:184) ~[archunit-1.1.0.jar!/:1.1.0]
           at com.tngtech.archunit.core.importer.ClassFileSource$FromJar$ClassFileInJar.makeJarUri(ClassFileSource.java:150) ~[archunit-1.1.0.jar!/:1.1.0]
           (...)
wolframhaussig commented 6 months ago

We just spoke at OOP, I'll have a look at the reproducer.

@odrotbohm did you already have time to look at the reproduction?

odrotbohm commented 6 months ago

This seems to be caused by this change shipped with Spring Boot 3.2. In contrast to what the commit message suggests, Boot's previous URLs started with jar:file:… and I have a local patch of ArchUnit's Location.FilePathLocationFactory that basically treats nested like file reinstantiate the old behavior.

@codecholeric – Do you think you could tweak the Location implementation to treat a nested sub-protocol like file?

odrotbohm commented 6 months ago

I just debugged this with the Boot team and it looks like the situation is slightly different. JarURLConnection has both getUrl() and getJarFileUrl(), the latter being used in ClassFileSource. The former returns the full URL (in case of Boot jar:nested:…) the latter is supposed the URI within the JAR (nested:…). Unfortunately, Boot 3.1 also returned the full URL for getJarFileUrl() so that ArchUnit would still resolve jar:file. This is now fixed in Boot 3.2 but reveals, that ArchUnit actually needs to call getUrl() to compose a URI pointing to the JarEntry resource within the JAR. A local copy of ClassFileSource using that method fixes the bug, and ArchUnit's build is green with it as well.

codecholeric commented 6 months ago

@odrotbohm so, you basically say we should use getUrl() instead of getJarFileUrl() in https://github.com/TNG/ArchUnit/blob/main/archunit/src/main/java/com/tngtech/archunit/core/importer/ClassFileSource.java#L150 ? I'm wondering if there is a little more documentation about this somewhere 🤔 Because to me the Javadoc really doesn't speak much

public URL getJarFileURL()
Returns the URL for the Jar file for this connection.

vs

public URL getURL()
Returns the value of this URLConnection's URL field
odrotbohm commented 6 months ago

Exactly. That's what @philwebb pointed out as, apparently, the assumption is that getUrl() returns the full URL, whereas getJarFileURL() is supposed to return the URL within the JAR.

codecholeric commented 6 months ago

If it's just that I should be able to create a quick fix and release a bugfix version 🙂 Just hope it doesn't break something on some other end, that URL handling is ... not my favorite experience so far 🤪 (also remembering some early troubles before we had a Windows CI environment)

odrotbohm commented 6 months ago

The ArchUnit build stays green with the suggested fix applied, if that's helpful. 😬

philwebb commented 6 months ago

Because to me the Javadoc really doesn't speak much

This is indeed true! Whilst working on Spring Boot's nested jar support I had to dig quite deeply into the JDK code to really understand what's going on. My understanding is that a typical jar URL looks something like this:

jar:file:/some/file.jar!/com/example/MyClass.class

This URL can be thought of as three distinct parts:

[jar:]   [file:/some/file.jar] !/ [com/example/MyClass.class]
^        ^                        ^
scheme   jar file URL             path within jar

Typically the "jar file URL" is a file: URL, but it doesn't need to be. It can be any URL that provides access to the jar content. The JDK optimizes for file paths, but can deal with any URL. For Spring Boot, we have a new URL scheme called nested:, so our URL now look like this:

jar:nested:/some/file.jar/!BOOT-INF/lib/nested.jar!/com/example/MyClass.class

I didn't dig too deeply into the ArchUnit code, but I'm guessing you're placing the path section of the URL with something else. If you call getJarFileURL() to build this URL you'll get this:

jar:file:/some/file.jar!/com/example/MyClass.class -> file:/some/file.jar!/com/example/SomeOther.class
jar:nested:/some/file.jar/!BOOT-INF/lib/nested.jar!/com/example/MyClass.class -> nested:/some/file.jar/!BOOT-INF/lib/nested.jar!/com/example/SomeOtherClass.class

I think the you get away with the file version because you have code to handle that scheme. The nested scheme doesn't work because you have no knowledge of it.

If you change to using getURL() you should get this:

jar:file:/some/file.jar!/com/example/MyClass.class -> jar:file:/some/file.jar!/com/example/SomeOther.class
jar:nested:/some/file.jar/!BOOT-INF/lib/nested.jar!/com/example/MyClass.class -> jar:nested:/some/file.jar/!BOOT-INF/lib/nested.jar!/com/example/SomeOtherClass.class

That keeps the jar: scheme and means your existing jar handling should work.

At least that's my understanding of things from the limted amount of time I spend investigating with @odrotbohm.

codecholeric commented 6 months ago

Thanks a lot @philwebb , that really helps me to understand 😃 I created #1259 now to fix this.