Apache Maven is a build tool for Java and other JVM-based projects that’s in widespread use, and so people that want to use Gradle often have to migrate an existing Maven build. This guide will help with such a migration by explaining the differences and similarities between the two tools' models and providing steps that you can follow to ease the process. You will also learn how to overcome some of the obstacles that arise from the differences.

1. Understanding the differences

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.

For a more complete feature comparison, please consult the Maven vs Gradle Feature Comparison Table.

Before you start, it’s useful to have some idea of how much work a migration is likely to entail. Here’s a list of Maven features that may make the process more difficult:

  • Bills of Material (BOMs)

  • "import" and "provided" scopes

  • Optional dependencies

  • Integration tests

  • Custom configurations

  • Less common Maven plugins

  • Custom plugins

Some of these can be handled without too much trouble and we’ll be discussing potential solutions later. Solutions that are often much more straightforward and easier to be used and maintained by the developers.

Once you’ve decided to go ahead with the migration, what should you do next? The best starting point is setting up a mechanism to validate your new build.

2. Build validation

Just like the software you’re trying to build, the build itself has expected outputs and behavior. A migration introduces the potential for those to change, either in big ways or in more subtle ones. And how do you check that your software is working as expected? Via tests (we hope).

A build isn’t usually difficult to test, but you do want to make sure all the same artifacts produced by the Maven build are also created by the Gradle build. Ideally, you should test for all potential inputs to the build as well, such as system properties and environment.

If you want to learn more about testing your builds, we will be publishing a separate guide and link at some point in the future.

Once you’re able to validate the Gradle build, you can start the migration. The first step is easy as Gradle provides a feature to automatically convert a Maven build to Gradle.

3. Automated conversion

Not only will the Gradle init task allow you to create a new skeleton project, but it will also automatically convert an existing Maven one to Gradle. All you have to do is run the command

$ gradle init

from the root project directory and let Gradle do its thing. That basically consists of parsing the existing POMs and generating corresponding Gradle build files plus a settings.gradle file if it’s a multi-project build.

You’ll find that the new Gradle build includes any custom repositories specified in the POM, your external and inter-project dependencies, the appropriate plugins (any of maven, java, and war), and more. See the user guide for a complete list of the automatic conversion features.

One thing to bear in mind is that assemblies are not automatically converted. They aren’t necessarily problematic to convert, but you will need to do some manual work.

If you’re lucky and don’t have many plugins or much in the way of customisation in your Maven build, you can simply run

$ gradle build

once the migration has completed. This will run the tests and produce the required artifacts without any extra intervention on your part. Of course, a big reason to switch to Gradle is because it has a richer and more powerful model, so many of you will already have extensively customized your Maven build. So we’ll look at some common obstacles and their solutions next.

4. Potential extra steps

There are many plugins and hence many different ways to customize a Maven build and we can’t cover them all. But here are some of the more common Maven techniques and plugins that require a little extra work to migrate to Gradle.

4.1. Bills of Materials (BOMs)

Maven allows you specify a list of dependencies in a separate POM inside tags. This special type of POM (a BOM) can then be imported into other POMs so that you have consistent library names and versions across all your projects.

Gradle does not support BOMs directly, but you can make use of them in your Gradle build files by applying the nebula-dependency-recommender-plugin.

Once in place, you’ll be able to import a BOM into your build file using the following syntax:

dependencyManagement {
    imports {
        mavenBom 'io.spring.platform:platform-bom:1.0.1.RELEASE'
    }
}

dependencies {
   compile 'org.springframework.integration:spring-integration-core'
}

Note: Gradle doesn’t support the import scope in the POMs of dependencies, so you’ll have to manually import them using the above syntax.

4.2. Provided scope and optional dependencies

Gradle has a lot of flexibility in the way that dependencies are managed, particularly through configurations. However, it doesn’t have a scope named provided out of the box unless you use the war plugin. The same basic behavior can be had from the compileOnly configuration added in Gradle 2.12.

If you need optional-like behavior or you’re using an older version of Gradle than 2.12, it is very easy to model these concepts in Gradle and various members of the community have produced plugins that do just that. Two well-used ones are Netflix’s Nebula provided-base and optional-base plugins. You can apply and use them like this:

plugins {
    id 'nebula.provided-base' version '2.2.2'
    id 'nebula.optional-base' version '2.2.2'
}

dependencies {
    provided 'org.apache.commons:commons-lang3:3.3.2'
    provided group: 'log4j', name: 'log4j', version: '1.2.17'
    compile 'org.apache.commons:commons-lang3:3.3.2', optional
    compile group: 'log4j', name: 'log4j', version: '1.2.17', optional
}

They also work with the new, preferred publishing mechanism, which is something that you would have to handle yourself if you wanted to implement your own configurations.

5. Maven profiles and properties

Maven allows you parameterize builds using properties of various sorts. Some are read-only properties of the project model, others are user-defined in the POM. It even allows you to treat system properties as project properties.

Gradle has a similar system of project properties, although it differentiates between those and system properties. You can, for example, define properties in:

  • the build file

  • a gradle.properties file in the root project directory

  • a gradle.properties file in the $HOME/.gradle directory

