Comprehensive Overview of MapStruct with Spring Boot and Lombok
Written on
Background
MapStruct is a Java library designed for generating code that maps various Java objects, such as DTOs and entities, utilizing annotations. The author has implemented MapStruct in a Spring Boot application, integrating it with Vavr and Lombok, while managing dependencies through Maven.
Setup
For Maven projects, the necessary configuration should be included in the pom.xml file:
<dependencyManagement>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
</dependency>
...
</dependencyManagement>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.10.1</version>
<configuration>
...
<annotationProcessorPaths combine.children="append">
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>${lombok-mapstruct-bindings.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
Refer to the MapStruct documentation for available versions, and find Lombok-mapstruct-binding versions as needed.
Mapper
Since this example utilizes Spring Boot, we will employ the annotation that enables the mapper to be created as a bean:
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public interface MyMapper {
}
Simple Example
Assuming we have a DTO and an entity where all properties share the same name and type, creating a mapper is straightforward:
@Builder
public class Car {
String color;
int amountOfSeats;
int maxSpeed;
}
@Builder
public class CarEntity {
String color;
int amountOfSeats;
int maxSpeed;
}
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public interface MyMapper {
CarEntity toEntity(Car car);
Car fromEntity(CarEntity entity);
}
MapStruct will utilize the builders generated by Lombok to create the objects.
Handling Different Field Names
In cases where the DTO and entity contain fields of the same type but with different names, you can use @Mapping(source = "sourceField", target = "targetField") to specify the mapping:
@Builder
public class Car {
String color;
int amountOfSeats;
int maxSpeed;
}
@Builder
public class CarEntity {
String color;
int numberOfSeats;
int maximumSpeed;
}
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public interface MyMapper {
@Mapping(source = "amountOfSeats", target = "numberOfSeats")
@Mapping(source = "maxSpeed", target = "maximumSpeed")
CarEntity toEntity(Car car);
@InheritInverseConfiguration
Car fromEntity(CarEntity entity);
}
Introducing Vavr Options
To enable MapStruct to map between different types, define a default method for the conversion. Ensure that the source and target names match or specify them using @Mapping.
@Builder
public class Car {
Option<String> color;
int amountOfSeats;
Option<Integer> maxSpeed;
}
@Builder
public class CarEntity {
String color;
int amountOfSeats;
int maximumSpeed;
}
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public interface MyMapper {
@Mapping(source = "maxSpeed", target = "maximumSpeed")
CarEntity toEntity(Car car);
@InheritInverseConfiguration
Car fromEntity(CarEntity entity);
default <T> T fromOption(Option<T> value) {
return value.getOrNull();}
default <T> Option<T> toOption(T value) {
return Option.of(value);}
}
If the object and its encapsulated option are of different types, a specific implementation must be provided.
Mapping Between Java and Vavr Collections
Similar to handling options, add the following methods to facilitate mapping between lists:
default <T> List<T> fromJavaList(java.util.List<T> javaList) {
return Option.of(javaList)
.map(List::ofAll)
.getOrNull();
}
default <T> java.util.List<T> toJavaList(List<T> vavrList) {
return Option.of(vavrList)
.map(List::toJavaList)
.getOrNull();
}
For sets, simply replace List with Set in the above examples.
Common Mapping Functions
If Vavr is used throughout your project, consider defining common default methods in a single MapperHelper interface to avoid repetition in every mapper. Include it using:
@Mapper(
componentModel = MappingConstants.ComponentModel.SPRING,
uses = MapperHelper.class
)
public interface MyMapper {
}
Alternatively, you can define the default methods in a simple interface and extend the helper interface in your mapper.
Non-Direct Mapping with Named Default Methods
To execute specific logic during mapping, such as converting a null string to an empty one, you can use @Named("name") with qualifiedByName:
@Builder
public class Car {
String color;
int amountOfSeats;
int maxSpeedMps;
}
@Builder
public class CarEntity {
String color;
int amountOfSeats;
int maxSpeedKph;
}
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public interface MyMapper {
@Mapping(
source = "maxSpeedMps",
target = "maxSpeedKph",
qualifiedByName = "mpsToKph"
)
CarEntity toEntity(Car car);
@Named("mpsToKph")
default int toKph(int mps) {
return mps * 3.6;}
}
This method can be applied to any input and output types.
Setting Default Values
MapStruct allows you to define default values for target fields:
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public interface MyMapper {
@Mapping(
target = "color",
defaultValue = "unspecified"
)
Car fromEntity(CarEntity car);
}
Default values can also be specified for other data types, such as long or enums.
Utilizing Generated Methods in Default Methods
Generated methods can be utilized within default methods:
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public interface MyMapper {
Car fromEntity(CarEntity car);
default List<Car> fromEntitySet(Set<CarEntity> entities) {
return entities.map(this::fromEntity).toList();}
}