Skip to content

Commit

Permalink
Merge pull request #358 from spotify/upgrade-parent
Browse files Browse the repository at this point in the history
Upgrade project configuration
  • Loading branch information
caesar-ralf authored Feb 7, 2024
2 parents 891fdb7 + 957cfd4 commit bdfb0de
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 170 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:

strategy:
matrix:
java_version: [8, 11, 17]
java_version: [8, 11, 17, 21]
steps:
- uses: actions/checkout@v4
with:
Expand Down
206 changes: 78 additions & 128 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,27 @@
# missing-link - a maven dependency problem finder

[![Build Status](https://github.com/spotify/missinglink/actions/workflows/ci.yaml/badge.svg?branch=master)](https://github.com/spotify/missinglink/actions/workflows/ci.yaml)
[![GitHub license](https://img.shields.io/github/license/spotify/scio.svg)](LICENSE)
[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.spotify/missinglink-maven-plugin/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.spotify/missinglink-maven-plugin)
[![Coverage Status](https://coveralls.io/repos/spotify/missinglink/badge.svg?branch=master)](https://coveralls.io/r/spotify/missinglink?branch=master)

Be warned. This project is still immature and in development.
The API may change at any time.
It may not find all problems. It may find lots of false positives.
Be warned. This project is still immature and in development. The API may change at any time. It may not find all problems. It may find lots of false positives.

## Quickstart - add missinglink to your Maven build

Add the following plugin to pom.xml:

```xml

<plugin>
<groupId>com.spotify</groupId>
<artifactId>missinglink-maven-plugin</artifactId>
<version>0.2.1</version>
<executions>
<execution>
<goals><goal>check</goal></goals>
<goals>
<goal>check</goal>
</goals>
<phase>process-classes</phase>
</execution>
</executions>
Expand All @@ -28,137 +30,109 @@ Add the following plugin to pom.xml:

See [how to configure the plugin below](#configuration-of-the-plugin).


## Problem definition

When using Java and Maven, it's easy to get into a state of pulling in a lot of
dependencies. Sometimes you even get transitive dependencies (I depend on X
which in turn depends on Y). This can lead to conflicting dependencies
sometimes.
When using Java and Maven, it's easy to get into a state of pulling in a lot of dependencies. Sometimes you even get transitive dependencies (I depend on X which in turn depends on Y). This can lead to conflicting dependencies sometimes.

I depend on libraries X and Y. X depends on Foo v2.0.0 and Y depends on Foo
v3.0.0
I depend on libraries X and Y. X depends on Foo v2.0.0 and Y depends on Foo v3.0.0

Thus, I now have transitive dependencies on two different (incompatible)
versions of Foo. Which one do I pick?

If I pick v2.0.0, Y may fail in runtime due to missing classes or methods. If I
pick v3.0.0, X may fail instead.
If I pick v2.0.0, Y may fail in runtime due to missing classes or methods. If I pick v3.0.0, X may fail instead.

In order to solve this, maven has an enforcer plugin which can detect these
problems. Then you have to manually choose one of the versions and hope that it
works.
In order to solve this, maven has an enforcer plugin which can detect these problems. Then you have to manually choose one of the versions and hope that it works.

You can also try to upgrade library X to use Foo v3.0.0. Sometimes this is
tricky and time-consuming, especially if X is a foreign dependency.
You can also try to upgrade library X to use Foo v3.0.0. Sometimes this is tricky and time-consuming, especially if X is a foreign dependency.

## A new approach at solving some of the problems

The idea is to programmatically analyze each dependency - what does the code
depend on and what does it export - on a lower level. Instead of just looking
at version numbers, we look at the actual signatures in the code.
The idea is to programmatically analyze each dependency - what does the code depend on and what does it export - on a lower level. Instead of just looking at version numbers, we look at the actual signatures in the code.

For instance, maybe the difference between Foo v2.0.0 and Foo v3.0.0 is only
this method signature:
For instance, maybe the difference between Foo v2.0.0 and Foo v3.0.0 is only this method signature:

```java
// Foo v2.0.0
void Foo.bar(String s, int i);
void Foo.

bar(String s, int i);

// Foo v3.0.0
void Foo.bar(String s, boolean b);
void Foo.

bar(String s, boolean b);
```

If X or Y doesn't actually use this method, it may not matter if we're using
version 2 or 3. This is often the case of large libraries where we only use a
small subset of the methods (google guava for instance).
If X or Y doesn't actually use this method, it may not matter if we're using version 2 or 3. This is often the case of large libraries where we only use a small subset of the methods (google guava for instance).

(Note: I am only looking at this from an API perspective - the actual code may
have different behaviour which is out of scope for this project)
(Note: I am only looking at this from an API perspective - the actual code may have different behaviour which is out of scope for this project)

# Maven plugin

This problem finder can be executed against your Maven project from the
command-line like:
This problem finder can be executed against your Maven project from the command-line like:

```
$ mvn com.spotify:missinglink-maven-plugin:0.2.1:check
```

The plugin will scan the source code of the current project, the runtime
dependencies (from the Maven model), and the bootstrap JDK classes (i.e.
`java.lang`, `java.util`) for conflicts. Any conflicts found will be printed
out, grouped by category of the conflict, the artifact (jar file) it was found
it, and the problematic class.
The plugin will scan the source code of the current project, the runtime dependencies (from the Maven model), and the bootstrap JDK classes (i.e.
`java.lang`, `java.util`) for conflicts. Any conflicts found will be printed out, grouped by category of the conflict, the artifact (jar file) it was found it, and the problematic class.

## Requirements

This plugin is using Java 8 language features. While the JVM used to execute
Maven must be at version 1.8 or greater, the Maven projects being analyzed can
be using any Java source version.
This plugin is using Java 8 language features. While the JVM used to execute Maven must be at version 1.8 or greater, the Maven projects being analyzed can be using any Java source version.

Note that when using a higher JVM version to execute Maven than what the
project is being compiled with (the `source` argument to
`maven-compiler-plugin`), some care should be taken to make sure that the
higher-versioned bootclasspath is not accidentally used with javac.
Note that when using a higher JVM version to execute Maven than what the project is being compiled with (the `source` argument to
`maven-compiler-plugin`), some care should be taken to make sure that the higher-versioned bootclasspath is not accidentally used with javac.

## Configuration of the plugin

Once projects get to be of a certain size, some level of conflicts - mostly
innocent - between the various dependencies and inter-dependencies of the
libraries used are inevitable. In this case, you will probably want to add the
`missinglink-maven-plugin` as a `<plugin>` to your pom.xml so you can tweak some
of its configuration options.
Once projects get to be of a certain size, some level of conflicts - mostly innocent - between the various dependencies and inter-dependencies of the libraries used are inevitable. In this case, you will probably want to add the
`missinglink-maven-plugin` as a `<plugin>` to your pom.xml so you can tweak some of its configuration options.

For example, `ch.qos.logback:logback-core` includes a bunch of optional classes
that reference `groovy.lang` classes. Since the logback dependency specifies
its dependency on groovy as `optional=true`, the Groovy jar is not
automatically included in your project (unless you explicitly need it).
For example, `ch.qos.logback:logback-core` includes a bunch of optional classes that reference `groovy.lang` classes. Since the logback dependency specifies its dependency on groovy as `optional=true`, the Groovy jar is not automatically included in your project (unless you explicitly need it).

The `missinglink-maven-plugin` offers a few configuration options that can be used
to reduce the number of warnings to avoid drowning in "false" positives.
The `missinglink-maven-plugin` offers a few configuration options that can be used to reduce the number of warnings to avoid drowning in "false" positives.

The suggested workflow for using this plugin is to execute it against your
project once with no configuration, then carefully add dependencies/packages to
the ignores list after you are sure these are not true issues.
The suggested workflow for using this plugin is to execute it against your project once with no configuration, then carefully add dependencies/packages to the ignores list after you are sure these are not true issues.

To add the plugin to your project, add the following to the `<plugins>` section:

```xml

<plugin>
<groupId>com.spotify</groupId>
<artifactId>missinglink-maven-plugin</artifactId>
<version>VERSION</version>
</plugin>
```

The plugin can be specified to fail the build if any conflicts are found. To
automatically execute the plugin on each build, add an `<execution>` section
like:
The plugin can be specified to fail the build if any conflicts are found. To automatically execute the plugin on each build, add an `<execution>` section like:

```xml

<configuration>
<failOnConflicts>true</failOnConflicts>
</configuration>
<executions>
<execution>
<goals><goal>check</goal></goals>
<phase>process-classes</phase>
</execution>
<execution>
<goals>
<goal>check</goal>
</goals>
<phase>process-classes</phase>
</execution>
</executions>
```

### Exclude some dependencies from analysis

Specific dependencies can be excluded from analysis if you know that all
conflicts within that jar are "false" or irrelevant to your project.
Specific dependencies can be excluded from analysis if you know that all conflicts within that jar are "false" or irrelevant to your project.

For example, logback-core and logback-classic have many references (in optional
classes) to classes needed by the Groovy language. To exclude these jars from
being analyzed, add an `<excludeDependencies>` section to `<configuration>`
For example, logback-core and logback-classic have many references (in optional classes) to classes needed by the Groovy language. To exclude these jars from being analyzed, add an `<excludeDependencies>` section to `<configuration>`
like:

```xml

<excludeDependencies>
<excludeDependency>
<groupId>ch.qos.logback</groupId>
Expand All @@ -173,19 +147,16 @@ like:

### Ignore conflicts in certain packages

Conflicts can be ignored based on the package name of the class that has the
conflict. There are separate configuration options for ignoring conflicts on
the "source" side of the conflict and the "destination" side of the conflict.
Conflicts can be ignored based on the package name of the class that has the conflict. There are separate configuration options for ignoring conflicts on the "source" side of the conflict and the "destination" side of the conflict.

For example, if `com.foo.Bar` calls a method `void doSomething(int)` in the
`biz.blah.Something` class, then `com.foo.Bar` is on the source/calling side
and `biz.blah.Something` is on the destination/callee side.
`biz.blah.Something` class, then `com.foo.Bar` is on the source/calling side and `biz.blah.Something` is on the destination/callee side.

Packages on the source side can be ignored with `<ignoreSourcePackages>` and
packages on the destination side can be ignored with
Packages on the source side can be ignored with `<ignoreSourcePackages>` and packages on the destination side can be ignored with
`<ignoreDestinationPackages>`:

```xml

<configuration>
<!-- ignore conflicts with groovy.lang on the caller side -->
<ignoreSourcePackages>
Expand All @@ -203,23 +174,19 @@ packages on the destination side can be ignored with

```

By default, all subpackages of the specified packages are also ignored, but
this can be disabled on an individual basis by adding
By default, all subpackages of the specified packages are also ignored, but this can be disabled on an individual basis by adding
`<filterSubpackages>false</filterSubpackages>` to the `<ignoreSourcePackage>`
or `<ignoreDestinationPackage>` element. **Note**: In previous releases (<=0.2.5), this setting was named
`ignoreSubpackages`. Setting `ignoreSubpackages` in your pom.xml is still supported; the plugin will
translate it to the new key value.
`ignoreSubpackages`. Setting `ignoreSubpackages` in your pom.xml is still supported; the plugin will translate it to the new key value.

### Target only conflicts from certain packages

Conversely, the plugin can be configured to _only_ report on conflicts in specific packages, based on the name of the class that has the
conflict. There are separate configuration options for targeting conflicts on the "source" side of the conflict
and the "destination" side of the conflict.
Conversely, the plugin can be configured to _only_ report on conflicts in specific packages, based on the name of the class that has the conflict. There are separate configuration options for targeting conflicts on the "source" side of the conflict and the "destination" side of the conflict.

Packages on the source side can be targeted with `<targetSourcePackages>` and
packages on the destination side can be targeted with `<targetDestinationPackages>`:
Packages on the source side can be targeted with `<targetSourcePackages>` and packages on the destination side can be targeted with `<targetDestinationPackages>`:

```xml

<configuration>
<!-- Only target conflicts coming from groovy.lang source package -->
<targetSourcePackages>
Expand All @@ -236,93 +203,76 @@ packages on the destination side can be targeted with `<targetDestinationPackage
</configuration>
```

By default, all subpackages of the specified packages are also ignored, but
this can be disabled on an individual basis by adding
By default, all subpackages of the specified packages are also ignored, but this can be disabled on an individual basis by adding
`<filterSubpackages>false</filterSubpackages>` to the `<ignoreSourcePackage>`
or `<ignoreDestinationPackage>` element.

**Note** that `target*` options CANNOT be used in conjunction with `ignore*` options. You can only specify one or the other.

# Caveats and Limitations

Because this plugin analyzes the bytecode of the `.class` files of your code
and all its dependencies, it has a few limitations which prevent conflicts
from being found in certain scenarios.
Because this plugin analyzes the bytecode of the `.class` files of your code and all its dependencies, it has a few limitations which prevent conflicts from being found in certain scenarios.

## Reflection

When reflection is used to load a class or invoke a method, this tool is not
able to follow the call graph past the point of reflection.
When reflection is used to load a class or invoke a method, this tool is not able to follow the call graph past the point of reflection.

## Dependency Injection containers

Most DI containers, such as Guice, use reflection to load modules at runtime
and wire object graphs together; therefore this tool can't follow the
connection between your source code and any modules that might be loaded by
Guice or other containers from libraries on the classpath.
Most DI containers, such as Guice, use reflection to load modules at runtime and wire object graphs together; therefore this tool can't follow the connection between your source code and any modules that might be loaded by Guice or other containers from libraries on the classpath.

## Dead code

This tool parses the bytecode of each `.class` file and looks at the "method
instruction" calls to build a graph between classes and which methods are
invoking which methods.
This tool parses the bytecode of each `.class` file and looks at the "method instruction" calls to build a graph between classes and which methods are invoking which methods.

Since the tool is scanning the bytecode but not actually *executing it*, it has
no awareness of whether or not a method instruction will actually be executed
at runtime.
Since the tool is scanning the bytecode but not actually *executing it*, it has no awareness of whether or not a method instruction will actually be executed at runtime.

If bytecode exists for invoking a method in a class but that code path will
never actually be activated at runtime, this tool will still follow that
connection and report any conflicts it might find through that path.
If bytecode exists for invoking a method in a class but that code path will never actually be activated at runtime, this tool will still follow that connection and report any conflicts it might find through that path.

## Safe instances of class not found

Some libraries enable optional features when other classes are available on the
classpath, for example Netty tries to detect if cglib is available. These code patterns look something like
Some libraries enable optional features when other classes are available on the classpath, for example Netty tries to detect if cglib is available. These code patterns look something like

```java
boolean coolFeatureEnabled = false;
try {
Class.forName("com.sprockets.SomeOptionalFeature");
coolFeatureEnabled = true;
} catch (Throwable t) {
try{
Class.

forName("com.sprockets.SomeOptionalFeature");

coolFeatureEnabled =true;
}catch(
Throwable t){
// optional sprockets library not available
}
}

...
if (coolFeatureEnabled) {
...
if(coolFeatureEnabled){
// load something that calls SomeOptionalFeature class
}
}
```

Javadeps will detect these calls to the optional classes and flag them as
conflicts, even though not having the class available will not cause any
runtime errors. Configure the plugin to ignore these classes/dependencies.
Javadeps will detect these calls to the optional classes and flag them as conflicts, even though not having the class available will not cause any runtime errors. Configure the plugin to ignore these classes/dependencies.

# History

This started as a [Spotify](https://github.com/spotify) hackweek project
in June 2015 by
This started as a [Spotify](https://github.com/spotify) hackweek project in June 2015 by
[Matt Brown](https://github.com/mattnworb),
[Kristofer Karlsson](https://github.com/krka),
[Axel Liljencrantz](https://github.com/liljencrantz)
and [Petter Måhlén](https://github.com/pettermahlen).

It was inspired by some real problems that happened when there were incompatible
transitive dependencies for a rarely used code path that wasn't detected until runtime.
It was inspired by some real problems that happened when there were incompatible transitive dependencies for a rarely used code path that wasn't detected until runtime.

We thought that should be detectable in build time instead - so we built this to see if it was feasible.

# License

This software is released under the Apache License 2.0. More information in
the file LICENSE distributed with this project.
This software is released under the Apache License 2.0. More information in the file LICENSE distributed with this project.

# Ownership

The Weaver squad is currently owning this project internally.
We are currently in the evaluating process of the ownership of this and other OSS Java libraries.
The ownership takes into account **ONLY** security maintenance.
The Weaver squad is currently owning this project internally. We are currently in the evaluating process of the ownership of this and other OSS Java libraries. The ownership takes into account **ONLY** security maintenance.

This repo is also co-owned by other people:

Expand Down
Loading

0 comments on commit bdfb0de

Please sign in to comment.