Those aren’t the only options, so if you are interested in finding out more about how and where you can define properties, check out the user guide. Unlike with Maven, we recommend using camel case for your property names rather than dot-separated words.

One important piece of behavior you need to be aware of is what happens when the same property is defined in both the build file and one of the external properties files: the build file value takes precedence. Always. Fortunately, you can mimic the concept of profiles to provide overridable default values.

Which brings us on to Maven profiles. These are a way to enable and disable different configurations based on environment, target platform, or any other similar factor. Logically, they are nothing more than limited ‘if' statements. And since Gradle has much more powerful ways to declare conditions, it does not need  to have formal support for profiles (except in the POMs of dependencies). You can easily get the same behavior by combining conditions with secondary build files, as you’ll see next.

Let’s say you have different deployment settings depending on environment: local development (the default), a test environment, and production. To add profile-like behavior, first create build files for each environment in the project root: profile-default.gradle, profile-test.gradle, and profile-prod.gradle. Next, add a condition similar to the following to the main build file:

if (!hasProperty('buildProfile')) ext.buildProfile = 'default'
apply from: "profile-${buildProfile}.gradle"

All you have to do then is put the environment-specific configuration, such as project properties, dependencies, etc., in the corresponding build file. To activate a particular profile, you can just pass in the relevant project property on the command line:

$ gradle -PbuildProfile=test build

Or you can set the project property another way. It’s up to you. And those conditions don’t just have to check project properties. You could check environment variables, the JDK version, the OS the build is running on, and anything else you can imagine.

One thing to bear in mind is that high level ‘if' statements make builds harder to understand and maintain, similar to the way they complicate Object-Oriented code. The same applies to profiles. Gradle offers you many better ways to avoid the extensive use of profiles that Maven often requires, for example by offering variants.

For a lengthier discussion on working with Maven profiles in Gradle, look no further than this article by Benjamin Muschko.

6. Resource filtering

Maven has a phase called process-resources that has the goal resources:resources bound to it by default. This gives the build author an opportunity to perform variable substitution on various files, such as web resources, packaged properties files, etc.

The Java plugin for Gradle provides a processResources task to do the same thing. Here’s an example configuration:

processResources {
    expand(version: version, buildNumber: currentBuildNumber)
}

So the left hand side of each colon is the token name and the right hand side is a project property. This variable substitution will apply to all your resource files (the ones under src/main/resources usually).

Gradle has other powerful ways for property processing. You can hook in your own filter via a closure that allows you to process the content line by line, or you can add your own FilterReader implementation. For more details, see the documentation for the ContentFilterable interface which all copy (and archive) tasks, including processResources, implement.

7. Integration tests

Although unit tests are very useful, they can’t ensure that an application or library works as a whole. It’s easy for bugs to appear in the interactions between objects and their interactions with the environment. That’s why many projects incorporate some form of higher level testing, sometimes termed integration, functional or acceptance testing.

Maven supports these types of test by providing an extra set of phases: pre-integration-test, integration-test, post-integration-test, and verify. It also uses the Failsafe plugin rather than Surefire so that failed integration tests don’t automatically fail the build (because you may need to clean up resources, such as a running application server).

Another factor to consider is where you keep your integration test classes. The default approach is to mix them with your unit test classes, but this is less than ideal. A common alternative is to use profiles so that you can keep the two types of test separate.

So how should you approach migrating such a setup to Gradle? Forget plugins: source sets are your friends in this situation. A standard Java project already has two source sets for your main classes and your unit tests. Why not add an extra one for integration tests? Or even more than one for different types of integration test? Say low-level tests against a live database and higher level tests with something like FitNesse.

By declaring a new source set, Gradle automatically sets you up with corresponding configurations ([sourceSet]Compile and [sourceSet]Runtime) as well as compilation tasks (compile[SourceSet][Lang]) and a resource processing task (process[SourceSet]Resources). All you need to do is add a task to run the tests and ensure that the classpaths are all set up. You might also want to add tasks for starting/stopping a database or application server if your tests require something like that.

Let’s now take a look at an example so you can see what’s involved in practice:

sourceSets {
    integTest {
        compileClasspath += main.output + test.output
        runtimeClasspath += main.output + test.output
    }
}
configurations {
    integTestCompile.extendsFrom testCompile
    integTestRuntime.extendsFrom testRuntime
}

In the above example, I create a new source set called integTest. I also make sure that the application or library classes, as well as their dependencies, are included on the classpath when compiling the integration tests.

Your integration tests will probably use some third party libraries of their own, so you’ll want to add those the compilation classpath too. That’s done in the normal way in the dependencies block:

dependencies {
   // ...
   integTestCompile 'org.codehaus.groovy:groovy-all:2.4.3'
   integTestCompile 'org.spockframework:spock-core:0.7-groovy-2.0', {
      exclude module: 'groovy-all'
   }
}

The integration tests will now compile, but there is currently no way to run them. That’s where the custom Test task comes in:

task integTest(type: Test) {
    dependsOn startApp
    finalizedBy stopApp
    testClassesDir = sourceSets.integTest.output.classesDir
    classpath = sourceSets.integTest.runtimeClasspath
    mustRunAfter test
}

In the above example, I’m assuming that the integration tests run against an application server that needs to be started and shut down at the appropriate times. You can learn more about how and what to configure on the Test task in Gradle’s DSL Reference.

All that’s left to do at this point is incorporate the integTest task into your task graph, for example by having build depend on it. It’s really up to you how you fit it into the build. If you want to support other test types, just rinse and repeat.

8. Common plugins

Maven and Gradle share a common approach of extending the build through plugins. Although the plugin systems are very different beneath the surface, they share many feature-based plugins, such as:

  • Shade/Shadow

  • Jetty

  • Checkstyle

  • JaCoCo

  • AntRun (see further down)

Why does this matter? Because many plugins rely on standard Java conventions, so migration is just a matter of replicating the configuration of the Maven plugin in Gradle. As an example, here’s a simple Maven Checkstyle plugin configuration:

...
<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-checkstyle-plugin</artifactId>
  <version>2.17</version>
  <executions>
    <execution>
      <id>validate</id>
      <phase>validate</phase>
      <configuration>
        <configLocation>checkstyle.xml</configLocation>
        <encoding>UTF-8</encoding>
        <consoleOutput>true</consoleOutput>
        <failsOnError>true</failsOnError>
        <linkXRef>false</linkXRef>
      </configuration>
      <goals>
        <goal>check</goal>
      </goals>
    </execution>
  </executions>
</plugin>
...

Everything outside of the configuration block can safely be ignored when migrating to Gradle. In this case, the corresponding Gradle configuration looks like the following:

checkstyle {
    config = resources.text.fromFile('checkstyle.xml', 'UTF-8')
    showViolations = true
    ignoreFailures = false
}

The Checkstyle tasks are automatically added as dependencies of the check task, which also includes test. If you want to ensure that Checkstyle runs before the tests, then just specify an ordering with the mustRunAfter() method:

test.mustRunAfter checkstyleMain, checkstyleTest

As you can see, the Gradle configuration is often much shorter than the Maven equivalent. You also have a much more flexible execution model since we are not longer constrained by Maven’s fixed phases.

While migrating a project from Maven, don’t forget about source sets. These often provide a more elegant solution for handling integration tests or generated sources than Maven can provide, so you should factor them into your migration plans.

8.1. Ant goals

Many Maven builds rely on the AntRun plugin to customize the build without the overhead of implementing a custom Maven plugin. Gradle has no equivalent plugin because Ant is a first-class citizen in Gradle builds, via the ant object. For example, you can use Ant’s Echo task like this:

task sayHello {
    doLast {
        ant.echo message: 'Hello!'
    }
}

Even Ant properties and filesets are supported natively. To learn more, check out the Ant chapter of the user guide.

9. Plugins you don’t need

It’s worth remembering that Gradle builds are typically easier to extend and customize than Maven. In this context, that means you may not need a Gradle plugin to replace a Maven one. For example, the Maven Enforcer plugin allows you to control dependency versions and environmental factors, but these things can easily be configured in a normal Gradle build script.

10. Uncommon and custom plugins

You may come across Maven plugins that have no counterpart in Gradle, particularly if you or someone in your organisation has written a custom plugin. Such cases rely on you understanding how Gradle (and potentially Maven) works, because you will usually have to write your own plugin.

For the purposes of migration, there are two key types of Maven plugin:

  • Those that use the Maven project object.

  • Those that don’t.

Why is this important? Because if you use one of the latter, you can trivially reimplement it as a Gradle task. Simply define task inputs and outputs to correspond to the mojo parameters and convert the execution logic into a task action.

If a plugin depends on the Maven project, then you will have to rewrite it. Don’t start by considering how the Maven plugin works, but look at what problem it is trying to solve. Then try to work out how to solve that problem in Gradle. You’ll probably find that the two build models are different enough that "transcribing" Maven plugin code into a Gradle plugin just won’t be effective. On the plus side, the plugin is likely to be much easier to write than the original Maven one because Gradle has a much richer build model.

If you do need to implement custom logic, either via build files or plugins, then be sure to familiarize yourself with Gradle’s DSL Reference, which provides comprehensive documentation on the API that you’ll be working with. It details the standard configuration blocks (and the objects that back them), the core types in the system (Project, Task, etc.), and the standard set of tasks. The main entry point is the Project interface as that’s the top-level object that backs the build files.

11. Conclusion

At this point, you should have a rough idea of how much work a migration is likely to be. Those that fit the standard Maven pattern without many extra plugins should be very straightforward to migrate. Builds that utilize common plugins involve a bit more work, but mostly consist of working out how to configure the Gradle equivalents.

More complex builds will require an in-depth understanding of Gradle before the migration takes place. If that’s the case, though, remember that we believe the resulting Gradle build will make more sense and be easier to maintain than the Maven one, and grow better with your changing requirements. In other words, the investment is worth it.