MapStruct makes your life easier
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: fahrenheitToCelsius
, milesToKilometers
, gallonsToLiter
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.