mapstruct详解

岳俊晖
2023-12-01

功能介绍

mapStruct是什么

MapStruct是基于JSR 269的Java注解处理器,因此可以在命令行构建中使用(javac、Ant、Maven等等),可以在IDE内使用。用于生成类型安全的bean映射类的Java注解处理器。属于编译时注解,如果转换bean内容有变化。需要手动clean下才能将变化的内容体现到class文件中。说白了就是通过注解的形式帮我们生成set,get方法。

mapStruct有哪些功能

最大的功能就是为我们转换两个不同的bean,或者List,比如拿java的代码规范来说。客户端入参–>系统的业务层入参—>db数据库的入参;就对这3个层次来说。一个req绝对是不能透传到db层的。为什么呢?就是为了降低耦合。提高业务的单一性,那么某一层参数有改变不会影响其他层。

整合到spring中

基本使用

接口注入静态方法调用

引入

 <!-- https://mvnrepository.com/artifact/org.mapstruct/mapstruct -->
<dependency>
  <groupId>org.mapstruct</groupId>
  <artifactId>mapstruct</artifactId>
  <version>1.3.1.Final</version>
</dependency>
<dependency>
  <groupId>org.mapstruct</groupId>
  <artifactId>mapstruct-processor</artifactId>
  <version>1.3.1.Final</version>
</dependency>

示例

基础映射

@Data
public class PersonDto {
    private String name;
    private String sex;
    private Date birthday;
    private int age;
    private Double money;
    private String password;
    private Date createTime;

}
//-----------------------------------
@Data
public class PersonVo {
    private String fullName;
    private String sex;
    private String birthday;
    private int age;
    private Double money;
    private String password;
}
//-----------------------------------
@Mapper
public interface PersonMapper {

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

    @Mapping( source = "fullName",target ="name")
    @Mapping( target ="birthday", dateFormat = "yyyy-MM-dd")
    PersonDto vo2dto(PersonVo vo);

    @Mapping( source = "name",target ="fullName")
    @Mapping( target ="birthday", dateFormat = "yyyy-MM-dd")
    @Mapping( target ="password",ignore = true)
    PersonVo dto2vo(PersonDto dto);

}

//-----------------------------------
ublic static void test1() {
        PersonDto dto = new PersonDto();
        dto.setAge(10);
        dto.setBirthday(Date.from(Instant.now()));
        dto.setMoney(2.543D);
        dto.setName("john");
        dto.setSex("F");
        dto.setPassword("password");
        dto.setCreateTime(new Date());

        System.out.println("dto:::" + dto.toString());
        PersonVo vo = PersonMapper.INSTANCE.dto2vo(dto);
        System.out.println("vo:::" + vo.toString());
        PersonDto dto2 = PersonMapper.INSTANCE.vo2dto(vo);
        System.out.println("dto2:::" + dto2.toString());

    }
//--------------OUTPUT:
dto:::PersonDto(name=john, sex=F, birthday=Wed Apr 07 11:53:05 CST 2021, age=10, money=2.543, password=password, createTime=Wed Apr 07 11:53:05 CST 2021)
vo:::PersonVo(fullName=john, sex=F, birthday=2021-04-07, age=10, money=2.543, password=null)
dto2:::PersonDto(name=john, sex=F, birthday=Wed Apr 07 00:00:00 CST 2021, age=10, money=2.543, password=null, createTime=null)

处理了以下几种情况:

  • 相同属性名称自动隐式映射
  • 属性名不同,需要指定@mapping
  • 类型不同,需要指定转换格式
  • 不需要设置的字段,使用ignore。如果字段在目标对象上不匹配的,则会自动忽略。

自动生成代码

package demon.study.mapstruct;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import javax.annotation.Generated;

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2021-04-07T11:51:35+0800",
    comments = "version: 1.3.1.Final, compiler: javac, environment: Java 1.8.0_181 (Oracle Corporation)"
)
public class PersonMapperImpl implements PersonMapper {

