Thursday, December 18, 2014

Maven vs Gradle

Note: Maven and Gradle are both complex tools. They both can do many things. My goal for this post isn't to cover everything that they can do. Instead I'm only going to cover a small subset of them in hopes that it will somehow be helpful.

There are several ways to create a Java project using Maven. I'm only going to show the way that uses the command line. To do that we'll be using an archetype. An archetype is basically a skeleton project. That is, Maven is going to create the necessary folder structure for our project.

Here's how to create a Java project through the command line using Maven:
mvn archetype:generate 
  -DarchetypeArtifactId=maven-archetype-quickstart
It will then ask you to define the value for the following properties:
  • groupId
  • artifactId
  • version
  • package
Maven's groupId is similar to Java's package statement in that it's there to avoid naming conflicts. Recall that you can't have two classes with the same name in the same package. Likewise you can't have two artifactId's with the same value and groupId.

According to Java's convention for naming packages:
Companies use their reversed Internet domain name to begin their package names—for example, com.example.mypackage for a package named mypackage created by a programmer at example.com.
The artifactId is the name of your project. It's also the name Maven gives to the jar file when you package your application for distribution. The type of packaging to use is specified by the package property. By default it's set to jar but you can also set it to war and ear.

When the maven-archetype-quickstart archetype finishes you should see the following folder structure for your project:
  • src
    • main
      • java
    • test
      • java
This is the same folder structure that Gradle uses. Speaking of Gradle, let's look at how to use it to create a skeleton Java project from the command line. Unfortunately because this is an incubating feature the way to do this has changed and probably will change again in the future. The original way to create a skeleton Java project when it was first introduced in Gradle 1.7 was to do this:
gradle setupBuild --type java-library
It has since changed and the current (as of version 2.2.1) way is to do it is this:
gradle init --type java-library
Note: You'll need to create the root folder for your project, but it's still less typing than what Maven would have you do.

Note: Maven's website lists maven-archetype-simple as a valid archetype. Unfortunately it appears that this archetype no longer exists. I have already made a pull request that fixes this documentation error.

If you'll recall Apache Ant used a build file named build.xml. Gradle uses a build script named build.gradle and Maven uses a configuration file called pom.xml POM stands for Project Object Model. The following is the minimum pom.xml file:
    <project>    
      <modelVersion>4.0.0</modelVersion>
      <groupId>my.company</groupId>    
      <artifactId>MyProject</artifactId>
      <version>0.0.1-SNAPSHOT</version>
    </project>
    
The modelVersion refers to the version of the (Project) Object Model that this configuration file is using.

Here's the minimum Gradle build script.

That's it! You don't need anything. Of course if you want Gradle to do anything useful you'll need to assign it some tasks to perform. The quickest way to do this is to apply a plugin to the build. For example the Java plugin has a test task which will run all of your unit tests.

Here's how to apply the Java plugin:
apply plugin: 'java'
If you wanted to compile all the Java files that are in the /src/main folder using Gradle you'd type the following on the command line:
gradle compileJava
The compiled class files are placed in a /build/classes/main folder.

For a Java project that uses Maven as its build tool you'd type the following on the command line:
mvn compile
This time the compiled class files are placed in a /target/classes folder.

We can change the location of where the source files are to be found. We can also change the destination for the compiled class files. Eclipse by default places Java files in a /src folder and compiled class files are placed in a /bin folder. To do this in Maven we need to add the following to the pom.xml file:
    <build>    
      <sourceDirectory>${project.basedir}/src</sourceDirectory>    
      <outputDirectory>${project.basedir}/bin</outputDirectory>
    </build>
    
Here's what that looks like in Gradle:
    sourceSets.main.output.classesDir = file("bin")
    sourceSets.main.java.srcDirs = ['src']
    
One of the things that Maven and Gradle are great at that Ant alone isn't is managing the dependencies of your project. For example your project probably depends on JUnit for its unit testing. JUnit itself depends on Hamcrest, which is called a transitive dependency.

Recall that I've already written a post on how to run JUnit tests in both Apache Ant and Gradle. In that post I used the following simple example.
import org.junit.Test;

import static org.junit.Assert.assertThat;
import static org.hamcrest.core.Is.is;

public class TestHelloWorld {

   @Test
   public void testAPassingTest() {
      assertThat(true, is(true));
   }

   @Test
   public void testAFailingTest() {
      assertThat(true, is(false));
   }
}
To compile this from the command line you would type
javac -cp junit-4.11.jar;hamcrest-core-1.3.jar TestHelloWorld.java
And then to run it you would do:
java -cp .;junit-4.11.jar;hamcrest-core-1.3.jar 
  org.junit.runner.JUnitCore TestHelloWorld
Here's what that looks like if the class was in the package my.company:
javac -cp junit-4.11.jar;hamcrest-core-1.3.jar 
  my/company/TestHelloWorld.java
java -cp .;junit-4.11.jar;hamcrest-core-1.3.jar 
  my/company/org.junit.runner.JUnitCore TestHelloWorld
Notice how much typing you have to do every time you want to run this unit test. That's a lot of time being wasted. Not only that but its extremely prone to human error. You might, for example, forget one of the transitive dependencies. Or perhaps you make a typo in listing the names of the jar files. Something else to consider is if you have several projects that need to be unit tested chances are you'll probably be making several copies of the jar files. So you'll be wasting memory as well. These aren't really an issue, though, if you use a dependency management tool like Maven or Gradle.

We'll need to add the following to our pom.xml file in order to tell Maven that our project depends on JUnit:
<dependencies>
 <dependency>
   <groupId>junit</groupId>
   <artifactId>junit</artifactId>
   <version>4.11</version>
   <scope>test</scope>
 </dependency>
