Use the minimum number of dependencies that is reasonable. Every dependency of a library is a liability of both that library and its consumers.
Adding a dependency for something that is difficult and complicated may be OK, but avoid adding a dependency just to save a few lines of code. Remember you’re also paying the cost of transitive dependencies. Before adding a dependency, consider what that dependency depends on.
Scrutinize all dependency additions. Whenever you add a new dependency, check the transitive dependencies it adds to your classpath. If many new transitive dependencies are pulled in, consider whether the functionality is worth the cost. If a different library solves the same problem but adds fewer dependencies, it might be preferable. Alternatively, if the functionality you need is small, reimplement it in your own library.
mvn dependency:tree
./gradlew dependencies
Prefer JDK classes where available. For example, XOM and JDOM
are very convenient and far easier to use than DOM. However, most
uses of these libraries can be satisfied with the org.w3c.dom
or other packages bundled with the JDK at some cost in development
time.
For any given functionality, pick exactly one library. For example, GSON, Jackson, and javax.json all parse JSON files. If one is already pulled in by another dependency, use that. Otherwise choose one and standardize on it. Do not include more than one in your dependency tree. Do not allow different team members to choose different libraries.
If you can reasonably reimplement functionality instead of adding
another dependency, do so. For example, if the only classes you’re
using from Guava are Preconditions
and Strings
, it’s not
worth adding a dependency on Guava. You can easily reimplement
any method in those classes.
When you do add a dependency, keep it scoped as narrowly as possible.
For example, libraries used only for testing such as JUnit, Mockito,
and Truth should have test
scope in a Maven build:
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13</version>
<scope>test</scope>
</dependency>
When building with Gradle, these libraries should have testCompile
scope:
dependencies {
testCompile 'junit:junit:4.13'
}
Libraries needed at compile time but not at runtime should be marked optional
in Maven. For example, dependents of a library that uses AutoValue
do not usually need to transitively depend on
com.google.auto.value:auto-value-annotations
.
<dependency>
<groupId>com.google.auto.value</groupId>
<artifactId>auto-value-annotations</artifactId>
<version>1.6.6</version>
<optional>true</optional> <!-- not needed at runtime -->
</dependency>
In Gradle these libraries should have compileOnly
scope:
dependencies {
compileOnly 'com.google.auto.value:auto-value-annotations:1.6.6'
}
The provided
scope should be used when the dependency is needed at runtime
and compile time. However, the specific JAR file will be supplied
by the environment in which the code runs. For example, Java servlet containers such as Tomcat,
Jetty, and App Engine supply javax.servlet:javax.servlet-api
.
The tools we use for Java projects are more often than not themselves written in Java. This includes build systems such as Maven, javac, and Ant; runtime environments such as Tomcat, Hadoop, Beam, and App Engine; and editors such as jEdit, Eclipse, and IntelliJ IDEA. Tools like these should have their own classpaths from which they load their own code.
It is critical not to mix the classpath of the tool with the classpath of the
product. Dependencies of the tool should not be dependencies of the product.
For example, there’s no reason javax.tools.JavaCompiler
should appear in the
classpath of an MP3 player simply because the player’s source code
was compiled by javac
.
Indeed javac
does not transmit its own dependencies into the products
it builds, but not all tools are this well behaved. Maven annotation processors
such as AutoValue and Animal Sniffer are sometimes declared to be dependencies
of the product itself in the pom.xml. Since they are only needed at
compile time, they should instead be added to the annotation processor
path.
When running code from Maven, prefer mvn exec:exec
to mvn exec:java
.
mvn exec:exec
uses a completely separate process to run the user’s
code while mvn exec:java
runs the user’s process in the same virtual machine
Maven itself uses.
Modern application servers are reasonably good about separating the code they host from their own. However there have been issues in the past where the boundary was not as aggressively maintained. The jre/lib/ext directory in particular has been a common source of hard to debug dependency issues where a different version of a library was loaded instead of the one the user expected. If you are creating a product that runs user supplied code, implement completely separate classpaths for user code and your product’s code.