If two included dependencies contain a module with the same name and same version number (which is unlikely, but might happen) the bootJar task in its current default behavior will generate a .jar file with colliding entries.

For example, these two libraries by accident contain the same module (but which are completely unrelated) and have by accident the same version string. (Which in it self should be no problem)

dependencies {
...
    implementation "org.web3j:core:3.4.0"
    implementation "com.google.zxing:core:3.4.0"
}

But the default bootJar task:

bootJar {
    launchScript()
    enabled = true
    archiveBaseName.set('foo')
}

.. will build without error and the resulting .jar file might also be runnable, but will crash non-deterministically on runtime either with - java.lang.NoClassDefFoundError: com/google/zxing/WriterException
- or java.lang.NoClassDefFoundError: org/web3j/protocol/Web3j

depending in which order the two files where included in the .jar file.

Unziping the bootjar file results in a overwrite-existing-file-warning:

> unzip foo.jar                   
Archive:  foo.jar
   creating: META-INF/
  inflating: META-INF/MANIFEST.MF    
....
 extracting: BOOT-INF/lib/core-3.4.0.jar  
 extracting: BOOT-INF/lib/abi-3.4.0.jar  
 extracting: BOOT-INF/lib/crypto-3.4.0.jar  
...
 extracting: BOOT-INF/lib/javax.json-1.1.4.jar  
 extracting: BOOT-INF/lib/barcode4j-2.1.jar  
replace BOOT-INF/lib/core-3.4.0.jar? [y]es, [n]o, [A]ll, [N]one, [r]ename: 

And listing its content shows that it includes the same file with two different file-sizes:

> unzip -l foo.jar | grep core-3.4
   244695  2020-04-16 15:38   BOOT-INF/lib/core-3.4.0.jar
   539901  2020-04-16 15:38   BOOT-INF/lib/core-3.4.0.jar

Improvement

One easy improvement would be to default to duplicatesStrategy(DuplicatesStrategy.FAIL) for the bootJar task. I think there is no valid use-case to include a colliding filename twice in the jar without notifying the developer.

Which will lead to

* What went wrong:
Execution failed for task ':foo:bootJar'.
> Encountered duplicate path "BOOT-INF/lib/core-3.4.0.jar" during copy operation configured with DuplicatesStrategy.FAIL

Workaround

