Build Tools
Ant, Maven and Gradle are the most popular build tools in Java platform. Almost every java projects now-a-days built on either Maven nor Gradle. Although Ant solves different problems, serves different purpose, many projects still leverage the benefits of Ant. In this article Maven and Gradle’s structure described side by side. Developer will find useful code snippets and resources for getting start.
Brief History
- Ant
-
First released 19 July 2000 [1]. Built on top of
Java
andXML
. Developer needs to explicitly code everything. - Maven
-
Maven prefers convention over configuration in their builds. It reduces explicit coding for the developers. Written in
Java
and working with POM (Project Object Model) basedXML
. It also supportsC#
,Ruby
,Scala
, and other languages. First released 13 July 2004 [2]. - Gradle
-
Gradle first released 2007. Written in Java, Kotlin, Groovy [3]. Gradle also prefers the concept of convention over configuration.
This article has no intention to compare between Maven and Gradle, neither it has intention to prove one is best over other. Both tools have their advantages and special use cases. Primary goal of this article is syntax comparison so that developer can start learning side by side or learn one from another. |
Convention
Maven and Gradle both prefers the concept Convention over Configuration
rather than defining explicit configuration every time like Ant.
By default, both tools implicitly applies some default configuration in your build script to reduce the repetitive configuration.
Convention over Configuration means, the environment in which you are working (e.g. language, framework) assumes many logical situations by default. So if you adapt those conventions (e.g. default source directory structure), it behaves as expected without having to configure anything (e.g. configure source directory location). By adopting conventions rather than explicitly creating configuration every time, programming become more productive task and reduces the repetitive configuration. If your business requirement needs to ignore default conventions, or the default behavior doesn’t match with your expectation, you can still change by explicitly defining you configuration. This concept is related to ideas like the concept of Sensible Defaults. |
Structure
During project generation Maven and Gradle generates their default directory structure. It doesn’t matter you have created from scratch, or generated the java project both tools expects and uses their default directory structure for the build process. If the default directory convention doesn’t match your needs you can still explicitly configure.
Gradle directory convention implicitly configured by plugins. You can change this default directory by SourceSet block. |
Gradle default directory structure
-
settings.gradle
file is optional and only required in root project for multi module projects. -
build
directory is generated output directory.
Override default configuration
sourceSets { (1)
main.java.srcDirs = ['src/main/java']
main.resources.srcDirs = ['src/main/resources']
test.java.srcDirs = ['src/test/java']
test.resources.srcDirs = ['src/resources']
}
1 | Explicitly directory structure configuration by sourceSets block. |
Sample build.gradle file generated by IntelliJ IDEA
plugins {
id 'java'
}
group 'org.example'
version '1.0-SNAPSHOT'
repositories {
mavenCentral()
}
dependencies {
testCompile group: 'junit', name: 'junit', version: '4.12'
}
Maven directory convention implicitly inherit from Super POM. You can change this default directory inside <build> element. |
Maven default directory structure
-
target
directory is generated output directory.
Override default configuration
<project>
[...]
<build>(1)
[...]
<sourceDirectory>../src/main/java</sourceDirectory>
<testSourceDirectory>../src/test/java</testSourceDirectory>
[...]
</build>
[...]
</project>
1 | Explicitly directory structure configuration inside <build> element. |
Sample pom.xml file generated by IntelliJ IDEA
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>maven-app</artifactId>
<version>1.0-SNAPSHOT</version>
</project>
Dependencies
External dependencies of your project usually declared as build script dependencies. Gradle and Maven takes the responsibility to resolved build script dependencies from available repositories and its cache. Gradle and Maven applies your dependencies to different scopes of your project such as during compile or runtime. This scope defined based on the configuration.
Gradle build script dependencies can be declared in many ways.
After applying plugins like java
, java-library
, java-platform
or application
appropriate dependency configuration (e.g. api
, implementation
) will be available to declare your dependencies.
apply plugin: 'java' (1)
dependencies {
implementation 'org.apache.commons:commons-lang3:3.10' (2)
implementation(
'org.springframework:spring-core:5.2.6.RELEASE',
'org.springframework:spring-aop:5.2.6.RELEASE'
)
testImplementation('org.mockito:mockito-all:1.10.19')
implementation group: 'com.google.inject', name: 'guice', version: '4.2.3' (3)
implementation files('hibernate.jar', 'libs/spring.jar') (4)
implementation fileTree('libs') (5)
implementation project(':shared') (6)
}
1 | Plugin makes available dependency configuration in your build script, so that you can use 'implementation', 'testImplementation' for dependencies. |
2 | The group:name:version style string notation. |
3 | Map-style notation. |
4 | Declaring arbitrary files as dependencies. |
5 | Putting all jars from 'libs' onto compile classpath. |
6 | Internal shared project/module as dependency. |
Most used dependency configuration:
- api
-
Declaring API dependencies.
- implementation
-
extends compile. Implementation only dependencies.
- compileOnly
-
Compile time only dependencies, not used at runtime.
- runtimeOnly
-
Runtime only dependencies.
- testImplementation
-
extends testCompile, implementation. Implementation only dependencies for tests.
- testCompileOnly
-
Additional dependencies only for compiling tests, not used at runtime.
- testRuntimeOnly
-
extends runtimeOnly Runtime only dependencies for running tests.
- annotationProcessor
-
Annotation processors used during compilation.
Lear more from the official documentation of DependencyHandler and Gradle Java Plugin.
Maven dependencies declared inside <dependencies>
element.
Default scope of dependencies is compile.
<project>
[...]
<dependencies>
[...]
<dependency>(1)
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>1.0</version>
<scope>runtime</scope>(2)
</dependency>
<dependency>(3)
<groupId>com.sample</groupId>
<artifactId>sample</artifactId>
<version>1.0</version>
<scope>system</scope>
<systemPath>${project.basedir}/Name_Your_JAR.jar</systemPath>
</dependency>
[...]
</dependencies>
[...]
</project>
1 | Dependency declaration. |
2 | Scope declaration. If nothing is specified default is compile. |
3 | Local jar as dependency.
This feature is deprecated.
It’s recommended to used local file repository instead. |
Most used dependency scope:
- compile
-
This is the default scope, used if none is specified.
- provided
-
This is much like compile, but indicates you expect the JDK or a container to provide the dependency at runtime.
- runtime
-
This scope indicates that the dependency is not required for compilation, but is for execution.
- test
-
This scope indicates that the dependency is only available for the test compilation and execution phases.
Transitive Dependencies
Transitive dependencies are the dependencies of your dependency in your build script. Gradle and Maven, both tools automatically resolves transitive dependencies. You can exclude or manage transitive dependencies by the additional configuration in your dependency.
Exclude transitive dependencies.
dependencies {
implementation('org.hibernate:hibernate:3.1') {
force = true (1)
exclude module: 'cglib' (2)
exclude group: 'org.jmock' (3)
exclude group: 'org.unwanted', module: 'iAmBuggy' (4)
transitive = false (5)
}
}
1 | In case of versions conflict '3.1' version of hibernate wins. |
2 | Excluding by module name. |
3 | Excluding by group name. |
4 | Excluding by both module and group. |
5 | Disabling all transitive dependencies of this dependency. |
Exclude transitive dependencies.
<project>
[...]
<dependencies>
[...]
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>1.0</version>
<exclusions>
<exclusion>(1)
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
[...]
</dependencies>
[...]
</project>
1 | Exclude all transitive dependencies. |
Repositories
Repositories are the home of dependencies and plugins. Your project build dependencies and plugins resolved from repositories and its cache. Usually repositories hosted on the remote servers. Public repositories such as maven central accessible to anyone. You need the additional configuration to access corporate or private repositories.
Based on your business requirement you may need to configure you private or corporate repository instead of relying on the public repository. Nexus Repository and JFrog Artifactory are the most used repository tools to configure your private or corporate repository. Both tools have free offering to get start. Most famous repository Maven Central also hosted using Nexus Repository. |
Dependency Repository
Dependency repositories helps to resolve your build script dependencies.
Gradle’s repositories can be defined using shorthand notation. Gradle will resolve the corresponding repository url.
repositories {
mavenLocal() (1)
mavenCentral() (2)
jcenter() (3)
google() (4)
}
1 | Maven local repository.
Gradle recommends avoiding mavenLocal() repository. |
2 | Maven central repository. |
3 | JCenter repository. |
4 | Google repository for android. |
Gradle’s corporate repository declaration.
repositories {
maven {
url REPOSITORY (1)
credentials { (2)
username = REPOSITORY_USERNAME
password = REPOSITORY_PASSWORD
}
mavenContent {
snapshotsOnly() (3)
releasesOnly() (4)
}
}
}
1 | Maven corporate repository url. |
2 | Credential for authentication, if any. |
3 | An optional configuration for snapshots only. |
4 | An optional configuration for release only. 3 and 4 should use separately. |
Gradle’s directory repository for local files.
repositories {
flatDir {
dirs 'lib' (1)
}
}
1 | Local directory location. |
Maven already applied default maven repository in your build script.
You don’t need to configure any <repository>
element to use the default maven central repository.
You can explicitly define any maven corporate repository in your build script.
<project>
[...]
<repositories>
[...]
<repository>(1)
<id>spring-snapshot</id>
<name>Spring Maven SNAPSHOT Repository</name>
<url>http://repo.springsource.org/libs-snapshot</url>
<snapshots>
<enabled>true</enabled>(2)
</snapshots>
<releases>
<enabled>false</enabled>(3)
</releases>
</repository>
[...]
</repository>
[...]
</project>
1 | Maven corporate repository in <repository> element. |
2 | An optional configuration for snapshots only. |
3 | An optional configuration for release only. |
Local file directory as repository can be configured like below.
<project>
[...]
<repositories>
[...]
<repository>
<id>maven-repository</id>
<url>file:///${project.basedir}/maven-repository</url>(1)
</repository>
[...]
</repository>
[...]
</project>
1 | Maven local file directory repository. |
Additionally, your jars needs to deploy on that directory. The local artifacts can be deployed by the following command.
mvn deploy:deploy-file -Dfile=<path-to-file> -DgroupId=<group-id> -DartifactId=<artifact-id> -Dversion=<version> -Dpackaging=jar -Durl=file:./maven-repository/ -DrepositoryId=maven-repository -DupdateReleaseInfo=true
Plugin Repository
Plugin repository helps to resolve your build script plugins. By default, Gradle and Maven both tools applied their official plugin repository in your build script. To use corporate or private plugin repository additional configuration required.
Gradle’s official plugin portal repository already applied in your build script by default.
You don’t need any additional configuration to resolve plugins.
To use official gradle plugin portal repository with other corporate repositories you can use gradlePluginPortal()
short notation.
pluginManagement {
gradlePluginPortal() (1)
}
1 | Gradle plugin portal repository declaration. |
Corporate plugin repository can be configured by pluginManagement
block.
pluginManagement {
repositories {(1)
maven {
url REPOSITORY (2)
credentials { (3)
username = REPOSITORY_USERNAME
password = REPOSITORY_PASSWORD
}
mavenContent {
snapshotsOnly() (4)
releasesOnly() (5)
}
}
}
}
1 | Plugin corporate repository configuration. |
2 | Maven corporate repository url. |
3 | Credential for authentication, if any. |
4 | An optional configuration for snapshots only. |
5 | An optional configuration for release only. 4 and 5 should use separately. |
By default, maven included its default plugin repository in your build script.
No additional configuration required to use default plugin repository.
Corporate plugin repository can be configured by <pluginRepository>
element.
<project>
[...]
<pluginRepositories>
[...]
<pluginRepository>(1)
<id>acme corp</id>
<name>Acme Internal Corporate Repository</name>
<url>http://acmecorp.com/plugins</url>
<snapshots>
<enabled>true</enabled>(2)
</snapshots>
<releases>
<enabled>true</enabled>(3)
</releases>
</pluginRepository>
[...]
</pluginRepositories>
[...]
</project>
1 | Plugin corporate repository. |
2 | Snapshot settings enabled. |
3 | Release settings enabled. |
Plugins
Plugin is the most important topic that is often overlooked. Plugin makes both of the build tools highly extensible. Most of the build features we use throughout our build process are comes from plugins. Build script plugins resolved from plugin repository. Plugin is one of the best topic to understand the core difference between two tools.
- Gradle
-
java
,java-library
,java-platform
andapplication
are among other core plugins intended to make your java build process easier. Gradle does not apply these plugins in your build script by default. By applying any of them, many configurations (e.g. default directory convention, dependencies configuration ) will be applied implicitly in your project build script. - Maven
-
Maven defines three lifecycles which are default lifecycle, clean lifecycle, site lifecycle. Plugin bindings for default lifecycle are defined separately for every packaging (e.g.
jar
,pom
). So, based on your packaging type maven applies different plugins in your build script. Forjar
packaging maven appliesmaven-compiler-plugin
,maven-install-plugin
,maven-deploy-plugin
,maven-jar-plugin
,maven-resources-plugin
,maven-surefire-plugin
plugins in your build script by default. You don’t need to declare these plugins explicitly unless you want to override the default configuration.
Plugin Declaration
plugins {
id 'java' (1)
id 'java-library' apply false (2)
id "org.springframework.boot" version "2.0.4.RELEASE" (3)
}
apply plugin: "java-library" (4)
1 | Java plugin declaration. Gradle will apply this plugin in the build by default. |
2 | Java library plugin declaration, default apply turned off. Gradle will not apply this plugin in the build. Need to apply explicitly to use this plugin. |
3 | Unofficial Gradle plugin. Version property must be mentioned. |
4 | Java library plugin applied explicitly. |
<project>
[...]
<build>
[...]
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>(1)
<version>3.8.1</version>
</plugin>
</plugins>
[...]
</build>
[...]
</project>
1 | Compiler plugin explicit declaration by <plugin> element. |
Plugin Explicitly Configuration
Plugin can be configured explicitly. For example, here we will configure a Java Compiler argument which is often miss understood by developers.
We can configure Java compiler to use the -source
and -target
compiler arguments, however it does not guarantee a cross-compilation.
We need to configure additional compiler argument -bootclasspath
.
Most of the cases developers ignored the later options.
Starting from Java 9 we can configure our java compiler to use a new -release
compiler argument which is the short form of all three compiler arguments together -source
, -target
and -bootclasspath
.
Often -source
and -target
compiler argument configured in this way.
It does not guarantee cross compilation.
compileJava {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
To leverage the -release
compiler argument, we can configure in this way.
compileJava { (1)
options.compilerArgs.addAll(['--release', '11'])
}
1 | Configured by compileJava block. compileJava configuration block enabled by java or java-library plugin. |
Alternatively, this can be configured in this way.
tasks.withType(JavaCompile) { (1)
options.compilerArgs.addAll(['--release', '11'])
}
1 | Configured by Tasks configuration.
JavaCompile task enabled by java or java-library plugin. |
Often -source
and -target
compiler argument configured in this way.
It does not guarantee cross compilation.
<project>
[...]
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
[...]
</project>
Starting from maven-compiler-plugin
version 3.6, we can configure -release
compiler argument in this way.
<project>
[...]
<build>
[...]
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<release>11</release>(1)
</configuration>
</plugin>
</plugins>
[...]
</build>
[...]
</project>
1 | Configured by <configuration> element. |
Alternatively, this can be configured in this way.
<project>
[...]
<properties>
<maven.compiler.release>11</maven.compiler.release>(1)
</properties>
[...]
<build>
[...]
<plugins>
<plugin>(2)
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
</plugin>
</plugins>
[...]
</build>
[...]
</project>
1 | Configured by <properties> element. |
2 | Configured by <properties> element will not work because the maven-compiler-plugin default version we use doesn’t rely on a recent enough version.
So we need to explicitly declare a recent version. |
Multi Module Project
Gradle and Maven both tools offer convenient way to configure multiple module project with minimal effort.
Multi module gradle project
include ':module-one',
':module-two',
':module-three' (1)
1 | Multi module declaration |
Multi module maven project
<project>
[...]
<modules>
<module>module-one</module>
<module>module-two</module>
<module>module-three</module>
</modules>(1)
[...]
</project>
1 | Multi module declaration |
Additional Resources
Watch
-
Pluralsight - Maven Fundamentals by Bryan Hansen.
-
Duration 2h 24m.
-
Released 28 Jun 2019.
-
-
Pluralsight - Gradle Build Tool Fundamentals by Kevin Jones.
-
Duration 2h 30m.
-
Released 15 Apr 2020.
-