MapStruct makes your life easier

Michał Nowak

The MapStruct library is a great tool that can help you save time and make your life easier. Many times, when working in a microservices environment, we need different objects depending on the layers. For example, when we want to detach our DTO objects from database objects or when we want to create new objects based on previous objects to call another microservice.

Predictions for the project

I will create a simple SpringBoot project to see how MapStruct works. Thanks to this project, you can read and add information about the car with different measurement values.(Depends on which endpoint will be called) The goal is to map between 3 types of objects:

  • CarInfoEU (European measurement system)
  • CarInfoUS (USA measurement system)
  • CarInfo (Domain object saved in the database)

Here is a sequence diagram of the save and get CarInfoUS object:

Save

Get

All cases from this article are available in the project: https://github.com/michaltomasznowak/mapstruct

MapStruct dependencies

In our project, we will use Maven Structure, but of course, you can use other project management systems (MapStruct library in version 1.4.1.Final, last version on December 20, 2021).

  <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct</artifactId>
        <version>1.4.1.Final</version>
    </dependency>

Because the MapStruct library builds our classes automatically during compilation, we need another pom configuration:

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.8.1</version>
            <configuration>
                <source>11</source> 
                <target>11</target> 
                <annotationProcessorPaths>
                    <path>
                        <groupId>org.mapstruct</groupId>
                        <artifactId>mapstruct-processor</artifactId>
                        <version>1.4.1.Final</version>
                    </path>
                    <!-- other annotation processors -->
                </annotationProcessorPaths>
            </configuration>
        </plugin>
    </plugins>
</build>

Let’s see our whole pom file:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.1</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.jlabs</groupId>
    <artifactId>mapstruct</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>mapstruct</name>
    <description>mapstruct project</description>
    <properties>
        <java.version>11</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web-services</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct</artifactId>
            <version>1.4.1.Final</version>
        </dependency>
        <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>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>11</source>
                    <target>11</target>
                    <annotationProcessorPaths>
                        <path>
                            <groupId>org.mapstruct</groupId>
                            <artifactId>mapstruct-processor</artifactId>
                            <version>1.4.1.Final</version>
                        </path>
                        <!-- other annotation processors -->
                    </annotationProcessorPaths>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

Simple mapping by MapStruct

In this step, we will try to map the object CarInfoEU to CarInfo. Let’s see the structure of those objects:

public class CarInfo {

    private int temp = 0;
    private double speed = 0;
    private int tank = 0;
}
public class CarInfoEU {

    private int insideCelsiusTemperature = 0;
    private double kilometersPerHour = 0;
    private int literTank = 0;
}

To easily map objects from CarInfoEU to CarInfo, we need to create an interface with the @Mapper annotation and componentModel="spring" to make a spring bean. Thanks to that, we can inject the CarInfoMapper object.

@Mapper(componentModel="spring")
public interface CarInfoMapper {

}

Now let’s add a method to map CarInfoEU to CarInfo.

@Mapper
public interface CarInfoMapper {

    @Mapping(source = "insideCelsiusTemperature", target = "temp")
    @Mapping(source = "kilometersPerHour", target = "speed")
    @Mapping(source = "literTank", target = "tank")
    CarInfo mapToCarInfo(CarInfoEU carInfoEU);
}

In this case, we only need to rewrite values so we can provide everything by annotation. After compiling, class CarInfoMapperImpl.class will be automatically generated.

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2021-12-20T18:20:34+0100",
    comments = "version: 1.4.1.Final, compiler: javac, environment: Java 15.0.2 (Oracle Corporation)"
)
public class CarInfoMapperImpl implements CarInfoMapper {

    @Override
    public CarInfo mapToCarInfo(CarInfoEU carInfoEU) {
        if ( carInfoEU == null ) {
            return null;
        }

        CarInfo carInfo = new CarInfo();

        carInfo.setTemp( carInfoEU.getInsideCelsiusTemperature() );
        carInfo.setSpeed( carInfoEU.getKilometersPerHour() );
        carInfo.setTank( carInfoEU.getLiterTank() );

        return carInfo;
    }
}

Complex mapping by MapStruct

Let’s consider a more complex case now. We want to save CarInfo with EU measurements, but we post CarInfoUS with USA measurements. Except mapping values, we want to make some additional operations to transfer Fahrenhait to Cesius miles to kilometers and gallons to liters. Objects to map:

public class CarInfo {

    private int temp = 0;
    private double speed = 0;
    private int tank = 0;
}
public class CarInfoUS {

    private double insideFahrenheitTemperature = 0;
    private double milesPerHour = 0;
    private double gallonTank = 0;

}

To map this objects, we will use this code:

@Mapper(componentModel="spring")
public interface CarInfoMapper {

    @Mapping(source = "insideCelsiusTemperature", target = "temp")
    @Mapping(source = "kilometersPerHour", target = "speed")
    @Mapping(source = "literTank", target = "tank")
    CarInfo mapToCarInfo(CarInfoEU carInfoEU);

    @Mapping(source = "insideFahrenheitTemperature", target = "temp", qualifiedByName = "temperatureCelsius")
    @Mapping(source = "milesPerHour", target = "speed", qualifiedByName = "miles")
    @Mapping(source = "gallonTank", target = "tank", qualifiedByName = "literTank")
    CarInfo mapToCarInfo(CarInfoUS carInfoUS);

