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 and XML. 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) based XML. It also supports C#, 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

    • Maven

    Gradle directory convention implicitly configured by plugins. You can change this default directory by SourceSet block.

    Gradle default directory structure

    Gradle
    • settings.gradle file is optional and only required in root project for multi module projects.

    • build directory is generated output directory.

    Override default configuration

    build.gradle
    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

    build.gradle
    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

    Maveb
    • target directory is generated output directory.

    Override default configuration

    pom.xml
    <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

    pom.xml
    <?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

    • Maven

    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.

    build.gradle
    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.

    pom.xml
    <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.

    • Gradle

    • Maven

    Exclude transitive dependencies.

    build.gradle
    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.

    pom.xml
    <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

    • Maven

    Gradle’s repositories can be defined using shorthand notation. Gradle will resolve the corresponding repository url.

    build.gradle
    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.

    build.gradle
    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.

    build.gradle
    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.

    pom.xml
    <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.

    pom.xml
    <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

    • Maven

    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.

    settings.gradle
    pluginManagement {
        gradlePluginPortal() (1)
    }
    1 Gradle plugin portal repository declaration.

    Corporate plugin repository can be configured by pluginManagement block.

    settings.gradle
    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.

    pom.xml
    <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 and application 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. For jar packaging maven applies maven-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

    • Gradle

    • Maven

    build.gradle
    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.
    pom.xml
    <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.

    • Gradle

    • Maven

    Often -source and -target compiler argument configured in this way. It does not guarantee cross compilation.

    build.gradle
    compileJava {
        sourceCompatibility = JavaVersion.VERSION_11
        targetCompatibility = JavaVersion.VERSION_11
    }

    To leverage the -release compiler argument, we can configure in this way.

    build.gradle
    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.

    build.gradle
    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.

    pom.xml
    <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.

    pom.xml
    <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.

    pom.xml
    <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.

    • Gradle

    • Maven

    Multi module gradle project

    settings.gradle
    include ':module-one',
            ':module-two',
            ':module-three' (1)
    1 Multi module declaration

    Multi module maven project

    pom.xml
    <project>
        [...]
        <modules>
            <module>module-one</module>
            <module>module-two</module>
            <module>module-three</module>
        </modules>(1)
        [...]
    </project>
    1 Multi module declaration

    Additional Resources

    Watch

    References