    @Override
    public PersonDto vo2dto(PersonVo vo) {
        if ( vo == null ) {
            return null;
        }

        PersonDto personDto = new PersonDto();

        personDto.setName( vo.getFullName() );
        personDto.setSex( vo.getSex() );
        try {
            if ( vo.getBirthday() != null ) {
                personDto.setBirthday( new SimpleDateFormat( "yyyy-MM-dd" ).parse( vo.getBirthday() ) );
            }
        }
        catch ( ParseException e ) {
            throw new RuntimeException( e );
        }
        personDto.setAge( vo.getAge() );
        personDto.setMoney( vo.getMoney() );
        personDto.setPassword( vo.getPassword() );

        return personDto;
    }

    @Override
    public PersonVo dto2vo(PersonDto dto) {
        if ( dto == null ) {
            return null;
        }

        PersonVo personVo = new PersonVo();

        personVo.setFullName( dto.getName() );
        personVo.setSex( dto.getSex() );
        if ( dto.getBirthday() != null ) {
            personVo.setBirthday( new SimpleDateFormat( "yyyy-MM-dd" ).format( dto.getBirthday() ) );
        }
        personVo.setAge( dto.getAge() );
        personVo.setMoney( dto.getMoney() );

        return personVo;
    }
}

多个对象转换为1个对象

@Data
public class A1 {
    private String a;
    private String b;
}

@Data
public class A2 {
    private String c;
}

@Data
public class A {
    private String a;
    private String b;
    private String c;
}


@Mapper
public interface AMapper {

    AMapper INSTANCE = Mappers.getMapper( AMapper.class );
    @Mapping(source = "a1.a", target = "a")
    @Mapping(source = "a1.b", target = "b")
    @Mapping(source = "a2.c", target = "c")
    A two2one(A1 a1,A2 a2 );
}

//-----------------output:

自动生成代码

public class AMapperImpl implements AMapper {

    @Override
    public A two2one(A1 a1, A2 a2) {
        if ( a1 == null && a2 == null ) {
            return null;
        }

        A a = new A();

        if ( a1 != null ) {
            a.setA( a1.getA() );
            a.setB( a1.getB() );
        }
        if ( a2 != null ) {
            a.setC( a2.getC() );
        }

        return a;
    }
}

自定义转换器

枚举与Integer型不能互相转换,需要自定义(枚举与String 可能会转换正确,Int向枚举也可能转换正确)。

不兼容的类型需要自定义转换。

可以使用@mapper(uses),@mapping(expression)实现

@mapping(expression) 主要用于多个属性与一个属性的映射


public class Custom {
    @Getter
    public enum CustomType {
        EMPTY(0, "EMPTY"),
        A(1, "A"),
        B(2, "B");

        private Integer code;
        private String desc;

        CustomType(Integer code, String desc) {
            this.code = code;
            this.desc = desc;
        }

        @Override
        public String toString() {
            return String.format("{%s,%s}", code, desc);
        }

        public static CustomType from(Integer code) {
            switch (code) {
                case 1:
                    return CustomType.A;
                case 2:
                    return CustomType.B;
                default:
                    return CustomType.EMPTY;
            }
        }
    }

    @Data
    public static class CustomDto {
        private Date birthday;
        private CustomType customA;
        private CustomType customB;
        private CustomType customC;
    }

    @Data
    public static class CustomVo {
        private long birthday;
        private String customA;
        private Integer customB;
        private Integer customC;
        private int all;
    }

    @Mapper(uses = {Date2IntHandler.class})
    public interface CustomMapper {
        CustomMapper INSTANCE = Mappers.getMapper(CustomMapper.class);

        @Mapping(target = "all", expression = "java(enum2Int(dto.getCustomB()) + enum2Int(dto.getCustomA())  )")
        CustomVo dto2vo(CustomDto dto);

        CustomDto vo2dto(CustomVo vo);

        default CustomType int2Enum(Integer code) {

            return CustomType.from(code);
        }

        default Integer enum2Int(CustomType type) {

            return type.getCode();
        }

    }

    public static class Date2IntHandler {

        public static Date int2Date(long i) {
            return new Date(i);
        }

        public static long date2Int(Date date) {
            return date.getTime();
        }
    }

    public static void main(String[] args) {
        test();
    }

