Java Build Tools: Ant vs. Maven vs. Gradle

This is an abbreviated chapter from my book Java for the Real World. Want more content like this? Click here to get the book!

For anything but the most trivial applications, compiling Java from the command line is an exercise in masochism. The difficulty including dependencies and making executable .jar files is why build tools were created.

For this example, we will be compiling this trivial application:

package com.example.iscream;

import com.example.iscream.service.DailySpecialService;
import java.util.List;

public class Application {
    public static void main(String[] args) {
        System.out.println("Starting store!\n\n==============\n");

        DailySpecialService dailySpecialService = new DailySpecialService();
        List<String> dailySpecials = dailySpecialService.getSpecials();

        System.out.println("Today's specials are:");
        dailySpecials.forEach(s -> System.out.println(" - " + s));
package com.example.iscream.service;

import java.util.List;

public class DailySpecialService {

    public List<String> getSpecials() {
        return Lists.newArrayList("Salty Caramel", "Coconut Chip", "Maui Mango");


The program make has been used for over forty years to compile source code into applications. As such, it was the natural choice in Java’s early years. Unfortunately, a lot of the assumptions and conventions with C programs don’t translate well to the Java ecosystem. To make (har) building the Java Tomcat application easier, James Duncan Davidson wrote Ant. Soon, other open source projects started using Ant, and from there it quickly spread throughout the community.

Build files

Ant build files are written in XML and are called build.xml by convention. I know even the word “XML” makes some people shudder, but in small doses it isn’t too painful. I promise. Ant calls the different phases of the build process “targets”. Targets that are defined in the build file can then be invoked using the ant TARGET command where TARGET is the name of the target.

Here’s the complete build file with the defined targets:


    <path id="classpath">
        <fileset dir="lib" includes="**/*.jar"/>

    <target name="clean">
        <delete dir="build"/>

    <target name="compile">
        <mkdir dir="build/classes"/>
        <javac srcdir="src/main/java"

    <target name="jar">
        <mkdir dir="build/jar"/>
        <jar destfile="build/jar/IScream.jar" basedir="build/classes"/>

    <target name="run" depends="jar">
        <java fork="true" classname="com.example.iscream.Application">
                <path refid="classpath"/>
                <path location="build/jar/IScream.jar"/>


With these targets defined, you may run ant clean, ant compile, ant jar, ant run to compile, build, and run the application we built.

Of course, the build file you’re likely to encounter in a real project is going to be much more complex than this example. Ant has dozens of built-in tasks, and it’s possible to define custom tasks too. A typical build might move around files, assemble documentation, run tests, publish build artifacts, etc. If you are lucky and are working on a well-maintained project, the build file should “just work”. If not, you may have to make tweaks for your specific computer. Keep an eye out for .properties files referenced by the build file that may contain configurable filepaths, environments, etc.


While setting up a build script takes some time up front, hopefully you can see the benefit of using one over passing commands manually to Java. Of course, Ant isn’t without its own problems. First, there are few enforced standards in an Ant script. This provides flexibility, but at the cost of every build file being entirely different. In the same way that knowing Java doesn’t mean you can jump into any codebase, knowing Ant doesn’t mean you can jump into any Ant file–you need to take time to understand it. Second, the imperative nature of Ant means build scripts can get very, very long. One example I found is over 2000 lines long! Finally, we learned Ant has no built-in capability for dependency management, although it can be supplemented with Ivy. These limitations along with some other build script annoyances led to the creation of Maven in the early 2000s.


Maven is really two tools in one: a dependency manager and a build tool. Like Ant it is XML-based, but unlike Ant, it outlines fairly rigid standards. Furthermore, Maven is declarative allowing you to define what your build should do and less about how to do it. These advantages make Maven appealing; build files are much more standard across projects and developers spend less time tailoring the files. As such, Maven has become somewhat of a de facto standard in the Java world.

Maven Phases

The most common build phases are included in Maven and can be executed by running mvn PHASE (where PHASE is the phase name). The most common phase you will invoke is install because it will fully build and test the project, then create a build artifact.

Although it isn’t actually a phase, the command mvn clean deserves a mention. Running that command will “clean” your local build directory (i.e. /target), and remove compiled classes, resources, packages, etc. In theory, you should just be able to run mvn install and your build directory will be updated automatically. However, it seems that enough developers (including myself) have been burned by this not working that we habitually run mvn clean install to force the project to build from scratch.

Project Object Model (POM) Files

Maven’s build files are called Project Object Model files, usually just abbreviated to POM, and are saved as pom.xml in the root directory of a project. In order for Maven to work out of the box, it’s important to follow this directory structure:

├── pom.xml
└── src
    ├── main
    │   ├── java
    │   │    <-- Your Java code goes here
    │   ├── resources
    │   │    <-- Non-code files that your app/library needs
    └── test
        ├── java
        │    <-- Java tests
        ├── resources
        │    <-- Non-code files that your tests need

As mentioned previously, Maven has dependency management built in. The easiest way to find the correct values are from the project's website or the MVNRepository site. For our build, we also need to use one of Apache's official plugins--the Shade plugin. This plugin is used to build fat .jar files.

Here's the complete POM file:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="" xmlns:xsi="" xsi:schemaLocation="             ">
                        <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">

At this point you can run mvn package and you will see the iscream-0.0.1-SNAPSHOT.jar file inside of the target folder. If you run java -jar iscream-0.0.1-SNAPSHOT.jar you can run the application.


Although Maven has made considerable strides in making builds easier, all Maven users have found themselves banging their head against the wall with a tricky Maven problem at one time or another. I've already mentioned some usability problems with plugins, but there's also the problem of "The Maven Way". Anytime a build deviates from what Maven expects, it can be difficult to put in a work-around. Many projects are "normal...except for that one weird thing we have to do". And the more "weird things" in the build, the harder it can be to bend Maven to your will. Wouldn't it be great if we could combine the flexibility of Ant with the features of Maven? That's exactly what Gradle is trying to do.


The first thing you will notice about a Gradle build script is that it is not XML! In fact, Gradle uses a domain specific language (DSL) based on Groovy, which is another programming language that can run on the JVM.

The DSL defines both the core parts of the build file and specific build steps called "tasks". It is also extensible making it very easy to define your own tasks. And of course, Gradle also has a rich third-party plugin library. Let's dive in.

Build files

Gradle build files are appropriately named build.gradle and start out by configuring the build. For our project we need to take advantage of a fat jar plugin, so we will add the Shadow plugin to the build script configuration.

In order for Gradle to download the plugin, it has to look in a repository, which is an index for artifacts. Some repositories are known to Gradle and can be referred to simply as mavenCentral() or jcenter(). The Gradle team decided to not reinvent the wheel when it comes to repositories and instead relies on the existing Maven and Ivy dependency ecosystems.


Finally after Ant's obscure "target" and Maven's confusing "phase", Gradle gave a reasonable name to their build steps: "tasks". We use Gradle's apply to give access to certain tasks. (The java plugin is built in to Gradle which is why we did not need to declare it in the build's dependencies.)

The java plugin will give you common tasks such as clean, compileJava, test, etc. The shadow plugin will give you the shadowJar task which builds a fat jar. To see a complete list of the available tasks, you can run gradle -q tasks.

Dependency Management

We've already discussed how a build script can rely on a plugin dependency, likewise the build script can define the dependencies for your project. Here's the complete build file:

buildscript {
    repositories {
    dependencies {
        classpath 'com.github.jengelman.gradle.plugins:shadow:1.2.4'

apply plugin: 'java'
apply plugin: 'com.github.johnrengelman.shadow'

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8
targetCompatibility = 1.8

repositories {

dependencies {
    compile group: '', name: 'guava', version: '21.0'

shadowJar {
    baseName = 'iscream'
    manifest {
        attributes 'Main-Class': 'com.example.iscream.Application'

Now that the build knows how to find the project's dependencies, we can run gradle shawdowJar to create a fat jar that includes the Guava dependency. After it completes, you should see /build/lib/iscream-0.0.1-SNAPSHOT-all.jar, which can be ran in the usual way (java -jar ...).


Gradle brings a lot of flexibility and power to the Java build ecosystem. Of course, there is always some danger with highly customizable tools--suddenly you have to be aware of code quality in your build file. This is not necessarily bad, but worth considering when evaluating how your team will use the tool. Furthermore, much of Gradle's power comes from third-party plugins. And since it is relatively new, it still sometimes feels like you are using a bunch of plugins developed by SomeRandomPerson. You may find yourself comparing three plugins that ostensibly do the same thing, each have a few dozen GitHub stars, and little documentation to boot. Despite these downsides, Gradle is gaining popularity and is particularly appealing to developers who like to have more control over their builds.

For a more in-depth comparison and other practical advice about the Java ecosystem, check out my book Java for the Real World.

Click here to get Java for the Real World!

Announcing Java for the Real World

When I started my first Java job, I was immediately overwhelmed by my knowledge gaps of the Java ecosystem. I knew how to write decent code and had a good understanding of the Java language, but I had never used Hibernate or “deployed a war to Tomcat” …and what’s a “pom”? I soon found that answers to these questions were less than straightforward. Wading through pages of dense documentation and poorly written tutorials often left me just as confused as when I started.

That’s why I decided to write Java for the Real World. Having lived through the pain of learning the Java ecosystem, I wanted to create a resource for anyone else in their first Java job to quickly become aware of all the ancillary technologies that Java uses. I intentionally do not provide deep tutorials in the book. Not only would that be an insurmountable task (nearly all of these technologies have books of their own), but I have found that companies use the tools in such different ways that tutorials only go so far. Instead, I provide an overview of the most common tools you are likely to encounter, example code to see the technology in action, and suggested resources for more in-depth study.

I’m also trying something new with this book. Instead of waiting to finish it, I am making the in-progress version available today for 50% off the suggested price. If you purchase today, you will get immediate access to everything I have already written and updates as more chapters are completed!

Click here to save 50%!

Markov Chains in Scala

Although Markov chains have use in machine learning, a more trivial application that pops up from time-to-time is in text generation. Given a sufficiently large enough corpus, the generated text will usually be unique and comprehensible (at least from sentence to sentence).

The full code and a sample corpus used in this post can be found here.

To store the corpus information, we will use a Map.

import scala.collection.mutable

val MARKOV_MAP:mutable.Map[Seq[String], mutable.Map[String, Int]] = new mutable.HashMap()

This structure maps chains of words to “next” words, according to how frequently those words follow the chain. For example, the corpus “I like apples. I like apples. I like oranges. I hate apples.” could create the this structure:

I like -> [(apples -> 2), (oranges -> 1)]
I hate -> [(apples -> 1)]

I say “could” because we can choose a chain size. A larger chain size will produce sentences more similar to those in the corpus, and a smaller chain size will result in more diversion from the corpus.

val CHAIN_SIZE = 2

Having established a chain size, the following function creates the chains from a sentence, and loads the data into the Markov map.

def adjustProbabilities(sentence:String):Unit = {
  val segments = sentence.split(" ").+:("").:+("").sliding(CHAIN_SIZE + 1).toList
  for(segment <- segments) {
    val key = segment.take(CHAIN_SIZE)
    val probs = MARKOV_MAP.getOrElse(key, scala.collection.mutable.Map())
    probs(segment.last) = probs.getOrElse(segment.last, 0) + 1
    MARKOV_MAP(key) = probs

Line 2 looks a bit intimidating, but all we are doing is splitting the sentence into words, adding a start empty string and terminal empty string (we’ll see why shortly), and using sliding to process the sentence in chunks. For example, the sentence “Shall I compare thee to a summer’s day” we get the list [["","Shall","I"],["Shall","I","compare"],["I","compare","thee"],["compare","thee","to"],["thee","to","a"],["to","a","summer's"],["a","summer's","day"],["summer's","day",""]].

In general, we don’t want to consider “Shall” and “shall” as separate words include commas, etc. so I also created a method to normalize the corpus. You may need to make adjustments for your specific corpus.

def normalize(line: String): String = {
    .filterNot("\\.-,\";:&" contains _)

Now we can read in a corpus and process it into the Markov map.

val filePath = "/path/to/shakespeare_corpus.txt"

  .map(s => s.trim)
  .foreach(s => adjustProbabilities(s))

This assumes each line is a sentence. If your corpus has multiple sentences per line, you might use something like this instead:

  .map(s => s.trim)
  .foreach(s => adjustProbabilities(s))

Now that the map is built, we can work on generating text. We first need to isolate words that start sentences, which we can do by leveraging the empty string inserted earlier.

val startWords = MARKOV_MAP.keys.filter(_.head == "").toList

A primary feature of Markov chains is that they only care about the current state, which in this case is a chain of words. Given a chain of words, we want to randomly select the next word, according to the probabilities established earlier.

import scala.util.Random
val r = new Random()

def nextWord(seed:Seq[String]):String = {
  val possible = MARKOV_MAP.getOrElse(seed, List())
  r.shuffle(possible.flatMap(pair => List.fill(pair._2)(pair._1))).head

This is admittedly a little ham-handed and likely not performant for large corpuses, but in my testing there was no noticeable delay. First we expand the list of possible words into a list with duplicates according to their probabilities. For example [("apple", 2), ("orange", 1), ("pear", 3)] expands to ["apple", "apple", "orange", "pear", "pear", "pear"]. Then we shuffle the list, and pull off the first word. This becomes the next word in the sentence.

Now that we have a method to generate words, we can start with a random starting word (startWords) and build the sentence from there. The process knows to stop when it reaches a terminal word, i.e. a word empty string.

import scala.collection.mutable.ArrayBuffer
def nextSentence():String = {
  val seed = startWords(r.nextInt(startWords.size))
  val sentence:ArrayBuffer[String] = ArrayBuffer()
  while(sentence.last != "") {
    sentence.append(nextWord(sentence.view(sentence.size - CHAIN_SIZE, sentence.size)))
  sentence.view(1, sentence.size - 1).mkString(" ").capitalize + ","

Since my sample corpus was Shakespeare’s sonnets, I generated 14 lines:

(0 until 14).map(_ => nextSentence()).mkString("\n")

With a little formatting cleanup…

Oaths of thy beauty being mute,
Unthrifty loveliness why dost thou too and therein dignified,
Ah! If thou survive my wellcontented day,
Betwixt mine eye,
These poor rude lines of thy lusty days,
Devouring time blunt thou the master mistress of my faults thy sweet graces graced be,
Leaving thee living in posterity?
Compared with loss and loss with store,
Proving his beauty new,
Whilst I thy babe chase thee afar behind,
Coral is far more red than her lips red,
Admit impediments love is as a careful housewife runs to catch,
Lascivious grace in whom all ill well shows,
Savage extreme rude cruel not to be.

Feel free to use my corpus or choose your own! Project Gutenberg is a great source.

How to combine Scala pattern matching with regex

Scala’s pattern matching is arguably one of its most powerful features and is straight-forward to use when matching on patterns like x::xs vs. x vs. Nil, but you can also use it to match regular expressions. This short tutorial will show you how to use pattern matching and regex to parse a simple DSL for filtering search results.

The domain of the tutorial is a library system where users can search by author, title, or year. They can also combine filters to make the search results more narrow. We’ll start by defining some objects to work with.

case class Book(title:String, author:String, year:Int)

val books = List(
  Book("Moby Dick", "Herman Melville", 1851),
  Book("A Tale of Two Cities", "Charles Dickens", 1859),
  Book("Oliver Twist", "Charles Dickens", 1837),
  Book("The Adventures of Tom Sawyer", "Mark Twain", 1876),
  Book("The Left Hand of Darkness", "Ursula Le Guin", 1969),
  Book("Never Let Me Go", "Kazuo Ishiguro", 2005)

To filter the books, we need to supply one or more predicates. A predicate is a function that accepts a Book and returns a Boolean. Our goal is to turn something like “author=Charles Dickens” into a predicate. For starters, we need to be able to parse out user-supplied value “Charles Dickens”. Scala’s regex compiler allows for groups to be surrounded by parentheses which can then be extracted as values. The example in the documentation is val date = """(\d\d\d\d)-(\d\d)-(\d\d)""".r. You can see there are three groups defined: one each for year, month, and day. Here are the patterns we’ll allow to constrain search results:

val authorEquals = """author=([\w\s]+)""".r
val authorLike   = """author~([\w\s]+)""".r
val titleEquals  = """title=([\w\s]+)""".r
val titleLike    = """title~([\w\s]+)""".r
val yearBefore   = """year<(\d+)""".r
val yearAfter    = """year>(\d+)""".r

Remember that the goal is to return a predicate for each filter. The syntax for an anonymous predicate is (b:Book) => [boolean]. Using our example, we could create a predicate (b:Book) => == "Charles Dickens". To make the function generic, we need to be able to extract the supplied author value from the filter. Using the predefined regular expressions combined with pattern matching, we can do just that.

def parseFilter(filterString:String):Book => Boolean = filterString match {
  case authorEquals(value) => (b:Book) => == value

The filterString is passed in and pattern matched against the pre-defined regular expression authorEquals. Since we declared one group in the expression, we can name that group (value) and then use that group as a variable. Here’s the complete function that includes all of the expressions.

def parseFilter(filterString:String):Book => Boolean = filterString match {
  case authorEquals(value) => (b:Book) => == value
  case authorLike(value)   => (b:Book) =>
  case titleEquals(value)  => (b:Book) => b.title == value
  case titleLike(value)    => (b:Book) => b.title.contains(value)
  case yearBefore(value)   => (b:Book) => b.year < Integer.valueOf(value)
  case yearAfter(value)    => (b:Book) => b.year > Integer.valueOf(value)
  case _                   => (b:Book) => false

The last case catches any filter that doesn’t match a pattern and returns a predicate that does not match any book. The functional result being that an invalid filter returns no search results.

Finally, we need to be able to check a book against one or more filters. The forall method is true only if all of the filters match the given book.

def checkBook(b:Book, filterString:String) = {
  val filters = filterString.split(",").map(s => parseFilter(s))

We now have everything in place to filter the books according to our search string. Here are some examples:

books.filter(b => checkBook(b, "author=Charles Dickens"))
res0: List[Book] = List(
    Book(A Tale of Two Cities,Charles Dickens,1859),
    Book(Oliver Twist,Charles Dickens,1837))

books.filter(b => checkBook(b, "author=Charles Dickens,year>1840"))
res1: List[Book] = List(
    Book(A Tale of Two Cities,Charles Dickens,1859))

books.filter(b => checkBook(b, "title~of"))
res2: List[Book] = List(
    Book(A Tale of Two Cities,Charles Dickens,1859),
    Book(The Adventures of Tom Sawyer,Mark Twain,1876),
    Book(The Left Hand of Darkness,Ursula Le Guin,1969))

Try to add some more filters such as “starts with” or “year equals” to get practice working with regex matching.

How to Add Integration Tests to a Play Framework Application Using Scala

If you are new to the Play framework and want to learn more about how Play tests are set up, or if you are new to the idea of HTTP integration testing, I encourage you to check out the tutorial I recently wrote for the Semaphore Community.

In the tutorial, I walk you through creating a simple JSON API using a library as the domain example. Then, I show you how to add tests to that API that make actual HTTP calls.

Here’s a preview of the tutorial contents

  1. Introduction
  2. Prerequisites
  3. Setting Up the Application
    1. Create a New Project
    2. Add routes
    3. Add Controllers
    4. Adding Models and a Repository
    5. Implementing the Controllers
  4. Adding Integration Tests
    1. Testing the Application Index
    2. Books Controller Tests
    3. Customer Controller Tests
  5. Conclusion

Click here to read the full tutorial!