Expressive Build Language and Deep API
The key to unlocking Gradle's power features within your build script lies in discovering and applying its domain model, as shown in Figure 4.
Figure 4: Build scripts apply the Gradle DSL and have access to its deep API.
As you can see in the figure, a build script directly maps to an instance of type
Project in Gradle's API. In turn, the dependencies configuration block in the build script invokes the method
dependencies() of the project instance. Like most APIs in the Java world, it's available as HTML Javadoc documentation on Gradle's website. Who would have known? You're actually dealing with code. Without knowing it, you generate an object representation of your
build logic in memory.
Each element in a Gradle script has a one-to-one representation with a Java class; however, some of the elements have been sugar-coated with a sprinkle of Groovy syntax. Having a "Groovy-fied" version of a class in many cases makes the code more compact than its Java counterpart and allows for using new language features such as closures.
Gradle can't know all the requirements specific to your enterprise build. By exposing hooks into lifecycle phases, Gradle allows for monitoring and configuring the build script's execution behavior. Let's assume you have the very unique requirement of sending out an email to the development team whenever a unit test failure occurs. The way you want to send an email (for example, via SMTP or a third-party email service provider) and the list of recipients are very specific to your build. Other builds using Gradle may not be interested in this feature at all. By writing a custom test listener that's notified after the test execution lifecycle event, you can easily incorporate this feature for your build.
Gradle establishes a vocabulary for its model by exposing a DSL implemented in Groovy. When dealing with a complex problem domain, in this case the task of building software, being able to use a common language to express your logic can be a powerful tool. Let's look at some examples. Most common to builds is the notation of a unit of work that you want to get executed. Gradle describes this unit of work as a task. Part of Gradle's standard DSL is the ability to define tasks very specific to compiling and packaging Java source code. It's a language for building Java projects with its own vocabulary that doesn't need to be relevant to other contexts.
Another example is the way you can express dependencies to external libraries, a very common problem solved by build tools. Out-of-the-box Gradle provides you with two configuration blocks for your build script that allow you to define the dependencies and repositories that you want to retrieve them from. If the standard DSL elements don't fit your needs, you can even introduce your own vocabulary through Gradle's extension mechanism.
This may sound a little nebulous at first, but once you're past the initial hurdle of learning the build language, creating maintainable and declarative builds comes easy. A good place to start is the Gradle Build Language Reference Guide. Gradle's DSL can be extended. You may want to change the behavior of an existing task or add your own idioms for describing your business domain. Gradle offers you plenty of options to do so.
Gradle is Groovy
Prominent build tools like Ant and Maven define their build logic through XML. As we all know, XML is easy to read and write, but can become a maintenance nightmare if used in large quantities. XML isn't very expressive. It makes it hard to define complex custom logic. Gradle takes a different approach. Under the hood, Gradle's DSL is written with Groovy providing syntactic sugar on top of Java. The result is a readable and expressive build language. All your scripts are written in Groovy as well. Being able to use a programming language to express your build needs is a major plus. You don't need to be a Groovy expert to get started. Because Groovy is written on top of Java, you can migrate gradually by trying out its language features. You could even write your custom logic in plain Java Gradle couldn't care less. Groovy veterans will assure you that using Groovy instead of Java will boost your productivity significantly. A great reference guide is the book Groovy in Action, Second Edition by Dirk Koenig et al. (Manning, 2009).
One of Gradle's big ideas is to give you guidelines and sensible defaults for your projects. Every Java project in Gradle knows exactly where source and test class file are supposed to live, and how to compile your code, run unit tests, generate Javadoc reports, and create a distribution of your code. All of these tasks are fully integrated into the build lifecycle. If you stick to the convention, there's only minimal configuration effort on your part. In fact, your build script is a one-liner. Seriously! Figure 5 illustrates how Gradle introduces conventions and lifecycle tasks for Java projects.
Figure 5: In Gradle, Java projects are build by convention with sensible defaults. Changing the defaults is easy and achieved through convention properties.
Default tasks are provided that make sense in the context of a Java project. For example, you can compile your Java production source code, run tests, and assemble a JAR file. Every Java project starts with a standard directory layout. It defines where to find production source code, resource files, and test code. Convention properties are used to change the defaults.
The same concept applies to other project archetypes like Scala, Groovy, web projects, and many more. Gradle calls this concept build by convention. The build script developer doesn't need to know how this works under the hood. Instead, you can concentrate on what needs to be configured. Gradle's conventions are similar those in Maven, but they don't leave you feeling boxed in. Maven is very opinionated; it proposes that a project only contains one Java source directory and only produces one single JAR file. This is not necessarily reality for many enterprise projects. Gradle allows you easily to break out of the conventions. On the opposite end of the spectrum, Ant does not give you a lot of guidance on how to structure your build script, allowing for a maximum level of flexibility. Gradle takes the middle ground by offering conventions combined with the ability to change them easily. Szczepan Faber, one of Gradle's core engineers, put it this way on his blog: "Gradle is an opinionated framework on top of an unopinionated toolkit."
Robust and Powerful Dependency Management
Software projects are usually not self-contained. All too often, your application code uses a third-party library providing existing functionality to solve a specific problem. Why would you want to reinvent the wheel by implementing a persistence framework if Hibernate already exists? Within an organization, you may be the consumer of a component or module implemented by a different team. External dependencies are accessible through repositories, and the type of repository is highly dependent on what your company prefers. Options range from a plain file system to a full-fledged enterprise repository. External dependencies may have a reference to other libraries or resources. We call these transitive dependencies.
Gradle provides an infrastructure to manage the complexity of resolving, retrieving, and storing dependencies. Once they're
downloaded and put in your local cache, they're made available to your project. A key requirement of enterprise builds is
reproducibility. Do you remember the last time a coworker said, "But it works on my box"? Builds have to produce the same result on different
machines, independent of the contents of your local cache. Dependency managers like Ivy and Maven in their current implementation
cannot fully guarantee reproducibility. Why is that? Whenever a dependency is downloaded and stored in the local cache, it doesn't take into account the artifact's origin. In situations where the repository is changed
for a project, the cached dependency is considered resolved, even though the artifact's content may be slightly different.
At worst, this will cause a failing build that's extremely hard to debug. Another common complaint specific to Ivy is the
fact that dependency snapshot versions, artifacts currently under development with the naming convention
–SNAPSHOT, aren't updated correctly
in the local cache, even though it changed on the repository and is marked as changing. There are
many more scenarios where current solutions fall short. Gradle provides its own configurable, reliable, and efficient dependency-management solution.
Large enterprise projects usually consist of multiple modules to separate functionality. In the Gradle world, each of the submodules is considered a project that can define dependencies to external libraries or other modules. Additionally, each subproject can be run individually. Gradle figures out for you which of the subproject dependencies need to be rebuilt, without having to store a subproject's artifact in the local cache.
For some companies, a large project with hundreds of modules is reality. Building and testing minor code changes can consume
a lot of time. You may know from personal experience that deleting old classes and resources by running a cleanup task is
a natural reflex. All too often, you get burned by your build tool not picking up the changes and their dependencies. What
you need is a tool that's smart enough to only rebuild the parts of your software that actually changed. Gradle supports incremental
builds by specifying task inputs and outputs. It reliably figures out for you which tasks need to be skipped, built, or partially
rebuilt. The same concept translates to multimodule projects, called partial builds. Because your build clearly defines the
dependencies between submodules, Gradle takes care of rebuilding only the necessary parts. No more running
clean by default!
Automated unit, integration, and functional tests are part of the build process. It makes sense to separate short-running types of tests from the ones that require setting up resources or external dependencies to be run. Gradle supports parallel test execution. This feature is fully configurable and ensures that you're actually taking advantage of your processor's cores. The buck doesn't stop here. Gradle is going to support distributing test execution to multiple machines in a future version. The days of reading your Twitter feed between long builds are gone.
Developers run builds many times during development. That means starting a new Gradle process each time, loading all its internal dependencies, and running the build logic. You'll notice that it usually takes a couple of seconds before your script actually starts to execute. To improve the startup performance, Gradle can be run in daemon mode. In practice, the Gradle command forks a daemon process, which not only executes your build, but also keeps running in the background. Subsequent build invocations will piggyback on the existing daemon process to avoid the startup costs. As a result, you'll notice a far snappier initial build execution.