    public static void test() {

        CustomDto dto = new CustomDto();
        dto.setBirthday(Date.from(Instant.now().minus(Period.ofDays(1000))));
        dto.setCustomA(CustomType.A);
        dto.setCustomB(CustomType.B);
        dto.setCustomC(CustomType.B);

        System.out.println("dto:::" + dto.toString());
        CustomVo vo = CustomMapper.INSTANCE.dto2vo(dto);
        System.out.println("vo:::" + vo.toString());
        CustomDto dto2 = CustomMapper.INSTANCE.vo2dto(vo);
        System.out.println("dto2:::" + dto2.toString());

    }
}

输出:

dto:::Custom.CustomDto(birthday=Thu Jul 12 14:38:04 CST 2018, customA={1,A}, customB={2,B}, customC={2,B})
vo:::Custom.CustomVo(birthday=1531377484863, customA=A, customB=2, customC=2, all=3)
dto2:::Custom.CustomDto(birthday=Thu Jul 12 14:38:04 CST 2018, customA={1,A}, customB={2,B}, customC={2,B})

自动生成代码

public class Custom$CustomMapperImpl implements CustomMapper {

    @Override
    public CustomVo dto2vo(CustomDto dto) {
        if ( dto == null ) {
            return null;
        }

        CustomVo customVo = new CustomVo();

        customVo.setBirthday( Date2IntHandler.date2Int( dto.getBirthday() ) );
        if ( dto.getCustomA() != null ) {
            customVo.setCustomA( dto.getCustomA().name() );
        }
        customVo.setCustomB( enum2Int( dto.getCustomB() ) );
        customVo.setCustomC( enum2Int( dto.getCustomC() ) );

        customVo.setAll( enum2Int(dto.getCustomB()) + enum2Int(dto.getCustomA()) );

        return customVo;
    }

    @Override
    public CustomDto vo2dto(CustomVo vo) {
        if ( vo == null ) {
            return null;
        }

        CustomDto customDto = new CustomDto();

        customDto.setBirthday( Date2IntHandler.int2Date( vo.getBirthday() ) );
        if ( vo.getCustomA() != null ) {
            customDto.setCustomA( Enum.valueOf( CustomType.class, vo.getCustomA() ) );
        }
        customDto.setCustomB( int2Enum( vo.getCustomB() ) );
        customDto.setCustomC( int2Enum( vo.getCustomC() ) );

        return customDto;
    }
}

注意:

@mapping的属性,sourceexpression 不能同时定义,因为expression中使用了表达式。

Maper中定义的转换函数 ,会被自动应用到相同类型属性的转换上,可能会导致非期望结果。

mapper使用方式

1、接口注入方式

mapStruct接口类的声明

//使用@Mapper(componentModel = "spring") 整合
@Mapper(componentModel = "spring") 
public interface ItemInfoConvert {

}
//------------------------------------------------------
//调用:
//直接接口注入,方法调用即可
 @Autowired
 private ItemInfoConvert itemInfoConvert;
 Person person = itemInfoConvert.deliveryDO2DTO(cart);
 

2、静态方法调用

mapStruct接口类的声明

public interface ItemInfoConvert {
   //静态方法
    ItemInfoConvert INSTANCE = Mappers.getMapper(ItemInfoConvert.class);
}
//---------------------------------------------
//调用:
Person person= ItemInfoConvert.INSTANCE.deliveryDO2DTO(cart);

建议按以下方式使用

@Data
public class PersonDto {
    ......

    /**
     * 输入为null,输出则为null
     * 为什么名字为toVo0?其实类是支持重载(overload),但是在使用stream时,区分不了是想用实例方法,还是类方法。
     * @param dto
     * @return
     */
    public static PersonVo toVo0(PersonDto dto) {
        return PersonMapper.INSTANCE.dto2vo(dto);
    }

    public PersonVo toVo() {
        return PersonMapper.INSTANCE.dto2vo(this);
    }

    public static PersonDto from(PersonVo vo) {
        if (vo == null) {
            return null;
        }
        return PersonMapper.INSTANCE.vo2dto(vo);

    }
}
//------------------------------------
@Data
public class PersonVo {
... ... 


