blob: ea2d453d053293db98901a856a72dacd9c0708c2 [file] [log] [blame] [view]
# Common Misconceptions
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
Due to smooth transitions from Maven2 into Maven3 (and soon
Maven4), and the fact that Maven2 plugins kept working with Maven3, maybe
even without change, there were some misconceptions crept in
as well. Despite the marvel of "compatibility", Maven3 resolution
differs quite much from Maven2, and the sole reason is actual improvement
in area of resolution, it became much more precise (and, due
that lost some "bad" habits present in Maven2). Here, we will try to
enumerate some of the most common misconceptions.
## Misconception No1: How Resolver Works
(Simplified)
The most typical use case for Resolver is to "resolve transitively"
dependencies. Resolver, to achieve this, internally (but these are
exposed via API as distinguished API calls as well) performs 3 steps:
"collect", "transform" and "resolve".
The "collect" step is first, where it builds the "dirty tree" (dirty graph)
of artifacts. It is important to remark, that in "collect" step, while
the graph is being built, Maven uses only POMs. Hence, if collecting an
Artifact that was never downloaded to your local repository, it will
download **the POMs only**. Using POMs resolver is able to build current
"node" of graph, but also figure outgoing vertices and adjacent nodes of
current node and so on. Which dependency is chosen to continue with from
the current node POM is decided by various criteria (configured).
The "transform" step transforms the "dirty graph": this is where conflict resolution
happens. It is here when resolver applies various rules to resolve conflicting
versions, conflicting scopes, and so on. Here, if "verbose tree" is asked for,
conflict resolution does not remove graph nodes, merely marks the conflicts
and the conflict "winner". Thus, "verbose tree" cannot be resolved.
Finally, the "resolve" step runs, when the (transformed) graph node artifacts
are being resolved, basically ensuring (and downloading if needed) their
correspondent files (i.e. JAR files) are present in local repository.
It is important to state, that in "collect" step happens the selection of nodes
by various criteria, among other by the configured scope filters. And here we
come to the notion of "runtime graph" vs "test graph".
In resolver, maybe un-intuitively, the "scope filter" is usually used (but does
not have to, this is just how it IS used in Maven Core, probably for historical
reasons) as "what should be omitted". The default session filter in Maven
is set up as this:
```
new ScopeDependencySelector("test", "provided")
```
This means, that "current dependency node" dependencies in "test" and "provided" scope
will be simply omitted from the graph. In other words, this filter builds
the "downstream runtime classpath" of supplied artifact (i.e. "what is needed by the
artifact at runtime when I depend on it").
Note: these are NOT "Maven related" notions yet, there is nowhere Maven in picture here,
and these are not the classpath used by Compiler or Surefire plugins, merely just
a showcase how Resolver works.
## Misconception No2: "Test graph" Is Superset Of "Runtime graph"
**Wrong**. As can be seen from above, for runtime graph we leave out "test" scoped
dependencies. It was true in Maven2, where test graph really was superset of runtime,
this does not stand anymore in Maven3. And this have interesting consequences. Let me show an example:
(Note: very same scenario, as explained below for Guice+Guava would work for Jackson Databind+Core, etc.)
Assume your project is using Google Guice, so you have declared it as a dependency:
```
<dependency>
<groupId>com.google.inject</groupId>
<artifactId>guice</artifactId>
<version>${guiceVersion}</version>
</dependency>
```
All fine and dandy. At the same time, you want to avoid any use of Guava. We all know Guava is a direct dependency
of Guice. This is fine, since as we know, the best practice is to declare all dependencies your code compiles
against. By not having Guava here, analyse tools will report if code touches Guava as "undeclared dependency".
But let's go one step further: turns out, to set up your unit tests, you **do need** Guava. So what now? Nothing, just
add it as a test dependency, so your POM looks like this:
```
<dependency>
<groupId>com.google.inject</groupId>
<artifactId>guice</artifactId>
<version>${guiceVersion}</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guavaVersion}</version>
<scope>test</scope>
</dependency>
```
The `dependency:tree` plugin for this project outputs this verbose tree:
```
[INFO] --- dependency:3.6.1:tree (default-cli) @ DEMO-PROJECT ---
[INFO] DEMO-PROJECT
[INFO] +- com.google.inject:guice:jar:6.0.0:compile
[INFO] | +- javax.inject:javax.inject:jar:1:compile
[INFO] | +- jakarta.inject:jakarta.inject-api:jar:2.0.1:compile
[INFO] | +- aopalliance:aopalliance:jar:1.0:compile
[INFO] | \- (com.google.guava:guava:jar:31.0.1-jre:compile - omitted for duplicate)
[INFO] \- com.google.guava:guava:jar:31.0.1-jre:test (scope not updated to compile)
[INFO] +- com.google.guava:failureaccess:jar:1.0.1:test
[INFO] +- com.google.guava:listenablefuture:jar:9999.0-empty-to-avoid-conflict-with-guava:test
[INFO] +- com.google.code.findbugs:jsr305:jar:3.0.2:test
[INFO] +- org.checkerframework:checker-qual:jar:3.12.0:test
[INFO] +- com.google.errorprone:error_prone_annotations:jar:2.7.1:test
[INFO] \- com.google.j2objc:j2objc-annotations:jar:1.3:test
```
And is right, this IS the "test graph" **of the project** and contains a conflict as noted by "omitted for duplicate"
and "scope not updated to compile" remarks next to Guava nodes.
So this setup results that:
* when you compile, it ensures Guava is NOT on compile classpath, so you cannot even touch it (by mistake)
* when test-compile and test-execute runs, Guava will be present on classpath, as expected
So far good, but what happens when this library is consumed downstream by someone? When it becomes used as a library?
Nothing, all works as expected!
When a downstream dependency declares dependency on this project, the downstream project will get this graph (from
the node that is your library):
```
[INFO] --- dependency:3.6.1:tree (default-cli) @ DOWNSTREAM-PROJECT ---
[INFO] DOWNSTREAM-PROJECT
[INFO] \- DEMO-PROJECT:compile
[INFO] \- com.google.inject:guice:jar:6.0.0:compile
[INFO] +- javax.inject:javax.inject:jar:1:compile
[INFO] +- jakarta.inject:jakarta.inject-api:jar:2.0.1:compile
[INFO] +- aopalliance:aopalliance:jar:1.0:compile
[INFO] \- com.google.guava:guava:jar:31.0.1-jre:compile
[INFO] +- com.google.guava:failureaccess:jar:1.0.1:compile
[INFO] +- com.google.guava:listenablefuture:jar:9999.0-empty-to-avoid-conflict-with-guava:compile
[INFO] +- com.google.code.findbugs:jsr305:jar:3.0.2:compile
[INFO] +- org.checkerframework:checker-qual:jar:3.12.0:compile
[INFO] +- com.google.errorprone:error_prone_annotations:jar:2.7.1:compile
[INFO] \- com.google.j2objc:j2objc-annotations:jar:1.3:compile
```
So what happens here? First, revisit "How Resolver Works", there you will see that for "runtime graph" of the
dependency the "test" and "provided" scopes of the dependency artifact **are not even considered**. They are simply
omitted. Not skipped, but completely omitted, like they do not even exists. Hence, in the graph there is
**no conflict happening** (as "test" Guava is completely omitted during "collect" step). Hence, everything
goes as expected.
### Important Consequences
One, maybe not so obvious consequence can be explained with use of `maven-assembly-plugin`. Let assume you want to
assemble your module "runtime" dependencies.
If you do it from "within" of the project, for example in package phase, your packaging will be incomplete:
Guava will be missing! But if you do it from "outside" of the project (i.e. subsequent module of the build, or
downstream dependency), the assembly will contain Guava as well.
This is a [Maven Assembly plugin bug](https://issues.apache.org/jira/browse/MASSEMBLY-1008), somewhat explained
in [MRESOLVER-391](https://issues.apache.org/jira/browse/MRESOLVER-391). In short, Maven Assembly plugin considers
"project test graph", and then "cherry-picks runtime scoped nodes" from it, which, as we can see in this case,
is wrong. You need to build different graphs for "runtime" and "test" classpath, unlike as it was true in Maven2.
For Assembly plugin, the problem is that as Mojo, it requests "test graph", then it reads configuration
(assembly descriptor, and this is the point where it learns about required scopes), and then it "filters"
the resolved "test graph" for runtime scopes. And it is wrong, as Guava is in test scope. Instead, the plugin
should read the configuration first, and ask Resolver for "runtime graph" and filter that. In turn, this problem
does not stand with `maven-war-plugin`, as the "war" Mojo asks resolution of "compile+runtime" scope. Of course,
WAR use case is much simpler than Assembly use case is, as former always packages same scope, while Assembly receives
a complex configuration and exposes much more complex "modus operandi".