Spring mapstruct
This post was migrated from Tistory. You can find the original here.
mapstruct
mapstruct is a package that generates code for converting between dto and entity.
1
2
3
4
5
6
7
8
9
10
11
//build.gradle
dependencies {
...
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
//Put mapstruct after lombok
implementation 'org.mapstruct:mapstruct:1.5.5.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final'
...
}
Caution
If you use lombok for getters, setters, builders, etc., lombok’s code generation needs to run before mapstruct runs.
Adding mapstruct after lombok in dependencies guarantees that lombok runs first. (It seems the order in which dependencies are added is related to the execution order.)
If mapstruct is added before lombok, compilation often fails because the getter/setter code isn’t there yet.
Usage
1
2
3
4
5
6
7
8
9
10
11
//EntityMapper.java
public interface EntityMapper<E, D> {
E toEntity(D dto);
D toDto(E entity);
// @BeanMapping(~) how to handle it when the source is null
// @MappingTarget the object to be modified based on the input
// A setter is required on the Target side
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
void partialUpdate(D dto, @MappingTarget E entity);
}
You can define a common EntityMapper interface like above and use it.
Besides entity <-> dto conversion, it can also generate code for partial updates.
@MappingTarget is the object being modified, and a setter is required on the Target side.
1
2
3
4
5
6
7
//PostMapper.java
@Mapper(
componentModel = "spring",
unmappedTargetPolicy = ReportingPolicy.IGNORE
)
public interface PostMapper extends EntityMapper<Post, PostDto>{
}
By default, if you write an interface annotated with @Mapper, an Impl (implementation) is generated at compile time.
Setting componentModel = “spring” registers the implementation as a bean.
1
2
3
4
5
6
@Mapper(
unmappedTargetPolicy = ReportingPolicy.IGNORE
)
public interface PostMapper extends EntityMapper<Post, PostDto>{
PostMapper INSTANCE = Mappers.getMapper(PostMapper.class);
}
If you don’t register it as a bean, you use it as shown above.
Manual mapping
1
2
3
4
5
6
7
8
9
10
11
12
//With ReportingPolicy.ERROR, an exception is thrown before manual mapping for unmatched fields
@Mapper(
componentModel = "spring",
unmappedTargetPolicy = ReportingPolicy.IGNORE
)
public interface PostMapper2 extends EntityMapper<Post, PostDto2>{
//Manual mapping when fields don't match
//source is the input, target is the output
@Override
@Mapping(source = "title", target = "title2")
PostDto2 toDto(Post entity);
}
If the dto and entity fields don’t match, manual mapping is required.
In the case above, the entity’s title is mapped to the Dto’s title2.
Specifying a mapping method
You can also specify a method to use for mapping.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Mapper(
componentModel = "spring",
unmappedTargetPolicy = ReportingPolicy.IGNORE
)
public interface ProgramResponseMapper {
@Mapping(source = "exerciseCondition", target = "exerciseConditionId", qualifiedByName = "mapConditionId")
public ProgramResponseDto toDto(ExerciseProgram program);
@Named("mapConditionId")
static Long mapConditionId(ExerciseCondition condition) {
return condition.getId();
}
}
This maps the entity’s exerciseCondition field to the responseDto’s exerciseConditionId field, using mapConditionId as the mapping method.
The @Named annotation designates which method should run for the corresponding mapping method name.
+) Mapping multiple fields, using one Mapper (B) inside another Mapper (A)
1
2
3
4
5
6
7
8
9
10
11
12
13
@Mapper(
componentModel = "spring",
unmappedTargetPolicy = ReportingPolicy.IGNORE,
uses = {ImageResDtoMapper.class, userResDtoMapper.class}
)
public interface PostMapper extends EntityMapper<Post, PostDto>{
@Override
@Mappings({
@Mapping(source = "image", target = "imageResDto"),
@Mapping(source = "user", target = "userResDto")
})
PostDto toDto(Post entity);
}
Put the other mapper classes you want to use into uses.
If the source object and target object correspond to a registered mapper, that mapper is applied automatically.
In the code above, even without specifying a separate mapping method, ImageResDtoMapper.toDto, which converts image => imageResDto, is applied automatically.
Generated code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
@Component
public class PostMapperImpl implements PostMapper {
public PostMapperImpl() {
}
public Post toEntity(PostDto dto) {
if (dto == null) {
return null;
} else {
Post.PostBuilder post = Post.builder();
post.title(dto.getTitle());
post.subTitle(dto.getSubTitle());
post.subscription(dto.getSubscription());
return post.build();
}
}
public PostDto toDto(Post entity) {
if (entity == null) {
return null;
} else {
PostDto.PostDtoBuilder postDto = PostDto.builder();
postDto.title(entity.getTitle());
postDto.subTitle(entity.getSubTitle());
postDto.subscription(entity.getSubscription());
return postDto.build();
}
}
public void partialUpdate(PostDto dto, Post entity) {
if (dto != null) {
if (dto.getTitle() != null) {
entity.setTitle(dto.getTitle());
} else {
entity.setTitle("");
}
if (dto.getSubTitle() != null) {
entity.setSubTitle(dto.getSubTitle());
} else {
entity.setSubTitle("");
}
if (dto.getSubscription() != null) {
entity.setSubscription(dto.getSubscription());
} else {
entity.setSubscription("");
}
}
}
}
By default, this is the kind of code that gets generated.
It doesn’t have to be a builder — the generated code flexibly adapts to whatever lombok provides, whether that’s setters, an all-args constructor, etc.
If the mapper fails to compile, it may be because the get/set methods or constructors needed to build the implementation weren’t set up properly.