1. Overview

Project Lombok is a library that helps with boilerplate code, allowing us to focus more on the core application logic.

Similarly, MapStruct is another library that helps with boilerplate when we need a mapping between two Java beans.

In this tutorial, we’ll look at effectively using these two libraries together.

2. Setup

Let’s add the mapstruct, lombok, and lombok-mapstruct-binding dependencies to our pom.xml:

<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
    <version>1.6.0.Beta2</version>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.32</version>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok-mapstruct-binding</artifactId>
    <version>0.2.0</version>
</dependency>

3. MapStruct and Lombok Integration

We’ll use the @Builder and @Data Lombok annotations in our setup**. While the former allows object creation via the Builder pattern, the latter provides constructor-based object creation via setters.**

3.1. Java POJO Setup

Now, let’s start by defining a simple source class for our mapper:

@Data
public class SimpleSource {
    private String name;
    private String description;
}

Next, we define a simple destination class for our mapper:

@Data
public class SimpleDestination {
    private String name;
    private String description;
}

Finally, we’ll also define another destination class, but using the @Builder Lombok annotation:

@Builder
@Getter
public class LombokDestination {
    private String name;
    private String description;
}

3.2. Using the @Mapper Annotation

When we use the @Mapper annotation, MapStruct automatically creates the mapper implementation.

Let’s define the mapper interface:

@Mapper
public interface LombokMapper {
    SimpleDestination sourceToDestination(SimpleSource source);
    LombokDestination sourceToLombokDestination(SimpleSource source);
}

When we execute the mvn clean install command, the mapper implementation class is created under the /target/generated-sources/annotations/ folder.

Let’s go through the generated implementation class:

public class LombokMapperImpl implements LombokMapper {

    @Override
    public SimpleDestination sourceToDestination(SimpleSource source) {
        if ( source == null ) {
            return null;
        }

        SimpleDestination simpleDestination = new SimpleDestination();

        simpleDestination.setName( source.getName() );
        simpleDestination.setDescription( source.getDescription() );

        return simpleDestination;
    }

    @Override
    public LombokDestination sourceToLombokDestination(SimpleSource source) {
        if ( source == null ) {
            return null;
        }

        LombokDestination.LombokDestinationBuilder lombokDestination = LombokDestination.builder();

        lombokDestination.name( source.getName() );
        lombokDestination.description( source.getDescription() );

        return lombokDestination.build();
    }
}

As we can see here, the implementation has two methods for mapping the source object to the different destinations. However, the major difference lies in how the destination object is built.

The implementation uses the builder() method for the LombokDestination class. On the other hand, it uses the constructor to create the SimpleDestination object and the setters to map the variables.

3.3. Test Case

Now, let’s look at a simple test case to see the mapper implementation in action:

@Test
void whenDestinationIsMapped_thenIsSuccessful() {
    SimpleSource simpleSource = new SimpleSource();
    simpleSource.setName("file");
    simpleSource.setDescription("A text file.");

    SimpleDestination simpleDestination = lombokMapper.sourceToDestination(simpleSource);
    Assertions.assertNotNull(simpleDestination);
    Assertions.assertEquals(simpleSource.getName(), simpleDestination.getName());
    Assertions.assertEquals(simpleSource.getDescription(), simpleDestination.getDescription());

    LombokDestination lombokDestination = lombokMapper.sourceToLombokDestination(simpleSource);
    Assertions.assertNotNull(lombokDestination);
    Assertions.assertEquals(simpleSource.getName(), lombokDestination.getName());
    Assertions.assertEquals(simpleSource.getDescription(), lombokDestination.getDescription());
}

As we can verify in the test case above, the mapper implementation successfully maps the source POJO to both the destination POJOs.

4. Conclusion

In this article, we looked at how using MapStruct and Lombok together can help us write less boilerplate, which enhances the readability of the code and increases the efficiency of the development process.

We looked at how to use the @Builder and the @Data Lombok annotations with MapStruct while mapping from one POJO to another.

As always, the code can be found over on GitHub.