Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fails with Spring Boot Nested Jars #1224

Closed
wolframhaussig opened this issue Jan 3, 2024 · 11 comments · Fixed by #1259
Closed

fails with Spring Boot Nested Jars #1224

wolframhaussig opened this issue Jan 3, 2024 · 11 comments · Fixed by #1259

Comments

@wolframhaussig
Copy link

wolframhaussig commented Jan 3, 2024

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
Copy link
Contributor

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

@araragao
Copy link

araragao commented Feb 9, 2024

+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
Copy link
Author

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
Copy link
Contributor

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
Copy link
Contributor

odrotbohm commented Feb 23, 2024

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
Copy link
Collaborator

@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
Copy link
Contributor

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
Copy link
Collaborator

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
Copy link
Contributor

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

@philwebb
Copy link

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
Copy link
Collaborator

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

codecholeric added a commit that referenced this issue Mar 6, 2024
For typical plain JAR URLs this doesn't make a real difference, but for
special JAR URLs, like Spring Boot uses, it does. The problem showed
with nested JAR URLs of Spring Boot. Those have a format like
```
jar:nested:/some/file.jar/!BOOT-INF/lib/nested.jar!/
```
Here the `connection.getJarFileURL()` is
```
nested:/some/file.jar/!BOOT-INF/lib/nested.jar
```
but the `connection.getURL()` is
```
jar:nested:/some/file.jar/!BOOT-INF/lib/nested.jar!/
```
Using the latter yields the correct result and allows the custom JAR URL
handler to kick in. Using the former will yield an exception that
ArchUnit doesn't understand the scheme `nested`.

Resolves: #1224
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants