MapStruct使用

前言

在实际项目开发中,我们常常要新建很多 DTO、VO 对象用于数据的展示或者对象的传输
痛点在于

  1. 很多对象的字段名不一致,但是却是同一个字段
  2. 部分字段类型需要转换,比如字符串解析为时间,比如设置默认值

繁琐的工作常常耽误很多时间,最近了解到类 MapStruct 工具可以相对来说比较方便的完成这项工作

开始之前

开始之前,先分析一下之前的做法

  1. 自己书写代码
1
user.setName(userInfo.getUserName);

这种方式数据多了会非常繁琐!

  1. 使用反射来做到自动注入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
        List<USER> users =new ArrayList<USER>(8) ;
        users.add(USER.builder().name("张三").password("123").build());
        users.add(USER.builder().name("里斯").build());
        List<HashMap<String, Object>> hashMapList = users.stream().map(x -> {
            HashMap<String, Object> map = new HashMap(8);
            Class<? extends USER> clazz = x.getClass();
            for (Field field : clazz.getDeclaredFields()) {
                field.setAccessible(true);
                String fieldName = field.getName();
                try {
                    Object o = field.get(x);
                    map.put(fieldName, o);
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
            return map;
        }).collect(Collectors.toList());

这里也能通过注解来做到不同的字段名做到相互转换,但是问题在于反射对于性能影像过大,而且需要新建注解

  1. 使用工具类,比如 beanUtil 或者 dozer 工具
1
2
3
        DozerBeanMapper dozerBeanMapper = new DozerBeanMapper();
        HashMap map = dozerBeanMapper.map(USER.builder()
                               .name("张三").password("123").build(), HashMap.class);

字段名称一致较为方便,但是字段不一致,beanUtil 不支持,dozer 需要自己来配置 xml 实现不同字段的映射关系,xml 都懂的,比较麻烦

MapStruct

简介

MapStruct 实际上会在编译的时候就将指定的 set/get 方法生生产,编译为 class 文件,实际接口方法调用时是通过它的是实现类来操作的,而它的赋值规则就是油我们注解配置而生成从而达到,帮你写代码的作用
推荐IntelliJ IDEA  可以配置对应的插件Mapstruct Support

基本使用

  1. 导包,这里用的目前的最新版本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 <mapstruct-version>1.3.1.Final</mapstruct-version>

<!--map struct start-->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-jdk8</artifactId>
<version>${mapstruct-version}</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct-version}</version>
</dependency>
<!--map struct end-->

  1. 书写转换接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Mapper(componentModel = "spring" )
@Component
public interface TestConverter {

TestConverter INSTANCE = Mappers.getMapper(TestConverter.class);

/**
* AAA 转 TestPo
*
*
* @param aaa aaa
* @return TestPo
*/
@Mappings({
@Mapping(source = "aaa", target = "aa"),
@Mapping(source = "bbb", target = "bb"),
@Mapping(target = "ccc", expression = "java(aaa.getCcc().toString())")
})
BwLsdMxPo dto2TestPo(AAA aaa);
}

  1. 使用时,直接注入,然后调用接口中的方法就行了
1
2
3
4
5
   @Autowired
private TestConverter testConverter;

...
TestPo testPo = TestConverter.dto2TestPo(x);

进阶使用

依赖注入

1
@Mapper(componentModel = "spring" )

在接口上@Mapper注解中的componentModel属性设置为 spring 代表将改接口的是实现类作为bean注入到 spring 中,这样我们就能使用@Autowired注解来注入该 bean

隐式转换

在许多情况下,MapStruct 会自动处理类型转换

  • 基本数据类型和包装类的转换,如intInteger
  • 基本数据类型之间和包装类之间,如intlongbyteInteger
  • enum类型和之间String
  • 在大数字类型(java.math.BigIntegerjava.math.BigDecimal)和 Java 基本类型(包括其包装器)以及 String 之间。java.text.DecimalFormat可以指定理解的格式字符串
  • 日期类型之间,这个需要指定格式化模板
1
2
3
4
5
6
7
8
9
@Mapper
public interface CarMapper {

@Mapping(source = "manufacturingDate", dateFormat = "dd.MM.yyyy")
CarDto carToCarDto(Car car);

@IterableMapping(dateFormat = "dd.MM.yyyy")
List<String> stringListToDateList(List<Date> dates);
}

显示的表达式转换

我们可以通过Expression标签来在字符串中书写 JAVA 代码,完成指定属性的转换,值得注意的是,若该方法的调用者未导包需要带上包名
defaultExpression为默认表达式

1
2
3
4
5
6
7
8
9
10
11
12
@Mapper
public interface SourceTargetMapper {

SourceTargetMapper INSTANCE = Mappers.getMapper( SourceTargetMapper.class );

@Mapping(target = "timeAndFormat",
expression = "java( new org.sample.TimeAndFormat( s.getTime(), s.getFormat() ) )")
Target sourceToTarget(Source s);

@Mapping(target="id", source="sourceId", defaultExpression = "java( UUID.randomUUID().toString() )")
Target sourceToTarget(Source s);
}

常量 默认值 属性忽略

有些情况下,部分属性为 null 时,我们需要设置默认值,常量和默认值的区别在于当不为空时,默认值会被替换为实际值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Mapper(uses = StringListMapper.class)
public interface SourceTargetMapper {

SourceTargetMapper INSTANCE = Mappers.getMapper( SourceTargetMapper.class );

@Mapping(target = "stringProperty", source = "stringProp", defaultValue = "undefined")
@Mapping(target = "longProperty", source = "longProp", defaultValue = "-1")
@Mapping(target = "stringConstant", constant = "Constant Value")
@Mapping(target = "integerConstant", constant = "14")
@Mapping(target = "longWrapperConstant", constant = "3001")
@Mapping(target = "dateConstant", dateFormat = "dd-MM-yyyy", constant = "09-01-2014")
@Mapping(target = "stringListConstants", constant = "jack-jill-tom")
Target sourceToTarget(Source s);
}

装饰器完成额外的操作

列举一个场景,比如实体类中价税合计 = 税额 + 合计金额,我们需要对赋完值的税额和合计金额做一个累加操作,这里可以通过装饰器来实现

  1. 一般实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public abstract class PersonMapperDecorator implements PersonMapper {

private final PersonMapper delegate;

public PersonMapperDecorator(PersonMapper delegate) {
this.delegate = delegate;
}

@Override
public PersonDto personToPersonDto(Person person) {
PersonDto dto = delegate.personToPersonDto( person );
dto.setFullName( person.getFirstName() + " " + person.getLastName() );
return dto;
}
}
  1. 通过 spring 注入来实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public abstract class PersonMapperDecorator implements PersonMapper {

@Autowired
@Qualifier("delegate")
private PersonMapper delegate;

@Override
public PersonDto personToPersonDto(Person person) {
PersonDto dto = delegate.personToPersonDto( person );
dto.setName( person.getFirstName() + " " + person.getLastName() );

return dto;
}
}

参考

MapStruct 官方文档

作者

孙博文

发布于

2020-04-29

更新于

2021-07-18

许可协议

评论