</dependencies>
    
Notice that we've also specified that this dependencies is only required for the unit tests of our application. If we try to do an assertThat in one of the Java files in the /src/main/java folder we'll get an error.

Here's how to run all of the unit tests via Maven:
mvn test
You can limit the scope of a dependency in Gradle just like you can in Maven. But unlike Maven, you have to specify the location of the repository in Gradle. Fortunately though this is pretty easy to do. Here's what the Gradle build script looks like:
apply plugin: 'java'

repositories {
  mavenLocal()
  mavenCentral()
}
    
dependencies {
  testCompile group: 'junit', name: 'junit', version: '4.11'
}
We can actually shorten the part where we're declaring the dependency like so:
dependencies {
  testCompile 'junit:junit:4.11'
}
To run all of the unit tests via Gradle just type the following:
gradle test
When an application becomes significantly large it can become difficult to maintain. A common technique in the software world is to break such projects into multiple pieces that only focus on a specific aspect of the application (ie separation of concerns). Popular open source frameworks like Spring, Hibernate, and even Apache Struts employ this technique.

Creating a multi-module project in Maven is just like creating a regular project except that you need to change the packaging element in the pom.xml file from jar to pom. That is, change this...
<packaging>jar</packaging>
...to this...
<packaging>pom</packaging>
Then cd into the parent project's directory and type
mvn archetype:generate -DarchetypeArtifactId=maven-archetype-quickstart
Note: The module's groupId does not have to be the same as the parent's groupId.

If you look in the parent project's pom.xml file you'll notice that Maven added a modules element. For example here's what Struts modules element looks like:
    
<modules>
  <module>bom</module>
  <module>xwork-core</module>
  <module>core</module>
  <module>apps</module>
  <module>plugins</module>
  <module>bundles</module>
  <module>archetypes</module>
</modules>
You'll also notice that Maven adds a parent element in the module's pom.xml. For example in Struts' core module you'll see this:
    
<parent>    
  <groupId>org.apache.struts</groupId>
  <artifactId>struts2-parent</artifactId>
  <version>2.3.21-SNAPSHOT</version>
</parent>
Basically this causes the module to inherit the dependencies declared in the parent project's pom.xml. If there are dependencies in your parent project that you don't want your modules to inherit you can set the optional element for that dependency to true. For example, suppose the parent project dependent on spring-core in order to compile. Here's how to prevent the modules from inheriting that dependency:
    
<dependency>    
  <groupId>org.springframework</groupId>    
  <artifactId>spring-core</artifactId>
  <version>4.1.3.RELEASE</version>
  <scope>compile</scope>
  <optional>true</optional>
</dependency>
You can create multi-module projects in Gradle as well. (Gradle calls them multi-project projects, but its the same thing.) First you'll need to create a settings.gradle file in the root folder of the parent project. In this file you specify the projects to be included in the build. For example here's what Struts settings file would look like:
include "bom", "xwork-core", "core", "apps", "plugins", "bundles", "archetypes"
Recall that in Maven all of the "sub-projects" inherit the dependencies that are declared in the root project. This is not how Gradle works. In Gradle all dependencies are optional by default. Not even the plugins are inherited. You can see this for yourself by performing the following steps.

Step 1: Create a folder named Project
Step 2: Create a build.gradle file with the following contents and place it in the Project folder
apply plugin: 'java'

repositories {
  mavenLocal()
  mavenCentral()
}

dependencies {
  testCompile 'junit:junit:4.11'
}
Step 3: Create a settings.gradle file with the following contents and place it in the Project folder
include 'Module'
Step 4: Create a subfolder under Project named Module
Step 5: Open a command prompt window and cd to the location of the Module folder
Step 6: Type the following on the command prompt and then press Enter
gradle dependencies
You should see something similar to the following.
    :ProjectB:dependencies

    ------------------------------------------------------------
    Project :ProjectB
    ------------------------------------------------------------

    No configurations

    BUILD SUCCESSFUL
    
Step 7: Run the dependencies task on the Project folder

You should see something similar to the following.
    :dependencies

    ------------------------------------------------------------
    Root project
    ------------------------------------------------------------
    archives - Configuration for archive artifacts.
    No dependencies

    compile - Compile classpath for source set 'main'.
    No dependencies

    default - Configuration for default artifacts.
    No dependencies

    runtime - Runtime classpath for source set 'main'.
    No dependencies

    testCompile - Compile classpath for source set 'test'.
    \--- junit:junit:4.11
        \---org.hamcrest:hamcrest-core:1.3

    testRuntime - Runtime classpath for source set 'test'.
    \--- junit:junit:4.11
        \--- org.hamcrest:hamcrest-core:1.3

    BUILD SUCCESSFUL
    
Notice that the parent project has all the configurations from the java plugin and a dependency on junit.

Here's how to configure the parent project's build script so that all of the sub-projects inherit both its plugins and its dependencies:
allprojects {
  apply plugin: 'java'

  repositories {
    mavenLocal()
    mavenCentral()
  }

  dependencies {
    testCompile 'junit:junit:4.11'
  }
}    
Well that's it. I think this post is long enough already so I'm just going to end it here.
References
From Maven's website:

From Gradle's User Guide:

TutorialsPoint's Maven POM

Jakob Jenkov's Maven Tutorial

mkyon's How To Run Unit Test With Maven

Wikipedia's entry for Separation of Conerns

"Chapter 7 : Multi-project Builds" from Hubert Klein Ikkink's Gradle Effective Implementation Guide

No comments:

Post a Comment