Our workaround (also based on https://github.com/spring-projects/spring-boot/issues/10778 ) is to include the group id of the included modules:

bootJar {
...
    rootSpec.filesMatching('**/*.jar', { jar ->
        // only rename jars which are going into the lib dir into the fat jar
        if (jar.path.startsWith("BOOT-INF/lib")) {
            // rename the destination from "module-version.jar" to "group-module-version.jar"
            // eg
            //   BOOT-INF/lib/core-3.4.0.jar
            //   BOOT-INF/lib/core-3.4.0.jar
            // becomes
            //   BOOT-INF/lib/org.web3j-core-3.4.0.jar
            //   BOOT-INF/lib/com.google.zxing-core-3.4.0.jar
            String groupId = jar.file.parentFile.parentFile.parentFile.parentFile.name
            jar.name = "$groupId-${jar.name}"
        }
    })

    // to play it save, set this from IGNORE to FAIL, so that gradle aborts the build if it tries to include
    // two files with the same name
    duplicatesStrategy(DuplicatesStrategy.FAIL)
}

Comment From: philwebb

It's interesting that Gradle's War task doesn't change the default DuplicationStrategy. There some discussion about that here.

I'm not sure if it's best that we align with the War task, or change ours. The current situation certainly isn't great.

Comment From: wilkinsona

This is a duplicate of https://github.com/spring-projects/spring-boot/issues/16241. That issue led to use deciding to remain aligned with the behaviour of Gradle's War task. Things have moved on a little since then as, with the work on layered jars, we now have the capability of accessing the dependency coordinates for some (and typically all) of the jars going into BOOT-INF/lib. As such, I think it's worth considering this one again.

Comment From: DanielWeigl

My thinking here is, that BootJar will always emit a .jar file, which is inherently a .zip compressed file (or?) -> ZIP on its own allows multiple entries with the same name to be added into one file. But most tools won't handle files with colliding entries correctly (ie. Linux ARK tool crashes, unzip asks if you want to replace an existing file, there was a android package signing issue which exploited this undefined behavior, etc).

Also the class-loader (at least the one from OpenJDK 1.8) only loads one of the included modules from the .jar file.

So in my opinion it never makes sense to have a colliding file/module in a .jar file - or? Then it would at least make sense to fail at building (i.e. duplicatesStrategy(DuplicatesStrategy.FAIL)) but also allow a developer to overwrite the behavior if really needed for what ever reasons.

Comment From: wilkinsona

@DanielWeigl I don't think I disagree with any of that, but I also like the fact that we're currently aligned with Gradle's default behaviour for both jar and war artifacts.

Comment From: DanielWeigl

im not very used to .war artifacts, does it make sense for them to contain colliding files/libs? Then its more of an upstream bug.

But I also dont really see a big issue to not align with .war logic in this regards, if it means to handle an error as early as possible.

It took us quite a long time to track down the issue, because it only occurred randomly and only after we updated a very unrelated library (which lead to include this dependencies with the exact same version string)

Comment From: DanielWeigl

Ah, I see there is already some progress in this regards upstream in gradle: eg. https://github.com/gradle/gradle/issues/10856

But accordingly to https://github.com/gradle/gradle/issues/10039#issuecomment-535577223 it should emit a warning (we are using gradle 6.3), but even ./gradlew build --warning-mode all does not show anything regards duplicate file names... so not sure if it also affects the war/jar task.

Demo project here: https://gitlab.com/DanielWeigl/duplicatestrategyfailingdemo

Comment From: wilkinsona

Thanks for the demo project. It produces the expected warning for me:

$ ./gradlew bootJar --warning-mode all

> Task :bootJar
Copying or archiving duplicate paths with the default duplicates strategy has been deprecated. This is scheduled to be removed in Gradle 7.0. Duplicate path: "BOOT-INF/lib/core-3.4.0.jar". Explicitly set the duplicates strategy to 'DuplicatesStrategy.INCLUDE' if you want to allow duplicate paths. Consult the upgrading guide for further information: https://docs.gradle.org/6.3/userguide/upgrading_version_5.html#implicit_duplicate_strategy_for_copy_or_archive_tasks_has_been_deprecated

BUILD SUCCESSFUL in 1s
3 actionable tasks: 3 executed

Comment From: DanielWeigl

Oh - was just trying again, and now i see it too, with a clean

#> ./gradlew clean bootJar --warning-mode all

> Task :bootJar
Copying or archiving duplicate paths with the default duplicates strategy has been deprecated. This is scheduled to be removed in Gradle 7.0. Duplicate path: "BOOT-INF/lib/core-3.4.0.jar". Explicitly set the duplicates strategy to 'DuplicatesStrategy.INCLUDE' if you want to allow duplicate paths. Consult the upgrading guide for further information: https://docs.gradle.org/6.3/userguide/upgrading_version_5.html#implicit_duplicate_strategy_for_copy_or_archive_tasks_has_been_deprecated

BUILD SUCCESSFUL in 2s
4 actionable tasks: 4 executed

#> ./gradlew bootJar --warning-mode all      

BUILD SUCCESSFUL in 1s
3 actionable tasks: 3 up-to-date

I probably first build it without warning-mode all and then did not realize, that the BootJar task got cached.... damn.

So, i guess we are in the clear here, and just need to wait for gradle 7. Thx for testing.

I mean - it will make the problem of colliding module-names more visible, but I guess the BootJar task will still need to provide a workaround on how to handle this situation.

Comment From: philwebb

We've decided to keep things aligned with the Gradle defaults for now. When Gradle 7 comes out, we'll pick up the duplication failure error for free.