In this chapter, you will learn about how Java applications are built and explore the typical life cycle of a build process. You will also learn about the value of automating the build by using specialized tools (rather than just your IDE’s out-of-the box build tool), the benefits of which allow the easy translation of the build to a continuous integration build server. Finally, you’ll review the good, bad, and ugly of the most popular build tools, with the goal of being able to make the best choice for your next Java project.
Nearly all software has to be built (or at least packaged), and Java applications are no exception. At the most basic level, the Java code you write has to be compiled into Java bytecode before it is executed on the Java Virtual Machine (JVM). However, any Java application of sufficient complexity will require the inclusion of additional external dependencies. This includes the libraries you use so often that you can forget they are third-party code, like SLF4J and Apache Commons Lang. With this in mind, a typical series of build steps for a Java application would look something like this (and the Maven users among you might recognize this):
Validate that the project is correct and all necessary information is available.
Compile the source code of the project.
Test the compiled source code by using a suitable unit-testing framework. These tests should not require the code to be packaged or deployed.
Take the compiled code and package it in its distributable format, such as a JAR.
Run any checks on results of integration tests to ensure that quality criteria are met.
Install the package into the local repository, for use as a dependency in other projects locally.
Done in the build environment, copies the final package to the remote repository for sharing with other developers and projects.
All of these steps can be done manually, and indeed it is recommended that every developer undertakes all of these steps manually at least once in their career. Much can be learned by going back to basics and working with the fundamental processes and tooling.
Although there is value in manually exploring build steps to learn, there is little to be gained from continually doing this. Not only can your (valuable) time as a developer be better used elsewhere, but automating a process makes it much more repeatable and reliable. Build tooling automates the compilation, dependency management, testing, and packaging of software applications.
Many Java application frameworks, IDEs, and platforms provide build tooling functionality out of the box, and it is often seemlessly integrated into the developer workflow; sometimes using bespoke implementations, and sometimes by using a specialized build tool. Although there are always exceptions, it is generally advantageous to utilize a specialialized build tool, as this allows all developers working on a project to successfully build the application, regardless of the operating system, IDE, or framework they are using. In the modern Java world, this is typically implemented with Maven or Gradle, but other options do exist, as you’ll explore later in this chapter.
According to the RebelLabs’ Ultimate Java Build Tool Comparison guide, a good build tool should provide the following features:
Managing dependencies
Incremental compilation
Properly handling compilation and resource management tasks across multiple modules or applications
Handling different profiles (development versus production)
Adapting to changing product requirements
Build automation
Build tools have evolved over the years, becoming progressively more sophisticated and feature rich. This provides developers with useful additions, such as the ability to manage project dependencies as well as automate tasks beyond compiling and packaging. Typically, build tools require two major components: a build script and an executable that processes the build script. Build scripts should be platform agnostic: they must be executable without modification on Windows, Linux, and Mac, and only the build tool binaries change. Within the build script, the concept of dependency management is vital, as is the structure (modularity) of the associated projects and the build process itself. Modern build tools address these concepts though the management of the following:
Build dependencies
External dependencies
Multimodule projects
Repositories
Plugins
Releasing and publishing artifacts
Before exploring several popular build tools, you first will need to be familiar with what each of the preceding concepts encompasses.
Before build tooling provided dependency management, the management of a Java application’s supporting libraries was typically done by manually copying around folders of class files and JARs! As you can imagine, this was not only time-consuming and error-prone, but critically it made a developer’s job much harder, as time and energy were needed to form a mental model of all the components (and the corresponding versions) within a codebase when assembling an application or debugging.
The majority of build tools have their own ways of handling code dependencies, as well as their differences. However, one thing is consistent: each dependency or library has a unique identity that consists of a group of some kind, a name, and a version. In Maven and Gradle, this is commonly referred to as the GAV coordinates of a dependency: group ID, artifact ID, and version. The dependencies required for your application are specified using this format, rather than the filename or URI of the external library. Your build tool is expected to know how to locate the dependency identified by the unique coordinates, and will typically download these from a centralized source, such as Maven Central or Repo@JFrog.
In the world of software development, there exists a dreaded place called dependency hell. The bigger your system grows and the more dependencies you integrate into your software, the more likely you are to find yourself languishing in this nightmare. With an application that has many dependencies, releasing new package versions can quickly become challenging, as changing one dependency may have a cascading effect.
You’ll explore the concept of semantic versioning (semver) in the next section of this chapter, but a core takeaway from this warning is that if dependency specifications are too rigid, you are in danger of version lock—the inability to upgrade a package without having to release new versions of every dependent package. However, approaching this problem from another perspective shows that if dependencies are specified too loosely, you will inevitably be affected by version promiscuity—assuming compatibility with more future versions than is reasonable. Dependency hell is where version lock and/or version promiscuity prevent you from easily and safely moving your project forward.
The majority of dependency management tooling allows you to specify a version range for your dependencies instead of a specific version if required. This allows you to find a common version of a dependency across multiple modules that have requirements. These can be extremely useful and equally dangerous if not used sensibly. Ideally, software builds should be deterministic; the resulting artifact from two identical builds (even those built some time apart) should be identical. You will explore some of the challenges with this shortly, but first let’s look into the possible ranges that you can specify. If you have used version ranges before, you will likely have used a similar, if not identical, syntax in the past. The following is the typical version range taken from the RebelLabs Java Build Tools crash course mentioned previously:
[x,y]From version x up to version y inclusive
(x,y)From version x up to version y exclusive
[x,y)From version x inclusive, up to version y exclusive
(x,y]From version x exclusive, up to version y inclusive
[x,)From version x inclusive and all greater versions
(,x]From version x inclusive and all lesser versions
[x,y),(y,)From version x inclusive and all greater versions, but specifically excluding version y
If you are using Maven to manage your dependencies, you might use something like Example 5-1 when importing the Twitter4J API.
<dependency><groupId>org.twitter4j</groupId><artifactId>twitter4j-core</artifactId><version>[4.0,)</version></dependency>
The preceding example uses semantic versioning (explained in more detail later), and essentially states that you want to import version 4.0.0 and up for the minor version of the dependency. If you initially create the project when the Twitter4J API is available at 4.0.0, this is the version you will include within your artifact. If a new 4.0.1 version is released, a new build of your artifact will include this version—and the same for the versions in this range, such as 4.0.2 and 4.0.15. However, if a new 4.1.0 or 5.0.0 version is released, this would not be included within your artifact, because this is outside the range specified. The theory behind using ranges like this is that patch upgrades will not change the API or semantic behavior of the dependency, but will include any security and nonbreaking bug fixes.
If you are regularly building and deploying your applications, any new minor upgrade will automatically get pulled in to your project without you having to manually modify any build configuration. However, the trade-off is that any updated version of the dependency may break your project—perhaps the authors of the patch did not realize that they must not break the API with a patch update, or maybe you were relying on an incorrectly functioning API. Because of this, it may be best to specify exact dependency versions, and only manually update them.
Many build tools provide plugins to ensure that you are using the most recent versions of all of your dependencies, and will warn you or fail the build if you are not. You will, of course, need a good suite of tests within your application to ensure that upgrading the libraries does not cause any unexpected failures; a compilation failure is the easiest to spot, but often not the most damaging.
Build tooling can detect and warn if you are using out-of-date build dependencies. Some even detect if a library has known security issues, which can be invaluable. But you are responsible for making sure this functionality is enabled and that you take action on any warnings. Increasingly, many of our projects are assembled from a vast array of third-party libraries and frameworks, and the applications you are building are handling data that is critical to your organization. This only increases your responsibility as a developer.
If you are working on a project that consists of multiple applications, it can be challenging to coordinate using the same dependencies across them. Sometimes this isn’t needed. For example, one of the key tenets with microservice-style systems is the independent evolvability of each of the services, and therefore you don’t want a mechanism that enforces dependencies to be upgraded in lockstep. In this case, the services should be loosely coupled, and tests or contracts used to assert that an upgrade of a component in one service does not break another.
In other situations, this coupling can be useful. Perhaps you have a group of (deliberately designed) high-coupled applications that must use the same serialization library, and all applications must be upgraded at the same time. Most modern Java dependency management tools have a concept of a parent build descriptor—for example, the Parent project object model (POM) in Maven—that allows the specification of dependencies that will be inherited via child build descriptors (for example, the dependencyManagement section within a Maven POM).
The use of parent build descriptors is powerful, but it can be abused, particularly with microservice projects. In one of my first microservice projects in 2012, my team and I decided to separate our entity classes into a separate Maven artifact that would be built and released separately from the other services that would import this. However, I soon discovered that any change and new release in the entity artifact meant that we had to build, release, and deploy all of our microservices because we were using the classes for serialization when sharing data across service boundaries. I had accidentally created a highly coupled application.
External dependencies include everything else that your project requires in order to be built successfully. This includes external executables—perhaps a JavaScript code minifier or an embedded driver—and access to any external resources—for example, a centralized security scanner. A build tool must allow you to package, execute, or specify a connection to an external resource.
Although multimodule projects are some of the least understood (and perhaps anecdotally, most abused) concepts, they can be powerful when deployed properly. Multimodule builds are effective when there are clearly defined components within an application that, although they have well-defined interfaces and clear boundaries, perhaps do not provide much value outside some large scope—in this case, the parent project. Accordingly, a multimodule project allows you to group together such components, along with the user-facing components, such as a GUI or web application, and build the entire system with a single command. The build system should be capable of determining the build order for such applications, and the resulting components and applications are all versioned with the same identifier.
The choice of how to organize your code has many implications, but your build tool should be able to build the project regardless. With the rise in popularity of the microservices architectural style, there has been increasing interest in the debate of using a single (mono)repo or multiple repositories to store your code. Although the concept sounds similar to the previously discussed ideas around multimodule projects, a multimodule project should always be stored within a single repository; this is because the modules are effectively coupled (in a positive way). However, if you are creating an application consisting of several (or many) microservices, the choice is not so clear-cut. If you have a large number of small, independently versioned code repositories, each project will have a small, manageable codebase, but what about all the code it depends on?
Several large engineering organizations prefer to keep their codebase in a single, large repository. Such a codebase is sometimes referred to as a monorepo. Monorepos have various advantages when it comes to scaling a codebase in a growing engineering organization. Having all the code in one place:
Allows a single lint, build, test, and release process.
Increases code reuse, and enables easy collaboration among many authors.
Encourages a cohesive codebase in which problems are refactored, not worked around. Tests across modules are run together, which finds bugs that touch multiple modules easier.
Simplifies dependency management within the codebase. All of the code you run with is visible at a single commit in the repo.
Provides a single place to report issues.
Makes it easier to set up a development environment.
As with many things within software development, there are always trade-offs. The negatives with using a single repository include the following:
The codebase looks more intimidating, and the cognitive load of understanding the system is typically much higher (especially if the system has low cohesion and high coupling).
The repository is typically (much) bigger in size, and there can be lots of feature branches if the team working on the codebase is large.
Loss of version information can occur; if all you have is a Git hash, how can you map this to version numbers of dependencies?
Forking dependencies into the monorepo can lead to modifications being made into this codebase that prevent upgrading of the library.
In regards to a build tool, having a large codebase in a single repository can present challenges, too. In particular, it requires a scalable version-control system and a build system that can perform fine-grained dependency management among many thousands of code modules in a single source tree. Compiling with tools that have arbitrary recursively evaluated logic becomes painfully slow. Build tools like Blaze, Pants, and Buck were designed for this type of usage in a way that other popular build tools, such as Ant, Maven, and SBT, were not.
Often, steps within a build, although not core to the build tool’s functionality, are nevertheless vital for a successful build. You often find yourself needing the same functionality over and over again, and you want to avoid copy/pasting hacks or quick fixes. This is where build tooling plugins can be useful. Plugins provide a way to package common build functionality that can be created by you or a third-party for reuse.
As we mentioned in the previous build dependency section, you are responsible for all libraries and dependencies that you bring into your application, and this also includes plugins. Be sure to research the quality of a plugin before using it, and ideally examine the associated source code. On a related note, this should not be an excuse for writing every plugin yourself. Please avoid Not Invented Here (NIH), as the developers of a well-built and maintained plugin will have done a lot of the difficult design work and edge-case handling for you.
Every build tool must be able to publish artifacts ready for deployment or consumption by a downstream dependency. Most build tools have the concept of releasing a module or code artifact. This process assigns a unique version number to a specific build, and ensures that this coordinate is also tagged appropriately in your VCS.
This section presents several of the popular Java build tools, and highlights strengths and weaknesses, with the goal of helping you decide which is best for your current project.
One of the first popular build tools in the Java space was Apache Ant. For many of us who were previously used to manually building Java applications via a combination of javac and Bash scripts, Ant was heaven sent. Ant takes an imperative approach: developers specify a series of tasks that contain instructions on how exactly to build a project. Ant was written in Java, and users of Ant could develop their own antlibs containing Ant tasks and types. A large number of ready-made commercial or open source antlibs provided useful functionality. Ant was extremely flexible and did not impose coding conventions or directory layouts to the Java projects that adopt it as a build tool. However, this flexibility was also its downfall.
The range of possible directory structures and layouts meant that nearly every project (even within the same organizations) were subtly different from each other. The flexibility of how artifacts were built also meant that developers could not rely on the availability of key life cycle events within a build, such as cleaning the workspace, verifying the build, packaging, releasing, and deploying. Developers attempted to compensate for this by writing accompanying build documentation, but this was difficult to maintain as a project evolved, and was particularly challenging for large projects consisting of many artifacts. Another challenge presented by Ant was although it followed the good design principle of single responsibility, it didn’t assist developers with managing dependencies. To address these shortcomings, Maven emerged from the Apache Turbine project as a competitor against Ant.
You can download Apache Ant from ant.apache.org. Extract the ZIP file into a directory structure of your choice. Set the ANT_HOME environment variable to this location and include the ANT_HOME/bin directory in your path. Make sure that the JAVA_HOME environment variable is set to the JDK. This is required for running Ant.
You can also install Ant by using your favorite package manager; for example:
$ apt-get install ant
$ brew install ant
Check your installation by opening a command line and typing ant -version into the command line:
$ ant -version Apache Ant(TM) version 1.10.1 compiled on February 2 2017
The system should find the command ant and show the version number of your installed Ant version.
You can see an example build.xml build script in Example 5-2.
<projectname="MyProject"default="dist"basedir="."><description>simple example build file</description><!-- set global properties for this build --><propertyname="src"location="src"/><propertyname="build"location="build"/><propertyname="dist"location="dist"/><propertyname="version"value="1.0"/><targetname="init"><!-- Create the time stamp --><tstamp/><!-- Create the build directory structure used by compile --><mkdirdir="${build}"/></target><targetname="compile"depends="init"description="compile the source"><!-- Compile the java code from ${src} into ${build} --><javacsrcdir="${src}"destdir="${build}"/></target><targetname="dist"depends="compile"description="generate the distribution"><buildnumber/><!-- Create the distribution directory --><mkdirdir="${dist}/lib"/><!-- Put everything in ${build} into theMyProject-${version}.${build.number}.jar --><jardestfile="${dist}/lib/MyProject-${version}.${build.number}.jar"basedir="${build}"/></target><targetname="clean"description="clean up"><!-- Delete the ${build} and ${dist} directory trees --><deletedir="${build}"/><deletedir="${dist}"/></target></project>
Assuming that you have packaged the Java source files in the directory structure as specified in the build script, you can build this project by using the following command:
$ ant -f build.xml
The simplest way to release and version your build artifacts with Apache Ant is to use the buildnumber task. In the dist target in the preceding build.xml, you can see the use of the <buildnumber/> declaration tag. You can also see that the build artifact destination file—shown as destfile="${dist}/lib/MyProject-${version}.${build.number}.jar"—is named as a concatenation of the string MyProject, a version variable set within the global properties and the build.number variable that is generated by the buildnumber task. This is a number that increases with each build, which ensures uniqueness.
You can also release artifacts built by Ant (and manage dependencies) by using the companion project Apache Ivy.
The Apache Maven build tool focuses on “convention over configuration,” and the primary goal is to allow a developer to comprehend the complete state of a development effort in the shortest period of time. To attain this goal, that Maven attempts to deal with several areas of concern:
Making the build process easy
Providing a uniform build system
Providing quality project information
Providing guidelines for best-practices development
Allowing transparent migration to new features
Maven allows a project to build by using its POM and a set of plugins that are shared by all projects using Maven, providing a uniform build system. Once you familiarize yourself with how one Maven project builds, you automatically know how all Maven projects build, saving you immense amounts of time when trying to navigate many projects. Maven aims to gather current principles for best-practices development, and make it easy to guide a project in that direction. For example, specification, execution, and reporting of unit tests are part of the normal build cycle using Maven. Current unit-testing best practices were used as guidelines:
Keeping your test source code in a separate, but parallel source tree
Using test case naming conventions to locate and execute tests
Have test cases set up their environment and don’t rely on customizing the build for test preparation.
Maven also aims to assist in project workflow such as release management and issue tracking.
Maven also suggests some guidelines on how to lay out your project’s directory structure so that after you learn the layout, you can easily navigate any other project that uses Maven and the same defaults. There are three built-in build life cycles: default, clean, and site. The default life cycle handles your project deployment, the clean life cycle handles project cleaning, and the site life cycle handles the creation of your project’s site documentation. Each of these build life cycles is defined by a different list of build phases, wherein a build phase represents a stage in the life cycle.
These life cycle phases (plus the other life cycle phases not noted here) are executed sequentially to complete the default life cycle. Given the preceding life cycle phases, this means that when the default life cycle is used, Maven will first validate the project, then will try to compile the sources, run those against the tests, package the binaries (e.g., JAR), run integration tests against that package, verify the integration tests, install the verified package to the local repository, and then deploy the installed package to a remote repository.
You can download Maven from the Apache Maven project web page. Ensure that the JAVA_HOME environment variable is set and points to your JDK installation, and then extract the ZIP file into a directory structure of your choice. Add the bin directory of the created directory apache-maven-3.5.2 to the PATH environment variable.
You can also install Maven by using your favorite package manager; for example:
$ apt-get install maven
$ brew install maven
Confirm everything is working:
$ mvn -v Maven home: /usr/local/Cellar/maven/3.5.2/libexec Java version: 9.0.1, vendor: Oracle Corporation Java home: /Library/Java/JavaVirtualMachines/jdk-9.0.1.jdk/Contents/Home Default locale: en_GB, platform encoding: UTF-8 OS name: "mac os x", version: "10.12.6", arch: "x86_64", family: "mac"
You can see an example pom.xml build script in Example 5-3.
<?xml version="1.0" encoding="UTF-8"?><projectxmlns="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>uk.co.danielbryant.oreilly.cdjava</groupId><artifactId>conference</artifactId><version>${revision}</version><packaging>jar</packaging><name>conference</name><description>Project for Daniel Bryant's O'Reilly Continuous Delivery with Java</description><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>1.5.6.RELEASE</version><relativePath/><!-- lookup parent from repository --></parent><properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><java.version>1.8</java.version><!-- Sane default when no revision propertyis passed in from the commandline --><revision>0-SNAPSHOT</revision></properties><scm><connection>scm:git:https://github.com/danielbryantuk/ oreilly-docker-java-shopping</connection></scm><distributionManagement><repository><id>artifact-repository</id><url>http://mojo.codehaus.org/oreilly-docker-java-shopping</url></repository></distributionManagement><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId></dependency><!-- Utils --><dependency><groupId>net.rakugakibox.spring.boot</groupId><artifactId>orika-spring-boot-starter</artifactId><version>1.4.0</version></dependency><dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId><version>3.6</version></dependency><!-- Test --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin><plugin><artifactId>maven-scm-plugin</artifactId><version>1.9.4</version><configuration><tag>${project.artifactId}-${project.version}</tag></configuration></plugin></plugins></build></project>
Assuming that all the Java code files are present and in the correct directory structure, you can build the application:
$ mvn clean install
Historically, releasing and versioning in Maven was conducted with the Maven Release plugin, but this had issues, particularly around the plugin doing lots of operations for each release: multiple clean and package cycles, POM transformations, commits, and source code management (SCM) revisions. Importantly for you, the Maven Release plugin does not support continuous delivery effectively.
Inspired by Axel Fontaine’s blog series about more-effective releasing with Maven (post-Maven 3.2.1), many developers favor using the Versions Maven plugin. At its core, the purpose of producing a release is nothing more than being able to link a version of the software as deployed onto a machine back to the matching revision of the source code in SCM. To accomplish this, you have to go through multiple steps, the minimum of which include the following:
Checking out the application code as it is
Specifying a version number so it can be uniquely identified
Building, testing, and packaging the application
Deploying the application artifact to an artifact repository where it can be deployed from
Tagging this state in the SCM so it can be associated with the matching artifact
The preceding POM has been configured to enable Fontaine’s releasing methodology. You’ll notice the <version>${revision}</version> and associated property that can be set via a command-line argument (and would typically be set as an environment variable via your continuous integration server). The <scm> and <distributionManagement> sections of the POM remain unchanged from the standard Maven approach. You can now produce releases on your CI server by invoking the following:
$ mvn deploy scm:tag -Drevision=$BUILD_NUMBER
BUILD_NUMBER is the environment variable provided by your CI server to identify the current build number for the project. For services and deliverables consumed by other teams and external parties, you can also easily combine this technique with semantic versioning by prefixing the version tag in your POM with the correct semantic version. You can then automatically produce releases internally and manually update this semantic version before each external delivery.
Maven was influential within the Java build ecosystem, but a new community emerged that believed that Maven, and the build process, was inflexible and overly opinionated. Accordingly, Gradle emerged as a potential competitor that was not only a lot more flexible, but also less verbose.
Gradle is an open source build automation system that builds upon the concepts of Apache Ant and Apache Maven and introduces a Groovy-based domain-specific language (DSL) instead of the XML form used by Apache Maven for declaring the project configuration. Gradle was designed for multiproject builds that could grow to be quite large, and it supports incremental builds by intelligently determining which parts of the build tree are up-to-date, so that any task dependent upon those parts will not need to be reexecuted. The initial plugins are primarily focused around Java, Groovy, and Scala development and deployment, but more languages and project workflows are on the roadmap.
Gradle and Maven have fundamentally different views on how to build a project. Gradle is based on a graph of task dependencies, where the tasks do the work. Maven uses a model of fixed, linear phases to which you can attach goals (the things that do the work). Despite this, migrations can be surprisingly easy because Gradle follows many of the same conventions as Maven, and dependency management works in a similar way.
You can download Gradle from the project’s installation page. You must have Java 7 or later installed in order to run the latest version of Gradle. Ensure that the JAVA_HOME environment variable is set and points to your JDK installation, and then extract the ZIP file into a directory structure of your choice. Add the bin directory of the created directory apache-maven-3.5.2 to the PATH environment variable.
You can also install Gradle by using your favorite package manager; for example:
$ apt-get install gradle
$ brew install gradle
Once you have everything installed, you can check that everything is working by executing the gradle binary with the v flag. You should see something similar to the following output:
~ $ gradle -v ------------------------------------------------------------ Gradle 4.3.1 ------------------------------------------------------------ Build time: 2017-11-08 08:59:45 UTC Revision: e4f4804807ef7c2829da51877861ff06e07e006d Groovy: 2.4.12 Ant: Apache Ant(TM) version 1.9.6 compiled on June 29 2015 JVM: 9.0.1 (Oracle Corporation 9.0.1+11) OS: Mac OS X 10.12.6 x86_64
You can see an example build.gradle build script in Example 5-4.
buildscript{ext{springBootVersion='1.5.3.RELEASE'}repositories{mavenCentral()}dependencies{classpath("org.springframework.boot:↵spring-boot-gradle-plugin:${springBootVersion}")classpath("net.researchgate:gradle-release:2.6.0")}}applyplugin:'java'applyplugin:'eclipse'applyplugin:'org.springframework.boot'applyplugin:'net.researchgate.release'version='0.0.1-SNAPSHOT'sourceCompatibility=1.8repositories{mavenCentral()}dependencies{compile('org.springframework.boot:spring-boot-starter')compile('org.springframework.boot:spring-boot-starter-web')compile('com..guava:guava:21.0')compile'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.8.6'testCompile('org.springframework.boot:spring-boot-starter-test')}
Assuming that all of the code is included in the expected directory structure, you can build the project by executing the following command:
$ gradle build
There are several popular options for releasing and publishing Gradle artifacts, but here you’ll focus on the ResearchGate gradle-release plugin, which is for providing Maven-like Gradle releases.
The preceding build.gradle script includes the dependencies for the gradle-release plugin, and also activates this within the apply plugin section of the script. You can begin an interactive release by issuing the gradle release command. This will trigger the following default series of events:
The plugin checks for any uncommitted files (added, modified, removed, or unversioned).
Checks for any incoming or outgoing changes.
Removes the SNAPSHOT flag on your project’s version (if used).
Prompts you for the release version.
Checks if your project is using any SNAPSHOT dependencies.
Will build your project.
Commits the project if SNAPSHOT was being used.
Creates a release tag with the current version.
Prompts you for the next version.
Commits the project with the new version.
You can also release artifacts not using the interact process (which will be essential when you begin releasing from a continuous integration build server), by executing the release command with the release.useAutomaticVersion flag set to true, with the essential arguments passed as additional command-line flags. Full details, including all of the command-line options, can be found within the project’s documentation. Here is the command:
$ gradle release -Prelease.useAutomaticVersion=true -Prelease.releaseVersion=1.0.0 -Prelease.newVersion=1.1.0-SNAPSHOT
Bazel is an open source tool that allows for the automation of building and testing of software. Google uses the build tool Blaze internally and released an open source part of the Blaze tool as Bazel, named as an anagram of Blaze. The Bazel extension language allows it to work with source files written in any language, with native support for Java, C, C++, and Python. Bazel produces builds and runs tests for multiple platforms. Bazel’s BUILD files describe how Bazel should build your project. They have a declarative structure and use a language similar to Python. BUILD files allow you to work at a high level of the system by listing rules and their attributes.
The complexity of the build process is handled by these preexisting rules. You can modify rules to tweak the build process, or write new rules to extend Bazel to work with any language or platform. Hermetic rules and sandboxing allow Bazel to produce correct, reproducible artifacts and test results. Caching allows reuse of build artifacts and test results. Bazel’s builds are fast. Incremental builds allow Bazel to do the minimum required work for a rebuild or retest. Correct and reproducible builds allow Bazel to reuse cached artifacts for whatever is not changed. If you change a library, Bazel will not rebuild your entire source.
Build systems most similar to Bazel are Pants and Buck. Pants’ development and feature set were informed by the needs and processes of many prominent software engineering organizations, including those at Twitter, Foursquare, Square, Medium, and others. But it can also be used in smaller projects. Pants supports Java, Scala, Python, C/C++, Go, Thrift, Protobuf, and Android code. Support for other languages, frameworks, and code generators can be added by third-party developers by authoring plugins through a well-defined module interface.
Buck is a build system developed and used by Facebook. It encourages the creation of small, reusable modules consisting of code and resources, and supports a variety of languages on many platforms. Buck is designed for building multiple deliverables from a single repository (a monorepo) rather than across multiple repositories. It has been Facebook’s experience that maintaining dependencies in the same repository makes it easier to ensure that all developers have the correct version of all the code, and simplifies the process of making atomic commits.
As these tools are not as popular as Ant, Maven, and Gradle, and because of the limited scope of this book, a full installation and release guide will not be included here. However, all of these details can be found on the respective project websites. An example Bazel BUILD file can be seen in Example 5-5, and you can see that the structure is not dissimilar to the Gradle build script that you examined earlier.
package(default_visibility=["//visibility:public"])java_binary(name="hello-world",main_class="com.example.myproject.Greeter",runtime_deps=[":hello-lib"],)java_library(name="hello-lib",srcs=glob(["*.java"],exclude=["HelloErrorProne.java"],),)java_binary(name="hello-resources",main_class="com.example.myproject.Greeter",runtime_deps=[":custom-greeting"],)java_library(name="custom-greeting",srcs=["Greeter.java"],resources=["//examples/java-native/src/main/resources:greeting"],)java_library(name="hello-error-prone",srcs=["HelloErrorProne.java"],)filegroup(name="srcs",srcs=["BUILD"]+glob(["**/*.java"]),)
These new style of build tools can be useful if you are working with a large monorepo-based codebase, and we will explore this in more detail in Chapter 9.
Many more open source JVM-based build tools could have been mentioned in this book, although we have tried to focus on the tools that are popular or provide novel functionality. This book focuses primarily on Java within the JVM space, but it is also worth mentioning that if you are working with Scala or Clojure, the Simple Build Tool (SBT) or Leiningen build tools are worth considering. Even if you are not working with other languages, these tools can still be useful when working with other projects. You will see the use of SBT later in the book when you will learn about load testing with the Gatling tool. Before wrapping up our tour of Java build tools, let’s look quickly at one final classic build tool: Make.
GNU Make is a tool that controls the generation of executables and other nonsource files of a program from the program’s source files. As shown in Example 5-6, Make gets its knowledge of how to build your program from a file called the makefile, which lists each of the nonsource files and how to compute it from other files. When you write a program, you should write a makefile for it, so that it is possible to use Make to build and install the program.
When you run Make, you can specify particular targets to update; otherwise, Make updates the first target listed in the makefile. Of course, any other target files needed as input for generating these targets must be updated first. Make uses the makefile to figure out which target files ought to be brought up-to-date, and then determines which of them actually need to be updated. If a target file is newer than all of its dependencies, it is already up-to-date, and it does not need to be regenerated. The other target files do need to be updated, but in the right order: each target file must be regenerated before it is used in regenerating other targets.
JFLAGS = -g JC = javac .SUFFIXES: .java .class .java.class: $(JC) $(JFLAGS) $*.java CLASSES = \ Foo.java \ Blah.java \ Library.java \ Main.java default: classes classes: $(CLASSES:.java=.class) clean: $(RM) *.class
Make may appear verbose to many Java developers, but it is worth learning about for simple build environments that are resource constrained and cannot run Maven of Gradle, or for building multilanguage projects.
Maven has long been the default Java build tool, primarily because of its standardized build process and structure. If you know how to build one Maven project, you know how to build them all. However, Gradle has seen an increase in popularity in the past several years, most likely because of the concise nature of the build.gradle build script. If you have ever had to wade through a large Maven project, you will remember only too well the challenges of navigating large XML files. Choosing a build tool is often an important first step in a new Java project, and this can be a life-long commitment, as migrating from one build tool to another is not a pleasant experience. So, which tool should you choose?
A good choice if your organization has heavy investment in this tool, or you are migrating/upgrading a project that already uses this tool.
You are in complete control of the project directory structure, build tooling, and build life cycle.
Not a good choice if your organization likes things to be standardized, as the flexibility provided by Ant means that build scripts will often diverge in layout and process.
Generally not recommended for starting a new project that uses modern frameworks like Dropwizard or Spring Boot.
A good de facto choice for building Java applications, especially if your organization already has good skills or an investment in this build tool.
The lack of flexibility and challenge of writing custom plugins mean that this tool is not appropriate for projects that require a custom build process (but do check that you really need a custom build process!).
Not recommended if you are building a simple project with many dependencies; navigating a 500+ line pom.xml can be challenging.
A good choice for building projects that require more flexibility in the life cycle or process than Maven can provide.
Great integration with the Groovy language and associated test frameworks like Spock and Geb.
The combination of the Gradle DSL and Groovy enable you to write custom and complex build logic.
Good for Spring Boot and other microservice frameworks, and has useful integration with, for example, contract-based testing tools.
The learning curve (and reliance on Groovy) could make this a bad choice if your organization works exclusively with Java.
Not a good choice if your organization likes things to be standardized, as there are many ways to write a build.gradle.
It really is worth spending some time at the beginning of a project to make sure you are using the most appropriate build tool, as this is something you will interact with every day.
You have covered a lot of ground on how Java applications are built within this chapter, and explored the benefits of automating the build process. Your new knowledge of the strengths and weaknesses of popular Java build tools will be useful when starting your next project, whether this is a large monorepo-based project or a series of independently built microservice-style code repositories. In summary:
All Java applications must be built—compiled to Java byte code—before they can be executed on the JVM.
Java applications of sufficient complexity will require the inclusion of additional external dependencies or libraries; these must be managed effectively.
You should package client-side JavaScript libraries as WebJars.
Although there is value in manually exploring build steps in order to learn more about this process, there is little to be gained from continually doing this.
Build tooling automates the compilation, dependency management, testing, and packaging of software applications.
It is generally advantageous to utilize a specialized build tool, such as Maven or Gradle, as this allows all developers working on a project to successfully build the application, regardless of the operating system, IDE, or framework they are using.
Build tooling can often detect and warn if you are using out-of-date (or insecure) build dependencies, but you are responsible for making sure this functionality is enabled and that you take action on any warnings.
Application code can be structured within a single version-controlled monorepo, or multiple independent repositories. The choice of structure will affect the build process and determine which build tool to use.
Semantic versioning, or semver, is a simple set of rules and requirements that dictate how version numbers are assigned and incremented. This is useful for releasing and managing your own dependencies, and is essential for avoiding “dependency hell.”
Choosing a build tool is often an important first step in a new Java project, and it can be a life-long commitment, because migrating from one build tool to another is generally not a pleasant experience.
Before learning about packaging Java applications for deployment, you will explore several additional build tools and associated skills that can be useful in the next chapter.