Gradle has very powerful dependency management features. In this tutorial I will walk through creating a multi module Java project, and explain:
- How the api and implementation dependencies work
- How to create a custom dependency resolution strategy to:
- Hard fail if an unwanted dependency is found
- Fix a dependency version
- Globally exclude a dependency
I will use IntelliJ for this tutorial. Start by creating a new Gradle project. I’m using Groovy as the Gradle DSL language, and the Gradle wrapper:
The Gradle wrapper means that the project includes a small jar that will bootstrap the build process. You don’t need a version of Gradle installed, rather the build will download the correct version. In my project, IntelliJ has generated the wrapper using version 7.6 of Gradle. I want to use a more recent version, so you can open the file gradle/wrapper/gradle-wrapper.properties and change the distributionUrl to a later version. I’m using version 8.6.
api and implementation dependency configurations
Next create two sub projects. I’ll call these “data-access” and “service”. The idea is to simulate a multi module project, representing a layered application, where one module performs data access, using Hibernate, and the other module is the service layer, which has no direct knowledge of the Hibernate data access layer.
If you have added the sub-projects using IntelliJ, they should automatically be added to the settings.gradle file at the root of the project, but you can open this file and check.
In the data-access project, add the java-library plugin to the build.gradle file:
plugins {
id 'java-library'
}
Now in the dependencies section, let’s add Spring and Hibernate:
dependencies {
api 'org.springframework:spring-core:6.0.11'
api 'org.springframework:spring-context:6.0.11'
implementation 'org.hibernate.orm:hibernate-core:6.5.2.Final'
We’ve added Spring as an api dependency, and Hibernate as an implementation dependency, the difference being:
- api – also on the classpath of later modules
- implementation – will be packaged into the final application, but is not on the build classpath for later modules
I think this is a great feature of Gradle, so much more powerful than Maven. It allows you to prevent later modules from being polluted by dependencies added for earlier modules. Let’s test out the access to these dependencies. In your data-access module, you can add a class that uses both libraries:
import org.hibernate.SessionFactory;
import org.springframework.context.ApplicationContext;
public class CustomerDAO {
public void getCustomer(Long id) {
SessionFactory sessionFactory = null;
ApplicationContext applicationContext = null;
}
}
Now build your project and confirm it works. (In the right nav, from your Gradle tab, you should be able to see Tasks -> build -> build.)
Now let’s add a class in the service project, and confirm that we can see Spring, but not Hibernate. In the service project build.gradle, add a dependency on the data-access project:
dependencies {
implementation(project(':data-access'))
Now let’s try and add a class to the service project which uses both Spring and Hibernate:
import org.hibernate.SessionFactory;
import org.springframework.context.ApplicationContext;
public class CustomerService {
public void processCustomer(Long id) {
ApplicationContext applicationContext = null;
SessionFactory sessionFactory = null;
}
}
When you try and build this, you should get an error:
package org.hibernate does not exist
import org.hibernate.SessionFactory;
^
Success! This proves that even though the data-access module is using Hibernate, and the service module depends on this module, the service module cannot use Hibernate code itself. The dependency has not bled into the service module. This is a great way to avoid accidental usage of dependencies from other modules. Now that we have proved this, delete the references to Hibernate from the CustomerService class and confirm the project can build again.
Throwing an error if a dependency is found
For the second part of this tutorial, I want to explain how you can customise dependency resolution. Again, Gradle has far more powerful mechanisms for doing this than Maven does. Firstly, let’s start by understanding what dependencies the project uses. On the command line, you can run:
./gradlew dependencies
This will show a number of different configurations, but most are blank, with only a couple of test configurations having dependencies. What is going on? The answer is that this command has only shown you dependencies for the top level project – not any of the sub projects. To see the dependencies for the data-access project, type:
./gradlew :data-access:dependencies
You should now see a much longer list, with the Spring and Hibernate dependencies included. So for Spring, as well as spring-core and spring-context, the list will show all the transitive dependencies, such as spring-jcl. Suppose we didn’t want spring-jcl, how could we detect it was being used? The answer is to write a custom dependency resolution strategy. In the top level build.gradle file, add the following:
allprojects { Project project ->
configurations.all {
println "Configuration: ${name}"
resolutionStrategy.eachDependency { DependencyResolveDetails details ->
println "Group: ${details.requested.group} Artifact: ${details.requested.name}"
if (details.requested.group == 'org.springframework' && details.requested.name == 'spring-jcl') {
throw new RuntimeException("Don't want spring-jcl")
}
}
}
}
Now try and build your project again. You should get a runtime exception.
Fixing a dependency version
What if you don’t want to hard fail, but rather change the fix the version to one specified by you? We can do that by overriding the version in the custom resolution strategy, so the above code becomes:
allprojects { Project project ->
configurations.all {
println "Configuration: ${name}"
resolutionStrategy.eachDependency { DependencyResolveDetails details ->
println "Group: ${details.requested.group} Artifact: ${details.requested.name}"
if (details.requested.group == 'org.springframework' && details.requested.name == 'spring-jcl') {
details.useVersion '6.0.5'
details.because 'we need v6.0.5'
}
}
}
}
If you rerun the dependencies commmand for the data-access module, the output should show that the version of spring-jcl has been fixed:
+--- org.springframework:spring-core:6.0.11
| \--- org.springframework:spring-jcl:6.0.11 -> 6.0.5
Excluding a dependency
What if you simply want to exclude a dependency entirely? In this case, things are simpler. Just use the exclude command in your configurations.all closure:
allprojects { Project project ->
configurations.all {
println "Configuration: ${name}"
exclude group: 'org.springframework', module: 'spring-jcl'
}
}
You can then rerun the dependencies command and confirm spring-jcl no longer appears in the list.
For more info on Gradle dependencies, see:
https://docs.gradle.org/current/userguide/declaring_dependencies.html
https://docs.gradle.org/current/userguide/dependency_locking.html
https://docs.gradle.org/current/userguide/resolution_strategy_tuning.html
Some of my other posts on Gradle:
Dependencies and configurations in Gradle
Gradle incremental tasks and builds
Gradle Release Plugin
Code coverage with Gradle and Jacoco