Gradle Tutorial : Part 3 : Multiple Java Projects

Romin Irani
Romin Irani’s Blog
10 min readJul 29, 2014

Welcome to Part 3 of the Gradle Tutorial. This part builds on Part 2, where we looked at using the Java plugin in Gradle to compile/build our Java project.

In this part of the tutorial, we shall look at a common scenario where you will have multiple Java projects that could be dependent on each other. For e.g. You could have a library project where you write some utility classes and another Java project that depends on it.

This part assumes that you have a Gradle installation on your machine and the basic environment is setup. For more information on that, refer to Part 1. Additionally, you know the basics of using the Java plugin in Gradle, which we covered in Part 2.

Just to remind readers, the focus will be on understanding Gradle and how it goes about doing stuff when it comes to Java projects. So we will not be worried about the Java code that goes into those projects.

LAST UPDATE : December 27, 2017:
i) Verified to work with Gradle 4.4.1.

Multiple Project Scenario

For this episode, we shall look at 3 projects that are arranged under a common directory. This is just for demonstration but it will help us understand the concepts well.

Our directory will be called javaprojects and inside of that we have 3 other folders that will house the individual projects as shown below:

javaprojects
|- api
|- common
|- app

Now, let us talk about the dependencies. These dependencies are something like this:

  1. common: This project contains some utility code and hence it will not depend on any of the other projects. Do note that this does not mean that common does not have dependencies on external libraries for compilation. So that concept that we saw in the earlier chapter (Part 2) still remains. It may depend on 3rd party JARs.
  2. api : This project contains some API code and it depends on common project.
  3. app : This project contains the application code and it depends on api and common projects respectively.

On my machine, I have created a folder named e:\javaprojects and inside of that I have 3 empty folders api, common and app. I have intentionally left them empty for now, so that you can see how it will all come together.

You can select the appropriate drive and/or root folder, but for the purpose of this episode, the root folder javaprojects is my container for the other 3 projects.

While they are empty, you can easily visualize that in reality you will have Java classes inside of each of these folders. And since we are going to eventually use the Java Gradle plugin to compile the code, the Java classes inside of these folders will follow convention i.e. they will be present under src/main/java folder as we saw in Part 2.

Including Multiple Projects in Build

The first thing you should think of is a central place to control the overall build for all the 3 projects. Gradle makes this easy for you by asking you to create a settings.gradle file in the root folder.

So, in the javaprojects folder, create a file named settings.gradle and in the file, all we will need to do is mention the projects (i.e. the folders) that comprise our 3 Java Projects i.e. api, common and app. The settings.gradle file is shown below:

include ":api", ":common", ":app"

Common Configuration

You are familiar with the build.gradle file, which is what the Gradle command looks for in terms of what it has to do.

Create a build.gradle file in e:\javaprojects directory. This will be our file that will specify what Gradle needs to do.

Remember we do not have any Java files so far in any of the 3 directories : app, common and api.

Let us look at one of the recommended and fairly intuitive structures for the build.gradle file as shown below:

allprojects {
//Put instructions for all projects
}
subprojects {
//Put instructions for each sub project
}

To understand what is happening, update your build.gradle file in the root folder i.e. javaprojects to contain the following:

allprojects {
task hello << { task -> println "I'm $task.project.name" }
}
subprojects {

}

What have we done here ? We have added a task (written in Groovy) to simply print out the project name. And we have put that inside the allprojects closure. This means that it will apply to all the projects.

Save the build.gradle file and go to the root folder at the command prompt/terminal where the build.gradle file is, and fire the following command:

gradle -q hello

This will produce the output as given below:

I'm javaprojects
I'm api
I'm app
I'm common

You can also fire the task individually for any specific project, as given below:

gradle -q app:hello

This will produce only app specific output as given below:

I'm app

This should make things clear that you can apply commands to all projects and then you have full control on how to run it i.e. run it in such a way that it applies to each project or any specific project.

Apply Java plugin to Sub Projects

Now, we know that the 3 projects : api, common and app are Java projects. Hence we can do the following for the 3 Projects:

  • Apply the Java plugin
  • Setup the MavenCentral repository, assuming that these projects will have 3rd party dependencies , which we would like to pick up from Maven Central.

The lines to be added to your build.gradle are shown in bold.

The build.gradle file in the root folder will now look like the following:

allprojects {
task hello << { task -> println "I'm $task.project.name" }
}
subprojects {
apply plugin: "java"
repositories {
mavenCentral()
}

}

We know that the Java plugin adds several tasks like clean, assemble, build, etc, which make it easy to work with the build process if you follow conventions. We saw that in Part 2, if you wish to refer to that.

By putting those commands inside of subprojects, we have essentially injected common characteristics into each of the sub projects i.e. common , api and app.

Go back to the command prompt or terminal and fire the following command:

gradle build

You will find in the output (which I am not listing here) that the build task got fired on each of the projects and all tasks that it was dependent on is also executed. Cool, isn't it ?

Remember, if you just wish to build the common project, you can always invoke the following (gradle <project-name>:<task-name>):

gradle api:build

We do not have any Java files in any of the folders so far, but that is fine for now.

Project Specific Configuration

What if you wanted to apply some tasks , plugins, dependencies differently to each project or one or more projects and not all of them.

For the 3 projects that we have, the build.gradle file would need to look something like this:

allprojects {
task hello << { task -> println "I'm $task.project.name" }
}
subprojects {
apply plugin: "java"
repositories {
mavenCentral()
}
}
//API Project specific stuff
project(':api') {
}
//Common Project specific stuff
project(':common') {
}
//App Project specific stuff
project(':app') {
}

As an example, let us say that we would like to apply the java plugin only to common project and not to any of the other projects. An example of such a build.gradle is shown below:

allprojects {
task hello << { task -> println "I'm $task.project.name" }
}
subprojects {
//Some other stuff (Empty for now)
}
//API Project specific stuff
project(':api') {
}
//Common Project specific stuff
project(':common') {
apply plugin: "java"

repositories {
mavenCentral()
}
}
//App Project specific stuff
project(':app') {
}

Save the above build.gradle file.

Now fire the following command :

gradle app:build

This should give us the error that there is no build task for project app. This is because build task is only available in the project for which the Java plugin has been applied.

FAILURE: Build failed with an exception.* What went wrong:
Task 'build' not found in project ':app'.

Similarly, if you fired the following command:

gradle common:build

You will find that it executes the build task successfully since it is available for that project as per our build.gradle file.

:common:compileJava UP-TO-DATE
:common:processResources UP-TO-DATE
:common:classes UP-TO-DATE
:common:jar UP-TO-DATE
:common:assemble UP-TO-DATE
:common:compileTestJava UP-TO-DATE
:common:processTestResources UP-TO-DATE
:common:testClasses UP-TO-DATE
:common:test UP-TO-DATE
:common:check UP-TO-DATE
:common:build UP-TO-DATE
BUILD SUCCESSFULTotal time: 3.681 secs

Project Compilation Dependencies

Recollect that we had defined our project dependencies as follows:

  1. api depends on common
  2. app depends on api and common

In addition, we will also need to consider that each of the projects might have dependencies on 3rd party JAR files.

So , let us say that the requirements to compile the projects are as follows:

  • common requires Apache Commons Lang 3.4
  • api requires Apache Commons Lang 3.4 and Apache Log4j 2.6.2
  • app requires Apache Log4j 2.6.2
  • All of the projects have JUnit Test cases and require that the JUnit Jar file is linked. Any JUnit JAR file above 4.x will do.

Given the above requirements (I am not going to show you the source Java files or JUnit Test cases, since that is not necessary here), our build.gradle file that is present in the root folder will now look like this:

subprojects {
apply plugin: "java"
repositories {
mavenCentral()
}
dependencies {
testCompile "junit:junit:4+"
}
}
//Common Project specific stuff
project(':common') {
dependencies {
compile 'org.apache.commons:commons-lang3:3.4'
}
}
//API Project specific stuffproject(':api') {
dependencies {
compile project(':common')
compile 'org.apache.commons:commons-lang3:3.4'
compile 'org.apache.logging.log4j:log4j-core:2.6.2'
}
}
//App Project specific stuffproject(':app') {
dependencies {
compile project(':common'), project(':api')
compile 'org.apache.logging.log4j:log4j-core:2.6.2'
}
}

Some notes on the above build.gradle file:

  • In the subprojects closure, we have added the stuff that is common to each project i.e. applied the Java plugin, added the maven repository and added the common dependency on JUnit JAR for the testCompile configuration.
  • Then for each of the sub projects, we have clearly specified the dependencies , not just on the project but also on any individual JARs.
  • Please note that we have considered only the compile configuration in the build file here. You can always target other configurations provided by Java plugin like testCompile, run, testRun and so on. So you can enhance the file depending on your requirement.
  • The compilation dependency on another project is specified via the
    compile project(<projectname>) statement
  • The compilation dependency on a JAR file is specified via the same mechanism that we saw in Part 2 i.e. “group:name:version”

As an exercise, I suggest adding a few of your own Java files, setting up the correct dependencies and then firing the gradle build command.

One or multiple build.gradle files?

The question that you should be asking now is whether it is the right approach to create a single large build.gradle file in the root folder that will:

  • Apply common code within the subprojects closure
  • Contain individual project(<projectname>) closures that will define tasks specific to that project, its own dependencies, etc.

We have an example of one such build.gradle file in the previous section. A good practice is actually not to have one single file but to break them into multiple build.gradle files and each of these specific build.gradle files will be present in the respective root folders of the 3 projects i.e. app, common and api.

In essence, what we are ending up with is a structure that looks like the following:

/javaprojects
|- /api
|- build.gradle
|- (Java Sources and files)
|- /common
|- build.gradle
|- (Java Sources and files)
|- /app
|- build.gradle
|- (Java Sources and files)
|- settings.gradle
|- build.gradle

Notice how at the root, we will continue to have the settings.gradle that includes all the projects as we saw.

There is a build.gradle at the root also. This will contain stuff that is common to each of the sub projects. For e.g. since each of our sub projects are Java projects and we plan to use the Maven Central repository, the contents of our build.gradle could be as short as the following:

subprojects {apply plugin: "java"

repositories {
mavenCentral()
}
dependencies {
testCompile "junit:junit:4+"
}
}

Now, our individual build.gradle for the specific projects will be roughly as shown below:

build.gradle for app project

dependencies {
compile project(':common'), project(':api')
compile 'org.apache.logging.log4j:log4j-core:2.6.2'
}

build.gradle for api project

dependencies {
compile project(':common')
compile 'org.apache.commons:commons-lang3:3.4'
compile 'org.apache.logging.log4j:log4j-core:2.6.2'
}

build.gradle for common project

dependencies {
compile 'org.apache.commons:commons-lang3:3.4'
}

In this way, you can manage the Gradle build files separately for each project. It will be easier in the long run to do things in this fashion, so that any new project can be added and its specific dependencies / tasks can be handled in its own build.gradle file.

Keep in mind that each of these individual build.gradle files can be enhanced to as much extent as you want depending on your requirements.

Download Project

You can download the entire project over here.

Recommended Reading

I suggest that you look at the following documentation in Gradle that contains a lot more examples and will help in solidifying your understanding : Multi-Project Builds.

As an exercise, if you use Android Studio and have attempted to generated both an Android project and an App Engine project, you will now be able to understand what has been generated when it comes to Gradle. You will find a settings.gradle there with all your modules in the Android Studio Project. It will contain a base build.gradle file that will contain common behaviour and then individual build.gradle files for each of the plugins. You should feel more confident of things now. We will get to those in a while, so if you do not want to revisit your Android Studio for now, that is fine too.

Moving forward

We have moved quite a bit now in our Gradle journey. By now, you should start feeling comfortable with the whole thought process of building your projects and how Gradle fits into the project.

But we still have a long way to go and the next logical step to take for us is to look at Gradle War (Web Application Archive) plugin that helps with Java Web projects. That is covered in the next part. Till then, Happy Gradling!

References

--

--

Romin Irani
Romin Irani’s Blog

My passion is to help developers succeed. ¯\_(ツ)_/¯