    @Named("temperatureCelsius")
    default double fahrenheitToCelsius(double temperature) {
        return ((5 * (temperature - 32)) / 9);
    }

    @Named("miles")
    default double milesToKilometers(double miles) {
        return miles * 1.6;
    }

    @Named("literTank")
    default double gallonsToLiter(double gallons) {
        return gallons * 3.7854;
    }
}

As you can see, this code is really similar to the previous. The new thing here is the qualifiedByName property that points to the method used to map one value to another with an operation. After compilation, this code is generated:

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2021-12-20T19:16:14+0100",
    comments = "version: 1.4.1.Final, compiler: javac, environment: Java 15.0.2 (Oracle Corporation)"
)
public class CarInfoMapperImpl implements CarInfoMapper {

    @Override
    public CarInfo mapToCarInfo(CarInfoEU carInfoEU) {
        if ( carInfoEU == null ) {
            return null;
        }

        CarInfo carInfo = new CarInfo();

        carInfo.setTemp(carInfoEU.getInsideCelsiusTemperature());
        carInfo.setSpeed(carInfoEU.getKilometersPerHour());
        carInfo.setTank(carInfoEU.getLiterTank());

        return carInfo;
    }

    @Override
    public CarInfo mapToCarInfo(CarInfoUS carInfoUS) {
        if ( carInfoUS == null ) {
            return null;
        }

        CarInfo carInfo = new CarInfo();

        carInfo.
          setTemp(fahrenheitToCelsius(
            carInfoUS.getInsideFahrenheitTemperatur() ));
        carInfo.setSpeed(
          milesToKilometers( carInfoUS.getMilesPerHour()));
        carInfo.
          setTank(gallonsToLiter( carInfoUS.getGallonTank()));

        return carInfo;
    }

Before setting values in the new object, we call the default methods from the interface in this code: fahrenheitToCelsiusmilesToKilometersgallonsToLiter

Compilation error

During compilation, the compiler will easily detect any errors you made and will point out to you where you made a mistake. Here’s what happens when you make a mistake in the mapper:

Useful mapstruct annotations

You can easily control your mapper by annotation. Here is the list of the most useful method annotations:

  • @AfterMapping
  • @BeforeMapping
  • @InheritConfiguration
  • @InheritInverseConfiguration

@AfterMapping

When we add to our Mapper method with the annotation @AfterMapping, then this method will be called after the main mapping. We can perform some additional operations on the mapped object. In this example, after mapping, we set the ID on the object:

   @AfterMapping
    default void setId(@MappingTarget CarInfo carInfo) {
        String id = UUID.randomUUID().toString();
        carInfo.setId(id);
    }

We point to the mapped object with the annotation @MappingTarget.

@BeforeMapping

This annotation works similar to @AfterMapping, but the operation for the method with this annotation will be called before all mapping operations. For example, we can provide some operations on objects to map. Here is an example:

  @BeforeMapping
    default void checkSpeedEU(CarInfoEU carInfoEU) {
        if (carInfoEU.getKilometersPerHour() > 100) {
            System.out.println("Too fast");
        }
    }

@InheritConfiguration

If we want to add another mapper with, for example, fewer values, we can inherit from the previous mapper only the values that we need and overwrite the new values. Here is an example where we will add a new mapper and ignore the literTank field.

    @Mapping(source = "insideCelsiusTemperature", target = "temp")
    @Mapping(source = "kilometersPerHour", target = "speed")
    @Mapping(source = "literTank", target = "tank")
    CarInfo mapToCarInfo(CarInfoEU carInfoEU);

    @InheritConfiguration
    @Mapping(source = "literTank", target = "tank", ignore = true)
    CarInfo mapToCarInfoWithoutTank(CarInfoEU carInfoEU);

In this case, all fields on the map will be inherited from the mapToCarInfo method, except the literTank field.

@InheritInverseConfiguration

Sometimes when we have an easy flow of mapping (we map the same values on two sites), we can use @InheritInverseConfiguration. Thanks to that, we get the reverse operation of mapping:

    @Mapping(source = "temp", target = "insideCelsiusTemperature")
    @Mapping(source = "speed", target = "kilometersPerHour")
    @Mapping(source = "tank", target = "literTank")
    CarInfoEU mapToCarInfoEU(CarInfo carInfo);

    @InheritInverseConfiguration
    CarInfo mapToCarInfo(CarInfoEU carInfoEU);

In this case, we have 2 mappings: from CarInfo to CarInfoEU and from CarInfoEU to CarInfo. I am not a big fan of this method. I prefer to have everything in a separate class and the mapping of CarInfo should be in the CarInfoMapper class and the operation to map to CarInfoEU should be in CarInfoEUMapper.

Summary

As you can see, with the MapStruct library, you can save a lot of time, according to the DRY principle (don’t repeat yourself), but you have to remember to find a balance between great tools, that produce mapstruct provides and clean, readable code. Sometimes it’s better to repeat something instead of putting everything in one place at all costs. 🙂

The main advantages of using MapStruct are:

  • Your code is cleaner and much easier to read and manage
  • You avoid writing a lot of repeating code
  • Thanks to the fact that MapStruct generates classes during compilation, there is no performance issue
  • Easy to add to a project
  • Easy to find bugs during compilation.

References

Meet the geek-tastic people, and allow us to amaze you with what it's like to work with j‑labs!

Contact us