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 com.google.common.collect.Lists;
import java.util.List;
public class DailySpecialService {
public List<String> getSpecials() {
return Lists.newArrayList("Salty Caramel", "Coconut Chip", "Maui Mango");
}
}
Ant
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:
<project>
<path id="classpath">
<fileset dir="lib" includes="**/*.jar"/>
</path>
<target name="clean">
<delete dir="build"/>
</target>
<target name="compile">
<mkdir dir="build/classes"/>
<javac srcdir="src/main/java"
destdir="build/classes"
classpathref="classpath"/>
</target>
<target name="jar">
<mkdir dir="build/jar"/>
<jar destfile="build/jar/IScream.jar" basedir="build/classes"/>
</target>
<target name="run" depends="jar">
<java fork="true" classname="com.example.iscream.Application">
<classpath>
<path refid="classpath"/>
<path location="build/jar/IScream.jar"/>
</classpath>
</java>
</target>
</project>
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.
Summary
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
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="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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>iscream</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>21.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>2.3</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.example.iscream.Application</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
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.
Summary
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.
Gradle
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.
Tasks
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 {
jcenter()
}
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 {
mavenCentral()
}
dependencies {
compile group: 'com.google.guava', 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 ...
).
Summary
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!