1

This is similar to Exclude dependency in child pom inherited from parent pom, except that it has to do with test vs compile scopes.

I have a parent POM that includes the org.slf4j:slf4j-api dependency so that all descendant projects will be using SLF4J for the logging API. Then, so that all projects can have some logging for unit tests (regardless of which SLF4J implementation they use in the main, that is non-test, part of the project), I include SLF4J Simple, but only in the test scope:

<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-simple</artifactId>
  <scope>test</scope>
</dependency>

(I understand the view that parent POMs should not declare dependencies and only use dependency management. While I don't disagree in general, configuring tests is a different story. I don't want every single subproject to have to declare JUnit, Hamcrest, Hamcrest Optional, Mockito, Simple Logging, etc. The testing framework should be uniform across all our projects without a huge amount of ceremony just to set up a project.)

This works fine until one project Foo wants to use Logback as the SLF4J implementation.

<dependency>
  <groupId>ch.qos.logback</groupId>
  <artifactId>logback-classic</artifactId>
  <version>1.4.1</version>
</dependency>

That works fine for the Foo application itself, but now for the Foo tests, there are suddenly two competing SLF4J implementations: Logback and SLF4J simple. This presents a bindings conflict:

SLF4J: Class path contains multiple SLF4J providers.
SLF4J: Found provider [ch.qos.logback.classic.spi.LogbackServiceProvider@363ee3a2]
SLF4J: Found provider [org.slf4j.simple.SimpleServiceProvider@4690b489]
SLF4J: See https://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J: Actual provider is of type [ch.qos.logback.classic.spi.LogbackServiceProvider@363ee3a2]

I need to do one of the following:

  • In the POM where I bring in the ch.qos.logback:logback-classic dependency, I need to exclude the org.slf4j:slf4j-simple from the parent POM. (This is the preferred solution.)
  • In the POM where I bring in the ch.qos.logback:logback-classic dependency, I need to specify that ch.qos.logback:logback-classic is for all scopes except the test scope (so as not to conflict with org.slf4j:slf4j-simple).

I don't readily see how to do either of these. Any ideas?

One suggestion was to redeclare org.slf4j:slf4j-simple with <scope>provided</scope>. Thus pom.xml for project Foo would look like this:

…
<dependency>
  <groupId>ch.qos.logback</groupId>
  <artifactId>logback-classic</artifactId>
</dependency>

<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-simple</artifactId>
  <scope>provided</scope>
</dependency>
…

Unfortunately that doesn't work. SLF4J still sees two SLF4J providers on the classpath, and is showing the message seen above. A scope of provided simply keeps the dependency from being included transitively in other projects; it doesn't seem to remove it from the classpath of the current project.

Garret Wilson
  • 18,219
  • 30
  • 144
  • 272
  • 2
    I do believe, it is not a good idea to specify dependencies in parent poms, at first glance it looks attractive, however as soon as you started overriding those dependencies your project turns into mess. BTW, you may specify scope=`provided` for that dependency in child module in order to ban it – Andrey B. Panfilov Sep 16 '22 at 15:44
  • You can solve this (as the referred question) with profiles. – xerx593 Sep 16 '22 at 15:50
  • Define the dependencies you need in your parent pom via dependencyManagement instead and define there the exclusions you really need. But I would recommend not to use a parent pom for such purposes better define you own so called BOM (a pom which contains only a dependencyManagement) as many frameworks do (spring boot, etc.) – khmarbaise Sep 16 '22 at 18:24
  • @xerx593 Never use profiles for dependencies. – khmarbaise Sep 16 '22 at 18:24
  • "I do believe, it is not a good idea to specify dependencies in parent poms …" I don't disagree, except for test configuration; see my updated question. "[Y]ou may specify scope=`provided` for that dependency in child module …" No, unfortunately that still isn't working. SLF4J still sees two SLF4J providers on the classpath. `provided` simply keeps it from being included transitively in other projects; it doesn't seem to remove it from the classpath of the current project. – Garret Wilson Sep 16 '22 at 20:13
  • "… better define you own so called BOM …" I am well acquainted with a build-of-materials POM, but for configuring the _test framework_ across our projects, it works better to actually declare the dependencies in the parent POM. – Garret Wilson Sep 16 '22 at 20:16
  • What about the other option: how could I declare `ch.qos.logback:logback-classic` for all scopes _except_ `test` scope? – Garret Wilson Sep 16 '22 at 20:22
  • @GarretWilson sorry, I have completely forgotten that `test` scope is "special", for you particular case it is possible to take advantage of [classpathDependencyExcludes config option](https://maven.apache.org/surefire/maven-surefire-plugin/test-mojo.html#classpathDependencyExcludes) – Andrey B. Panfilov Sep 17 '22 at 10:25
  • Andrey I had seen this Surefire plugin exclusion. But how does that help with transitive dependency references? So if MyApp extends from MyRoot, which declares `slf4j-simple` in `test` scope; but then has a dependency on `BaseApp` which has `logback-classic` as a dependency, even if `BaseApp` excludes `slf4j-simple` from the Surefire plugin, that will have no effect on `MyApp`, and `MyApp` will wind up with both `slf4j-simple` and `logback-classic` on the classpath. On the other hand, how could `BaseApp` ever remove something from `MyApp`'s inheritance? I'll need to think more about this … – Garret Wilson Sep 17 '22 at 15:00

1 Answers1

0

It sounds like you are trying to build the Cathedral using wrong tools and instead of Cathedral you are getting pagan temple :)

  1. technically, it is possible to override classpath/module dependencies imposed by parent pom by defining system scope, something like:
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-simple</artifactId>
    <version>2.0.1</version>
    <scope>system</scope>
    <systemPath>${project.basedir}/../dummy.jar</systemPath>
</dependency>

however, I wouldn't recommend to do that

  1. another option is to take advantage of classpathDependencyExcludes config option of surefire plugin, something like:
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <configuration>
        <classpathDependencyExcludes>org.slf4j:slf4j-simple</classpathDependencyExcludes>
    </configuration>
</plugin>
  1. If particular parent does not suit child's needs, child may adopt another parent :) There is no strict requirement that the aggregator pom must be the parent pom
  2. the real problem is unlike modern build tools maven does not distinguish test compile and test runtime scopes, however it is possible to emulate such behaviour
<properties>
    <surefire.runtime>${project.build.directory}/surefire-runtime/slf4j-simple-2.0.1.jar</surefire.runtime>
</properties>

...

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-dependency-plugin</artifactId>
    <executions>
        <execution>
            <id>copy-surefire-runtime</id>
            <goals>
                <goal>copy</goal>
            </goals>
            <configuration>
                <artifactItems>
                    <artifactItem>
                        <groupId>org.slf4j</groupId>
                        <artifactId>slf4j-simple</artifactId>
                        <version>2.0.1</version>
                        <type>jar</type>
                        <overWrite>false</overWrite>
                        <outputDirectory>${project.build.directory}/surefire-runtime/</outputDirectory>
                    </artifactItem>
                </artifactItems>
            </configuration>
        </execution>
    </executions>
</plugin>
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <configuration>
        <additionalClasspathElements>${surefire.runtime}</additionalClasspathElements>
    </configuration>
</plugin>

yep, too many words there, but in my opinion that is only correct configuration for test runtime dependencies, m.b. it worth to submit a corresponding PR to surefire project - I believe that needs to write about 10 LoC to avoid maven-dependency-plugin configuration and able to configure test runtime in the following way:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <configuration>
        <additionalClasspathElements>
            <additionalClasspathElement>org.slf4j:slf4j-api:2.0.1</additionalClasspathElement>
        </additionalClasspathElements>
    </configuration>
</plugin>
Andrey B. Panfilov
  • 4,324
  • 2
  • 12
  • 18