In this article, you'll find code samples and instructions on how to effectively use MapStruct, Lombok, and Spring Boot.
introduce
When you implement a service of any size, you often need to move data from one structure to another. Typically, this is the same data used at different logical layers - at the business logic, database level, or controller level for transfer to the front-end application.
To transfer this data, you have to repeat a lot of boilerplate. really tired. I'd like to draw your attention to a library that may help you save energy. Meet MapStructure!
With this library, you can only specify structure mapping schemes. Its implementation will be collected by the library itself.
where to find it
The latest version can be found in the Maven central repository.
You can add this to your pom.xml:
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.5.5.Final</version>
</dependency>
You need to add annotation processor in plugin:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.5.5.Final</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
If you use Gradle, you can add this to your build.gradle
:
implementation "org.mapstruct:mapstruct:${mapstructVersion}"
annotationProcessor "org.mapstruct:mapstruct-processor:${mapstructVersion}"
how it works
Let's take some data classes as an example:
public class CatEntity {
private Long id;
private String name;
private String color;
// getters and setters
}
public class CatDto {
private Long id;
private String name;
private String color;
// getters and setters
}
This is all the code we need to write for its implementation:
@Mapper
public interface CatMapper {
CatEntity toEntity(CatDto dto);
CatDto toDto(CatEntity entity);
}
Implementations of this interface will be created by MapStruct itself:
@Generated
public class CatMapperImpl implements CatMapper {
@Override
public CatEntity toEntity(CatDto dto) {
if ( dto == null ) {
return null;
}
CatEntity catEntity = new CatEntity();
catEntity.setId( dto.getId() );
catEntity.setName( dto.getName() );
catEntity.setColor( dto.getColor() );
return catEntity;
}
@Override
public CatDto toDto(CatEntity entity) {
if ( entity == null ) {
return null;
}
CatDto catDto = new CatDto();
catDto.setId( entity.getId() );
catDto.setName( entity.getName() );
catDto.setColor( entity.getColor() );
return catDto;
}
}
How to use MapStruct with Java records
In Java 14, the record class was added. MapStruct can handle them too:
public class CatEntity {
private Long id;
private String name;
private String color;
// getters and setters
}
public record CatRecord(
Long id,
String name,
String color
) {
}
If we create a mapping interface:
@Mapper
public interface CatRecordMapper {
CatEntity toEntity(CatRecord record);
CatRecord toRecord(CatEntity entity);
}
Then Mapstruct will generate the following implementation:
@Generated
public class CatRecordMapperImpl implements CatRecordMapper {
@Override
public CatEntity toEntity(CatRecord record) {
if ( record == null ) {
return null;
}
CatEntity catEntity = new CatEntity();
catEntity.setId( record.id() );
catEntity.setName( record.name() );
catEntity.setColor( record.color() );
return catEntity;
}
@Override
public CatRecord toRecord(CatEntity entity) {
if ( entity == null ) {
return null;
}
Long id = null;
String name = null;
String color = null;
id = entity.getId();
name = entity.getName();
color = entity.getColor();
CatRecord catRecord = new CatRecord( id, name, color );
return catRecord;
}
}
How to use MapStruct with Project Lombok
In the Java world, there is a well-known large library - Project Lombok. It also reduces boilerplate code that developers have to write. More details about the library you can find on the official website.
To add this library to your project, you need to add this to pom.xml:
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.28</version>
<scope>provided</scope>
</dependency>
And, you need to add this to your annotation processor:
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.28</version>
</path>
</annotationProcessorPaths>
For Gradle, it's a little easier. just add it to build.gradle
implementation 'org.projectlombok:lombok:1.18.28'
annotationProcessor "org.projectlombok:lombok:1.18.28"
And an important step has been taken! To integrate Project Lombok with MapStruct, you need to add the binding library:
annotationProcessor 'org.projectlombok:lombok-mapstruct-binding:0.2.0'
For our example, @Data
it is sufficient to use annotations, which add getters and setters.
@Data
public class CatDto {
private Long id;
private String name;
private String color;
}
@Data
public class CatEntity {
private Long id;
private String name;
private String color;
}
For the same mapper interface, it generates the following implementations:
public interface CatMapper {
CatEntity toEntity(CatDto dto);
CatDto toDto(CatEntity entity);
}
@Generated
public class CatMapperImpl implements CatMapper {
@Override
public CatEntity toEntity(CatDto dto) {
if ( dto == null ) {
return null;
}
CatEntity catEntity = new CatEntity();
catEntity.setId( dto.getId() );
catEntity.setName( dto.getName() );
catEntity.setColor( dto.getColor() );
return catEntity;
}
@Override
public CatDto toDto(CatEntity entity) {
if ( entity == null ) {
return null;
}
CatDto catDto = new CatDto();
catDto.setId( entity.getId() );
catDto.setName( entity.getName() );
catDto.setColor( entity.getColor() );
return catDto;
}
}
How to add Spring Boot logic to Mapper
Sometimes it is necessary to retrieve some fields from a Spring bean. Assuming we don't store weight information in the database, this is not accessible in Entity. Instead, we have some Spring services to provide this information.
@Data
public class CatDto {
private Long id;
private String name;
private String color;
private Integer weight;
}
@Data
public class CatEntity {
private Long id;
private String name;
private String color;
}
And there is service that provides weight information:
@Service
public class CatWeightProvider {
public Integer getWeight(String name) {
// some logic for retrieving weight info
return 5;
}
}
To use this bean to retrieve weight information within the mapper interface, it should be replaced by an abstract class with methods describing any additional logic.
@Mapper(componentModel = "spring")
public abstract class CatMapper {
@Autowired
private CatWeightProvider provider;
@Mapping(target = "weight", source = "entity.name", qualifiedByName = "retrieveWeight")
public abstract CatDto toDto(CatEntity entity);
@Named("retrieveWeight")
protected Integer retrieveWeight(String name) {
return provider.getWeight(name);
}
}
In this case MapStruct will generate an implementation of this abstract class:
@Generated
@Component
public class CatMapperImpl extends CatMapper {
@Override
public CatDto toDto(CatEntity entity) {
if ( entity == null ) {
return null;
}
CatDto catDto = new CatDto();
catDto.setWeight( retrieveWeight( entity.getName() ) );
catDto.setId( entity.getId() );
catDto.setName( entity.getName() );
catDto.setColor( entity.getColor() );
return catDto;
}
}
How to ignore fields and map fields with different names
For example, a field in a database entity and a field in a dto have different names.
For example, we need to ignore field weights in the dto layer.
@Data
public class CatDto {
private String name;
private String color;
private Integer weight;
}
@Data
public class CatEntity {
private Long idInDatabase;
private String nameInDatabase;
private String colorInDatabase;
}
This can be done with the following parameters:
@Mapper
public interface CatMapper {
@Mapping(target = "weight", ignore = true)
@Mapping(target = "name", source = "entity.nameInDatabase")
@Mapping(target = "color", source = "entity.colorInDatabase")
CatDto toDto(CatEntity entity);
}
Therefore, the implementation of MapStruct will be:
@Generated
public class CatMapperImpl implements CatMapper {
@Override
public CatDto toDto(CatEntity entity) {
if ( entity == null ) {
return null;
}
CatDto catDto = new CatDto();
catDto.setName( entity.getNameInDatabase() );
catDto.setColor( entity.getColorInDatabase() );
return catDto;
}
}
in conclusion
We've covered the most popular scenarios that arise when developing multi-tier applications. Therefore, the combination of Project Lombok and the MapStruct library can significantly save developers time and effort on boilerplate.
Thank you for your attention!