A multi-module Java project with Gradle. Step by step

  • Tutorial
A lot of articles about Gradle have been written. And for my part, I would like to add such step-by-step instructions to the piggy bank, the reading of which, I hope, will allow those who are new to Gradle to “try out” and continue to study this tool on their own.

This article will not describe in detail such topics as gradle plugins, tasks, dependencies, automatic testing and other delights of this project builder. Firstly, each topic deserves a separate article or even a series of articles, and secondly, there are already articles on these topics on the hub, for example: Gradle: Tasks Are Code , Gradle: Better Way To Build . And on the official Gradle website there is a beautifully written Gradle User Guide. I will focus on the immediate solution of the task, and all related topics will be described in the framework of this task itself.
First we decide on the goal, what do we want to get at the exit? And the goal is indicated in the title of the article. We want to get a project with several modules, which is built using Gradle. And so, let's get started.


Step 1. Install gradle

Note: If you just want to “play” with gradle by downloading the files for the article, or you got someone else's sources with the magic file gradlew (gradlew.bat) in the root of the project, then installing gradle is not necessary.

You can install Gradle by downloading the latest version from the Gradle download page or using the package manager in your favorite OS (note: I installed it on Mac OS via brew and on Debian via apt-get from standard sources)

The result of the first step:
$ gradle -version
------------------------------------------------------------
Gradle 1.11
------------------------------------------------------------
Build time:   2014-02-11 11:34:39 UTC
Build number: none
Revision:     a831fa866d46cbee94e61a09af15f9dd95987421
Groovy:       1.8.6
Ant:          Apache Ant(TM) version 1.9.2 compiled on July 8 2013
Ivy:          2.2.0
JVM:          1.8.0_05 (Oracle Corporation 25.5-b02)
OS:           Mac OS X 10.9.3 x86_64


Step 2. Empty project, plugins, wrapper

Create a project folder and in its root create a file build.gradlewith the following contents:

{project_path} /build.gralde
apply plugin: “java”
apply plugin: “application”
task wrapper(type: Wrapper) {
    gradleVersion = '1.12'
}

Let's take a closer look at what we wrote in the file. It uses the dynamic language Groovy. Using a full-fledged programming language in gradle gives you more freedom compared to package builders using declarative languages.
In this file we connect plugins javaand application. The plugin javacontains such useful tasks as jar - to compile a jar archive, compileJava - to compile source codes, etc. You can read more about the plugin here . Pluginapplicationcontains tasks: run - launch the application; installApp - install the application on the computer, this task creates executable files for * nix and for windows (bat file); distZip - collects the application in a zip archive, placing all jar files there, as well as operating system-specific scripts. Read more about the plugin in the documentation .
Now we dwell in more detail on the problem wrapper. This very useful task is probably the most ingenious solution designed to make life easier for programmers. Having executed $ gradle wrapper, we obtain the following result:

$ gradle wrapper
:wrapper
BUILD SUCCESSFUL
Total time: 7.991 secs

$ ls -a
.          ..          .gradle          build.gradle     gradle          gradlew          gradlew.bat

We see that the script created gradlew executables for * nix, gradlew.bat for Windows, as well as the gradle and .gradle folders. The hidden .gradle folder can not be included in the repository, it contains dependency libraries. All basic lies in gradle and in the gradlew file itself. Now we can safely give our project to any person who has jdk of the desired version, and he will be able to independently compile, assemble, install the project using ./gradlew. Note that my version of gradle (see the result of the command $ gradle -versionabove) is different from the one I specified in the build.gradle file, but this is not scary, because after running the wrapper task, we will get the necessary version of gradle.

$ ./gradlew -version
------------------------------------------------------------
Gradle 1.12
------------------------------------------------------------
Build time:   2014-04-29 09:24:31 UTC
Build number: none
Revision:     a831fa866d46cbee94e61a09af15f9dd95987421
Groovy:       1.8.6
Ant:          Apache Ant(TM) version 1.9.3 compiled on December 23 2013
Ivy:          2.2.0
JVM:          1.8.0_05 (Oracle Corporation 25.5-b02)
OS:           Mac OS X 10.9.3 x86_64

Now gradleyou can safely use instead gradlew. By the way, executing a command $ ./gradlewwithout parameters will create a folder .graldeand download all the dependent libraries there (about the dependencies below). But the execution of this command is not necessary, since at any start of gradle (gradlew), dependencies will be checked and missing files will be downloaded. Therefore, having received the project in which the gradlew files are located, you can immediately start the desired task, a list of which can be obtained by the ./gradlew tasks

Results of the second step command (output is shortened):
$ ./gradlew tasks
:tasks
------------------------------------------------------------
All tasks runnable from root project
------------------------------------------------------------
Application tasks
-----------------
distTar - Bundles the project as a JVM application with libs and OS specific scripts.
distZip - Bundles the project as a JVM application with libs and OS specific scripts.
installApp - Installs the project as a JVM application along with libs and OS specific scripts.
run - Runs this project as a JVM application
...
Other tasks
-----------
wrapper
...
To see all tasks and more detail, run with --all.
BUILD SUCCESSFUL
Total time: 7.808 secs


Step 3. Fill in the blanks

At this stage, we can already perform several tasks gradle. We can even compile a jar file, but nothing but an empty manifest will be there. It is time to write the code. Gradle uses the same directory structure as Maven by default , namely
src
    -main
        -java
        -resources
    -test
        -java
        -resources

main/java- these are java-files of our program, main/resources- these are other files (* .properties, * .xml, * .img and others). There testare files necessary for testing.
Since testing will not be considered in this article, we will get by creating a folder src/mainwith all the subfolders and start creating our application. And the application is Hello World, in which we will use the Log4j library . We’ll just figure out how dependencies work in gradle. We build.gradlewill make changes to the file , create a file com/example/Main.javawith the main application class in the folder src/main/java, as well as a file with Log4j settings src/main/resources/log4j.xml. And the file gradle.properties(optional, details below)

{project_path} /build.gradle
apply plugin: "java"
apply plugin: "application"
mainClassName = "com.example.Main"
sourceCompatibility = JavaVersion.VERSION_1_7
targetCompatibility = JavaVersion.VERSION_1_7
repositories {
	mavenCentral()
}
dependencies {
	compile "log4j:log4j:1.2.17"
}
jar {
	manifest.attributes("Main-Class": mainClassName);
}
task wrapper(type: Wrapper) {
	gradleVersion = "1.12"
}

{project_path} /gradle.properties
org.gradle.java.home=/Library/Java/JavaVirtualMachines/jdk1.7.0_55.jdk/Contents/Home/

{project_path} /src/main/java/com/example/Main.java
package com.example;
import org.apache.log4j.Logger;
public class Main {
	private static final Logger LOG = Logger.getLogger(Main.class);
	public static void main(String[] args) {
		LOG.info("Application started");
		System.out.println("I'm the main project");
		LOG.info("Application finished");
	}
}

{project_path} /src/main/resources/log4j.xml

Consider the changes in the file build.gradle. We have added a variable mainClassName. It indicates the main class of our application and is used by the plugin applicationin the task run. It is this class that will be launched. We also added variables sourceCompatibilityand targetCompatibility, assigning them a value JavaVersion.VERSION_1_7. These are the variables from the plugin java; they show which version of jdk we need when building. The next block is repositories. In this block we connect the Maven repository . Gradle is very “friendly” with him. The block dependenciescontains the dependencies of our application. We look at the details of the settings in the documentation . Here we indicate that for the taskcompilelog4j required. The example has a shorthand syntax. You can write a detailed version and it will look like this:
complie group: 'log4j', name: 'log4j', version: '1.2.17'

For comparison, a similar block in maven:
log4jlog4j1.2.17

You can also configure file compile files('libs/a.jar', 'libs/b.jar')and subproject dependencies compile project(':library_project').
The last addition to build.gradleis a block jar. It also applies to the plugin java. It contains additional information for building a jar file. In this case, we add the main class to the manifest using the variable declared above mainClassName.
Next is an optional file gradle.properties. The description of this file is scattered throughout the documentation, a little is here and here . In this case, we are actually redefining the variable JAVA_HOME. This is true when you have several versions of jdk, as in my case, you could pay attention at the beginning of the article, it $ gradle -versionshows that my version JVM: 1.8.0_05 (Oracle Corporation 25.5-b02).
I think that dwell on files in detail src/main/java/Main.javaand src/main/resources/log4j.xmldoes not make sense, since everything is extremely simple. We send two messages to Logger, the message “I'm the main project” is displayed in the console. The log4j settings file says that our logger will also output messages to the console.

The results of the third step:
$ ./gradlew run
:compileJava
Download http://repo1.maven.org/maven2/log4j/log4j/1.2.17/log4j-1.2.17.jar
:processResources
:classes
:run
INFO com.example.Main: Application started
I'm the main project
INFO com.example.Main: Application finished
BUILD SUCCESSFUL
Total time: 14.627 secs

It can be seen that the missing library is being downloaded, and its use has been demonstrated.

Step 4. Reaching the goal

We already have a project that works, builds and runs through gradle. It remains to complete the smallest detail: implement the multi- modularity stated in the title of the article, or multi-project , using gradle terminology. Create two directories in the root of the project: main_projectand library_project. Now move the folder srcand file build.gradleto the directory you just created main_project, and create a new file settings.gradlewith the following contents in the root (more on this file here ):

{project_path} /settings.gradle
rootProject.name = 'Gradle_Multiproject'
include 'main_project'

In this file we say what our project is called and what folders to connect (actually independent gradle projects). At this stage, we need one folder main_project. After such changes, we can perform $ ./gradlew runeither with an indication of a specific subproject $ ./gradlew main_project:run, and we will get the same result as at the end of step 3. That is, a working project. We can also execute all other jar, build, installApp commands and so on. Gradle, if you do not specify a specific subproject, will start the task in all connected subprojects that have this task (for example, if the application plugin is connected to only one subproject, we will have main_project, the command $ ./gradlew runwill run only that subproject)
Now we will create the code in ours library_project. We create build.gradleandsrc/main/java/com/example/library/Simple.java

