Using MapStruct with Gradle

Update Feb. 26, 2017: Since writing this post, usage of annotation processors with Gradle got much easier and the set-up described in the following isn’t required anymore. The example project on GitHub has been updated accordingly.

You work with Gradle to build your application and would like to make use of MapStruct to generate mappings between different representations of your model? Then read on to learn how to make MapStruct work with the Groovy based build tool.

Background

MapStruct is implemented in form of an annotation processor as specified by JSR 269. Annotation processors are plugged into the Java compiler and can inspect the sources during compilation as well as create new sources as it is done by MapStruct. JSR 269 processors can be integrated into basically any form of Java build as long as you work with Java 6 or later.

One way of using an annotation processor is to put its JAR onto the compilation classpath where it will be picked up automatically by the Java compiler. This approach works, but it has the advantage that it exposes the processor and its classes to the compiled application which thus – accidentially or not – could import types from the processor.

This sort of issue can be avoided by setting up the processor separately. When working with javac directly, the processorpath option can be used for this purpose, while for Maven projects the maven-annotation-plugin is the recommended way to integrate annotation processors.

Set up MapStruct in your Gradle build

To integrate MapStruct into a Gradle build, first make sure you use the Java 6 language level by adding the following to the build.gradle file of your project:

ext {
    javaLanguageLevel = '1.6'
    generatedMapperSourcesDir = "${buildDir}/generated-src/mapstruct/main"
}

sourceCompatibility = rootProject.javaLanguageLevel

It’s a good idea to declare a property which holds the language level. That way it can be referenced later on where required. We also define a property which specifies the target directory for the generated mapper classes.

The next step is to add the MapStruct annotation module (org.mapstruct:mapstruct:<VERSION>) as compilation dependency and to declare a separate dependency configuration which contains the MapStruct processor module (org.mapstruct:mapstruct-processor:<VERSION>):

configurations {
    mapstruct
}

dependencies {
    compile( 'org.mapstruct:mapstruct:<VERSION>' )
    mapstruct( 'org.mapstruct:mapstruct-processor:<VERSION>' )
}

The separate dependency configuration makes sure that the classes from the processor aren’t visible to the compiled application. To make the generated sources available for the actual compilation step add the previously configured path to the main source set like this:

sourceSets.main {
    ext.originalJavaSrcDirs = java.srcDirs
    java.srcDir "${generatedMapperSourcesDir}"
}

We also store the original source directories in a property in order to reference them later on. Now it’s time to set up a task for the invocation of the annotation processor. To do so, declare a task of the type JavaCompile like this:

task generateMainMapperClasses(type: JavaCompile) {
    ext.aptDumpDir = file( "${buildDir}/tmp/apt/mapstruct" )
    destinationDir = aptDumpDir

    classpath = compileJava.classpath + configurations.mapstruct
    source = sourceSets.main.originalJavaSrcDirs
    ext.sourceDestDir = file ( "$generatedMapperSourcesDir" )

    options.define(
        compilerArgs: [
            "-nowarn",
            "-proc:only",
            "-encoding", "UTF-8",
            "-processor", "org.mapstruct.ap.MappingProcessor",
            "-s", sourceDestDir.absolutePath,
            "-source", rootProject.javaLanguageLevel,
            "-target", rootProject.javaLanguageLevel,
        ]
    );

    inputs.dir source
    outputs.dir generatedMapperSourcesDir;
    doFirst {
         sourceDestDir.mkdirs()
    }
    doLast {
        aptDumpDir.delete()
    }
}

The task’s classpath comprises both, the actual compilation classpath as well as the mapstruct configuration set up before. As source path the previously stored source directories are used.

The options passed to the compile task should be rather self-explanatory. Note that by passing -proc:only, the task will only invoke the given processor but perform no compilation (that will be done by the default compilation step later on).

By declaring the inputs and outputs of the task we make sure Gradle’s incremental build functionality is leveraged. That way Gradle will skip the task when running the build a second time and the generated output files still are up to date.

Finally you need to make sure that the generation of mapper types happens before the compilation of all sources. This can be achieved by declaring the following dependency:

compileJava.dependsOn generateMainMapperClasses

Give it a shot

You can find the complete build.gradle file on GitHub. It is part of an example project which generates a simple mapper class and executes some tests against it. To clone the example project just execute

git clone https://github.com/mapstruct/mapstruct-examples.git

You can then build the example by running

cd mapstruct-on-gradle && ./gradlew build

The project comes with the Gradle Wrapper, a small utility which retrieves the right Gradle version upon the first build. So it is not required to install Gradle separately.

In case you have questions, ideas or any other kind of feedback just add a comment to this post or leave a message in the mapstruct-users group.

comments powered by Disqus