    public static PersonDto toDto0(PersonVo vo) {
        if (vo == null)
        {
            return null;
        }
        return PersonMapper.INSTANCE.vo2dto(vo);
    }

    public   PersonDto toDto(){
        return PersonMapper.INSTANCE.vo2dto(this);
    }

    public static PersonVo from(PersonDto dto){
        if (dto == null)
        {
            return null;
        }
        return PersonMapper.INSTANCE.dto2vo(dto);
    }
}

//------------------------------------
 public static void test0() {
        PersonDto dto1 = new PersonDto();
        dto1.setAge(10);
        dto1.setBirthday(Date.from(Instant.now()));
        dto1.setMoney(2.543D);
        dto1.setName("john");
        dto1.setSex("F");
        dto1.setPassword("password");
        dto1.setCreateTime(new Date());

        PersonDto dto2 = new PersonDto();
        dto2.setAge(20);
        dto2.setBirthday(Date.from(Instant.now()));
        dto2.setMoney(7.543D);
        dto2.setName("tom");
        dto2.setSex("M");
        dto2.setPassword("password");
        dto2.setCreateTime(new Date());

        List<PersonDto> list = Arrays.asList(dto1, null, dto2);

        //未考虑空引用
        List<PersonVo> list2 = list.stream().map(dto -> dto.toVo()).collect(Collectors.toList());
        //考虑了空引用
        List<PersonVo> list3 = list.stream().map(PersonDto::toVo0).collect(Collectors.toList());

    }
//------------------------------------


更多控制

见官方文档,可以实现:

  • 内嵌属性
  • collection,maps,list,stream等
  • defaultvalue等

问题总结

  1. 保证相同的数据结构,即参数的类型相同,同string 或者同Integer,类型不同无法转换

    Exception in thread "main" java.lang.NoSuchMethodError: demon.study.mapstruct.PersonVo.setBirthday(Ljava/util/Date;)V
    

    必须要使用@Mapping来解决此类问题。

  2. 避开关键字,比如:delete等

  3. 如果项目中也同时使用到了 Lombok,一定要注意 Lombok的版本要等于或者高于1.18.10,否则会有编译不通过的情况发生

注解说明

  • @Mapper:注解在接口、类上,这样 MapStruct 才会去实现该接口

    属性componentModel:该属性用于指定实现类的类型,有几个属性值:

    • default:默认,不使用任何组建类型,可以通过Mappers.getMapper(Class) 方式获取实例对象
    • spring:在实现类上注解 @Component,可通过 @Autowired 方式注入
    • jsr330:实现类上添加@javax.inject.Named 和@Singleton注解,可以通过 @Inject注解获取。
  • @Mappings:配置多个@Mapping

  • @Mapping:配置属性映射,若源对象属性与目标对象名字一致,会自动映射对应属性

    • source:源属性、target:目标属性
    • dateFormat:可将 String 到 Date 日期之间相互转换,通过 SimpleDateFormat,该值为 SimpleDateFormat 的日期格式
    • ignore: 忽略这个字段

精细控制

控制拷贝属性时,源字段为null时,是否覆盖目标对象。

@Mapper(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE,
nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS)   //进行null 检查,如果为null,则不复制属性值。


nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.SET_TO_DEFAULT //赋值 默认值
nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE         //忽略,保持目标对象属性值    

The strategy works in a hierarchical fashion. @Mapping#nullValueCheckStrategy will override @BeanMapping#nullValueCheckStrategy, @BeanMapping#nullValueCheckStrategy will override @Mapper#nullValueCheckStrategy and @Mapper#nullValueCheckStrategy will override @MaperConfig#nullValueCheckStrategy.

NullValuePropertyMappingStrategy also applies when the presence checker returns not present.

参考

  • MapStruct 官网:https://mapstruct.org/
  • MapStruct 官方文档:https://mapstruct.org/documentation/reference-guide/ , https://mapstruct.org/documentation/1.3/reference/html/#expressions
  • MapStruct maven 地址:https://mvnrepository.com/artifact/org.mapstruct/mapstruct
  • MapStruct github 地址:https://github.com/mapstruct/mapstruct/
  • https://www.cnblogs.com/javaguide/p/11861749.html
 类似资料: