Gradle Tutorial : Part 3 : Multiple Java Projects

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 : January 22, 2015 :
i) Minor text changes.
ii) Final Project available for download now

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 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

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, e:\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 e:\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. 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 and 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 SUCCESSFUL

Total 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:

  1. common requires Apache Commons Lang 3.3.2
  2. api requires Apache Commons Lang 3.3.2 and Apache Log4j 1.2.7
  3. app requires Apache Log4j 1.2.7
  4. 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.3.2'
  }
} 

//API Project specific stuff 

project(':api') { 
   dependencies {
      compile project(':common')
      compile 'org.apache.commons:commons-lang3:3.3.2'
      compile 'log4j:log4j:1.2.17'
   }
} 


//App Project specific stuff 

project(':app') { 
   dependencies {
      compile project(':common'), project(':api')
      compile 'log4j:log4j:1.2.17'
   }
}

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 'log4j:log4j:1.2.17'
}

build.gradle for api project

dependencies {
      compile project(':common')
      compile 'org.apache.commons:commons-lang3:3.3.2'
      compile 'log4j:log4j:1.2.17'
   }

build.gradle for common project

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

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

Advertisements

22 thoughts on “Gradle Tutorial : Part 3 : Multiple Java Projects

  1. Thanks a million,trillion and billion. It is the most comprehensive and well written article on Gradle Build. I am only at part 3, but I can’t stop thanking you. Reading your article, I got the most needed information about Gradle and all other stuff and I think, your article should be on official documentation page. Keep up the good work

  2. javaprojects $ gradle build
    :common:compileJava UP-TO-DATE
    :common:processResources UP-TO-DATE
    :common:classes UP-TO-DATE
    :common:jar UP-TO-DATE
    :api:compileJava UP-TO-DATE
    :api:processResources UP-TO-DATE
    :api:classes UP-TO-DATE
    :api:jar UP-TO-DATE
    :api:assemble UP-TO-DATE
    :api:compileTestJava UP-TO-DATE
    :api:processTestResources UP-TO-DATE
    :api:testClasses UP-TO-DATE
    :api:test UP-TO-DATE
    :api:check UP-TO-DATE
    :api:build UP-TO-DATE
    :app:compileJava UP-TO-DATE
    :app:processResources UP-TO-DATE
    :app:classes UP-TO-DATE
    :app:jar UP-TO-DATE
    :app:assemble UP-TO-DATE
    :app:compileTestJava UP-TO-DATE
    :app:processTestResources UP-TO-DATE
    :app:testClasses UP-TO-DATE
    :app:test UP-TO-DATE
    :app:check UP-TO-DATE
    :app:build 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

    Why taks run for “common” project fell into 2 parts? it was run partly before api tasks and partly after app tasks.

    1. Interesting. There has to be some sort of dependencies between the tasks but again I don’t think that is a good answer. Maybe someone else can add some points to that.

      1. Hi Romin, thanks for the response. Oh, my bad, I haven’t thanked you for such a wonderful tutorial !

        Just wondering does it happen to you too, or only myself? here is my build.gradle files:

        1. under javaprojects:

        javaprojects fujyang$ more build.gradle

        allprojects {
        task hello < println “I’m $task.project.name”
        }
        }

        subprojects {
        apply plugin: “java”
        repositories {
        mavenCentral()
        }

        dependencies {
        testCompile “junit:junit:4+”
        }
        }

        2. under common:

        javaprojects fujyang$ more common/build.gradle
        dependencies {
        compile ‘org.apache.commons:commons-lang3:3.3.2’
        }

        3. under api:

        javaprojects fujyang$ more api/build.gradle
        dependencies {
        compile project(‘:common’)
        compile ‘org.apache.commons:commons-lang3:3.3.2’
        compile ‘log4j:log4j:1.2.17’
        }

        4. under app:

        javaprojects fujyang$ more app/build.gradle
        dependencies {
        compile project(‘:common’), project(‘:api’)
        compile ‘log4j:log4j:1.2.17’
        }

      2. Here is why I think Fujian got that output. The build task for the java plugin includes the following tasks that it runs:
        compileJava
        processResources
        classes
        jar
        assemble
        compileTestJava
        processTestResources
        testClasses
        test
        check
        build

        Since the the app and api only have compile dependency on common, Gradle goes only as far as the jar step and then uses that jar as the dependency for app and api. As a proof for my theory, I ran “gradle jar” and got this output:
        $ gradle jar
        :common:compileJava UP-TO-DATE
        :common:processResources UP-TO-DATE
        :common:classes UP-TO-DATE
        :common:jar UP-TO-DATE
        :api:compileJava UP-TO-DATE
        :api:processResources UP-TO-DATE
        :api:classes UP-TO-DATE
        :api:jar UP-TO-DATE
        :app:compileJava UP-TO-DATE
        :app:processResources UP-TO-DATE
        :app:classes UP-TO-DATE
        :app:jar UP-TO-DATE

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s