{project_path} /library_project/build.gradle
apply plugin: "java"
sourceCompatibility = JavaVersion.VERSION_1_7
targetCompatibility = JavaVersion.VERSION_1_7


{project_path} /library_project/src/main/java/com/example/library/Simple.java
package com.example.library;
public class Simple {
	private int value;
	public int getValue() {
		return value;
	}
	public void setValue(int value) {
		this.value = value;
	}
} 


build.gradlefor this subproject is much simpler. We use the java plugin and set the variables with the JDK version. This is sufficient for this article. Now we want gradle to know about the subproject library_project, we will describe this in the file settings.gradle:

{project_path} /settings.gradle
rootProject.name = 'Gradle_Multiproject'
include 'main_project', 'library_project'

Now we can compile the jar file containing our library with the command $ ./gradlew library_project:jar.
$ ./gradlew library_project:jar
:library_project:compileJava
:library_project:processResources UP-TO-DATE
:library_project:classes
:library_project:jar
BUILD SUCCESSFUL
Total time: 10.061 secs

The resulting file can be found at: {project_path}/library_project/build/libs/library_project.jar.
Now let's add class usage Simpleto main_project. To do this, {project_path}/main_project/build.gradleadd a line compile project(":library_project")to the block in the file dependencies, which says that to perform the compile task in this module, you need a project library_project.

Addition from MiniM : In gradle, the symbol ":" is used instead of "/" and for a more branched structure, project links may look like this:: loaders: xml-loader "

{project_path} /main_project/build.gradle (block dependencies)
dependencies {
	compile "log4j:log4j:1.2.17"
	compile project(":library_project")
}


{project_path} /main_project/src/main/java/com/example/Main.java
package com.example;
import org.apache.log4j.Logger;
import com.example.library.Simple;
public class Main {
	private static final Logger LOG = Logger.getLogger(Main.class);
	public static void main(String[] args) {
		LOG.info("Application started");
		System.out.println("I'm the main project");
		Simple simple = new Simple();
		simple.setValue(10);
		System.out.println("Value from Simple: " + simple.getValue());
		LOG.info("Application finished");
	}
}

You can check.

The result of the fourth step:
$ ./gradlew run
:library_project:compileJava UP-TO-DATE
:library_project:processResources UP-TO-DATE
:library_project:classes UP-TO-DATE
:library_project:jar UP-TO-DATE
:main_project:compileJava
:main_project:processResources UP-TO-DATE
:main_project:classes
:main_project:run
INFO com.example.Main: Application started
I'm the main project
Value from Simple: 10
INFO com.example.Main: Application finished
BUILD SUCCESSFUL
Total time: 11.022 secs


Step 5 (final). We remove garbage

The main goal was achieved, but at this stage quite logical questions could arise about duplication of information in build files, deeper gradle settings, and also what to study further. For independent study, I advise you to read the contents of the links at the end of the article. In the meantime, let's tidy up our build files by creating build.gradlein the root of the project and changing the contents of the rest of the build files

{project_path} /build.gradle
apply plugin: "idea"
apply plugin: "eclipse"
subprojects {
    apply plugin: "java"
    tasks.withType(JavaCompile) {
        sourceCompatibility = JavaVersion.VERSION_1_7
        targetCompatibility = JavaVersion.VERSION_1_7
    }
    repositories {
        mavenCentral()
    }
}
task wrapper(type: Wrapper) {
    gradleVersion = "1.12"
}

{project_path} /main_project/build.gradle
apply plugin: "application"
version = '1.0'
mainClassName = "com.example.Main"
dependencies {
	compile "log4j:log4j:1.2.17"
	compile project(":library_project")
}
jar {
	manifest.attributes("Main-Class": mainClassName);
}

{project_path} /build.gradle
version = "1.1_beta"

In the root, build.gradlewe will store what applies to all projects (in fact, all settings can be stored in general, but I prefer to separate large files) and what is not needed in subprojects, for example, we need only one wrapper, in the root.
In the block subprojectswe put the settings of subprojects, namely: we connect the java plugin - everyone needs it; set the jdk version; we connect the maven repository. Also in this file we include idea and eclipse plugins . These plugins contain tasks for generating project files for the corresponding IDEs. And here we transfer the wrapper task. It is needed only at the root to create gradlew files common to all.
In subprojects we removed all unnecessary and added a variableversion. The value of this variable will be added to jar files, for example, instead of library_project.jar now there will be library_project-1.1.beta.jar.
In addition to the block subprojects, you can use allprojectsor project(':project_name'). More details here .

On this I will end. I hope this article aroused interest among people not familiar with Gradle, and prompted a more detailed study and the subsequent use of this tool in their projects.

Thanks for attention.

Additions:
MiniM : In gradle, the symbol ":" is used instead of "/" and for a more branched structure, project links may look like this:: loaders: xml-loader "
leventov : with a plugin idea есть проблемы. issues.gradle.org/browse/GRADLE-1041, in the last comment there is a solution.

useful links

Sources of the project created in the article on bitbucket ( zip archive )
Gradle
Gradle User Guide
Apache Logging Services
Apache Maven
Groovy Language

Also popular now: