tags: [MapStruct]
categories: [工具]


前言

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

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

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

开始之前

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

  1. 自己书写代码

    1. user.setName(userInfo.getUserName);

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

  2. 使用反射来做到自动注入

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

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

  1. 使用工具类,比如beanUtil或者dozer工具
    1. DozerBeanMapper dozerBeanMapper = new DozerBeanMapper();
    2. HashMap map = dozerBeanMapper.map(USER.builder()
    3. .name("张三").password("123").build(), HashMap.class);
    字段名称一致较为方便,但是字段不一致,beanUtil不支持,dozer需要自己来配置xml实现不同字段的映射关系,xml都懂的,比较麻烦

MapStruct

简介

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

基本使用

  1. 导包,这里用的目前的最新版本 ```xml

    1. <mapstruct-version>1.3.1.Final</mapstruct-version>
    2. <!--map struct start-->
    3. <dependency>
    4. <groupId>org.mapstruct</groupId>
    5. <artifactId>mapstruct-jdk8</artifactId>
    6. <version>${mapstruct-version}</version>
    7. </dependency>
    8. <dependency>
    9. <groupId>org.mapstruct</groupId>
    10. <artifactId>mapstruct-processor</artifactId>
    11. <version>${mapstruct-version}</version>
    12. </dependency>
    13. <!--map struct end-->
  1. 2. 书写转换接口
  2. ```java
  3. @Mapper(componentModel = "spring" )
  4. @Component
  5. public interface TestConverter {
  6. TestConverter INSTANCE = Mappers.getMapper(TestConverter.class);
  7. /**
  8. * AAA 转 TestPo
  9. *
  10. *
  11. * @param aaa aaa
  12. * @return TestPo
  13. */
  14. @Mappings({
  15. @Mapping(source = "aaa", target = "aa"),
  16. @Mapping(source = "bbb", target = "bb"),
  17. @Mapping(target = "ccc", expression = "java(aaa.getCcc().toString())")
  18. })
  19. BwLsdMxPo dto2TestPo(AAA aaa);
  20. }
  1. 使用时,直接注入,然后调用接口中的方法就行了

    1. @Autowired
    2. private TestConverter testConverter;
    3. ...
    4. 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. @Mapper
    2. public interface CarMapper {
    3. @Mapping(source = "manufacturingDate", dateFormat = "dd.MM.yyyy")
    4. CarDto carToCarDto(Car car);
    5. @IterableMapping(dateFormat = "dd.MM.yyyy")
    6. List<String> stringListToDateList(List<Date> dates);
    7. }

    显示的表达式转换

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

    1. @Mapper
    2. public interface SourceTargetMapper {
    3. SourceTargetMapper INSTANCE = Mappers.getMapper( SourceTargetMapper.class );
    4. @Mapping(target = "timeAndFormat",
    5. expression = "java( new org.sample.TimeAndFormat( s.getTime(), s.getFormat() ) )")
    6. Target sourceToTarget(Source s);
    7. @Mapping(target="id", source="sourceId", defaultExpression = "java( UUID.randomUUID().toString() )")
    8. Target sourceToTarget(Source s);
    9. }

    常量 默认值 属性忽略

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

    1. @Mapper(uses = StringListMapper.class)
    2. public interface SourceTargetMapper {
    3. SourceTargetMapper INSTANCE = Mappers.getMapper( SourceTargetMapper.class );
    4. @Mapping(target = "stringProperty", source = "stringProp", defaultValue = "undefined")
    5. @Mapping(target = "longProperty", source = "longProp", defaultValue = "-1")
    6. @Mapping(target = "stringConstant", constant = "Constant Value")
    7. @Mapping(target = "integerConstant", constant = "14")
    8. @Mapping(target = "longWrapperConstant", constant = "3001")
    9. @Mapping(target = "dateConstant", dateFormat = "dd-MM-yyyy", constant = "09-01-2014")
    10. @Mapping(target = "stringListConstants", constant = "jack-jill-tom")
    11. Target sourceToTarget(Source s);
    12. }

    装饰器完成额外的操作

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

  1. 一般实现

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

    1. public abstract class PersonMapperDecorator implements PersonMapper {
    2. @Autowired
    3. @Qualifier("delegate")
    4. private PersonMapper delegate;
    5. @Override
    6. public PersonDto personToPersonDto(Person person) {
    7. PersonDto dto = delegate.personToPersonDto( person );
    8. dto.setName( person.getFirstName() + " " + person.getLastName() );
    9. return dto;
    10. }
    11. }

参考

MapStruct官方文档