mapstruct对象复制&转换


简介

从功能上来讲,mapstruct是一款类似于BeanUtils.copyProperties(Object source, Object target)一样,实现对象属性值复制的;从实现上来讲,mapstruct是一款类似于lombok,基于你给出的方法入参出参模型及方法、类上的相关辅助注解,直接在编译时生成对应的属性值转换实现类。因为mapstruct是使用自动生成的代码实现的对象属性值赋值(而不是像BeanUtils一样采用反射获取值赋值),所以性能更快、效率更高。更多详见官网

使用步骤简述

第一步:引入相关依赖

...
<properties>
    <org.mapstruct.version>1.4.2.Final</org.mapstruct.version>
</properties>
...
<dependencies>
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct</artifactId>
        <version>${org.mapstruct.version}</version>
    </dependency>
</dependencies>
...
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.8.1</version>
            <configuration>
                <source>1.8</source>
                <target>1.8</target>
                <annotationProcessorPaths>
                    <path>
                        <!-- 如果项目中还是用到了lombok,那么也需要加上lombok处理器声明 -->
                        <groupId>org.projectlombok</groupId>
                        <artifactId>lombok</artifactId>
                        <version>${lombok.version}</version>
                    </path>
                    <path>
                        <groupId>org.mapstruct</groupId>
                        <!-- 此处理器用于 在编译时生成具体的Mapper实现 -->
                        <artifactId>mapstruct-processor</artifactId>
                        <version>${org.mapstruct.version}</version>
                    </path>
                </annotationProcessorPaths>
                <!-- 编译时输出mapstruct的详细信息 start -->
                <showWarnings>true</showWarnings>
                <compilerArgs>
                    <arg>
                        -Amapstruct.verbose=true
                    </arg>
                </compilerArgs>
                <!-- 编译时输出mapstruct的详细信息 end -->
            </configuration>
        </plugin>
    </plugins>
</build>
...

第二步:定义Mapper转换器

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;

@Mapper
public interface CarMapper {
    
    @Mapping(target = "seatCount", source = "numberOfSeats")
    CarDto carToCarDto(Car car);
}

@Mapper
public abstract class CarMapper {
    
    @Mapping(target = "seatCount", source = "numberOfSeats")
    public abstract CarDto carToCarDto(Car car);
}

第三步:使用Mapper转换器

public static void main(String[] args) throws IOException {
    // source对象
    Car car = new Car();
    car.setNumberOfSeats(123);
    car.setColor("白色");

    // 获取mapper实例
    CarMapper mapper = Mappers.getMapper(CarMapper.class);
    
    // 调用对应方法,实现转换
    CarDto carDto = mapper.carToCarDto(car);
    
    // 输出: CarDto(seatCount=123, color=白色)
    System.out.println(carDto);
}

获取Mapper实例的方式

注:获取Mapper实例的方式,取决于@Mapper(componentModel=xxx)中,componentModel的模式:

  • default:the mapper uses no component model, instances are typically retrieved via Mappers.getMapper(Class)
  • cdi模式:the generated mapper is an application-scoped CDI bean and can be retrieved via @Inject
  • spring模式:the generated mapper is a Spring bean and can be retrieved via @Autowired
  • jsr330模式:the generated mapper is annotated with @javax.inject.Named and @Singleton, and can be retrieved via @Inject

default模式

default时,可通过 Mappers.getMapper(Class)获取实例

@Mapper
//等价于@Mapper(componentModel = "default")
public interface CarMapper {
    
    @Mapping(target = "seatCount", source = "numberOfSeats")
    CarDto carToCarDto(Car car);
}

获取实例

// 获取mapper实例
CarMapper mapper1 = Mappers.getMapper(CarMapper.class);
// 输出: com.szlaozicl.mybatisplusdemo.tmp.CarMapperImpl@69e308c6
System.out.println(mapper1);

CarMapper mapper2 = Mappers.getMapper(CarMapper.class);
// 输出: com.szlaozicl.mybatisplusdemo.tmp.CarMapperImpl@1a1ed4e5
System.out.println(mapper2);

注:Mappers.getMapper(Class)获取实例时,每次都是获取到一个新的实例。所以如果非要使用Mappers.getMapper(Class)的话,需要尽量避免重复创建,可以使用下述方式:

@Mapper
public interface CarMapper {

 /** 全局使用这一个对象 */
 CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);

 @Mapping(target = "seatCount", source = "numberOfSeats")
 CarDto carToCarDto(Car car);
}

或者

@Mapper
public abstract class BusMapper {

 /** 全局使用这一个对象 */
 public final BusMapper INSTANCE = Mappers.getMapper(BusMapper.class);

 @Mapping(target = "seatCount", source = "numberOfSeats")
 public abstract CarDto carToCarDto(Car car);
}

spring模式

spring模式时,可通过 Mappers.getMapper(Class)@Autowired@Resource等方式获取实例

@Mapper(componentModel = "spring")
public interface CarMapper {
    
    @Mapping(target = "seatCount", source = "numberOfSeats")
    CarDto carToCarDto(Car car);
}

获取实例

@SpringBootApplication
public class SpringBootDemoApplication implements ApplicationRunner {
    
    @Autowired
    private CarMapper carMapper1;
    
    @Resource
    private CarMapper carMapper2;
    
    public static void main(String[] args) throws IOException {
        SpringApplication.run(SpringBootDemoApplication.class, args);
    }
    
    @Override
    public void run(ApplicationArguments args) throws Exception {
        // 获取mapper实例
        CarMapper mapper = Mappers.getMapper(CarMapper.class);
        // 输出: com.szlaozicl.mybatisplusdemo.tmp.CarMapperImpl@f2c488
        System.out.println(mapper);
        
        // 输出: com.szlaozicl.mybatisplusdemo.tmp.CarMapperImpl@54acff7d
        System.err.println(carMapper1);
        
        // 输出: com.szlaozicl.mybatisplusdemo.tmp.CarMapperImpl@54acff7d
        System.err.println(carMapper2);
    }
}

cdi模式和jsr330模式

使用较少,不作介绍,详见官网。

常用知识点

source是转化源,target是转化目标

target是新对象

@Mapper(componentModel = "spring")
public interface CarMapper {
    
    /** Car为source, CarDto为target */
    CarDto carToCarDto(Car car);
}

注:target是一个新创建的对象。

target是已有对象

@Mapper(componentModel = "spring")
public interface CarMapper {
    
    /** Car为source, CarDto为target */
    void  carToCarDto(Car car, @MappingTarget CarDto carDto);
}

字段名不同时

通过@Mappingsourcetarget指定字段名映射

@Mapper(componentModel = "spring")
public interface CarMapper {
    
    /** 指定不同字段名间的映射 */
    @Mapping(target = "seatCount", source = "numberOfSeats")
    CarDto carToCarDto(Car car);
}

注:默认的,mapstruct只会转换字段名称相同的字段。

指定默认值

@Mapper
public interface CarMapper {
    
    CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
    
    /**
     * 当目标字段值最后为null前, 设置其默认值为100
     */
    @Mapping(target = "seatCount", defaultValue = "100")
    CarDto carToCarDto(Car car);
}

常量值

@Mapper
public interface CarMapper {
    
    CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
    
    /**
     * 指定常量值
     */
    @Mapping(target = "seatCount", constant = "100")
    CarDto carToCarDto(Car car);
}

多个字段映射

@Mapper
public interface CarMapper {
    
    @Mapping(target = "length", source = "carLength")
    @Mapping(target = "seatCount", source = "numberOfSeats")
    CarDto carToCarDto(Car car);
}

@Mapper
public interface CarMapper {
    
    @Mappings(value = {
            @Mapping(target = "length", source = "carLength"),
            @Mapping(target = "seatCount", source = "numberOfSeats")
    })
    CarDto carToCarDto(Car car);
}

多级字段定位

可通过{字段名}.{字段名}.{字段名}的形式进行多级定位

public class multiLevel_field {
    
    public static void main(String[] args) {
        Car car = new Car();
        car.setColor("yellow");
        car.setNumberOfSeats(10);
        car.setPerson(new Person("张三", 28));
        
        // 输出:multiLevel_field.CarDto(color=yellow, seatCount=10, personName=张三, personAge=28)
        CarDto carDto = CarMapper.INSTANCE.carToCarDto(car);
        System.out.println(carDto);
    
        // 输出:multiLevel_field.Car(color=yellow, numberOfSeats=10, person=multiLevel_field.Person(name=张三, age=28))
        car = CarMapper.INSTANCE.carDtoToCar(carDto);
        System.out.println(car);
    }
    
    
    @Mapper
    public interface CarMapper {
        
        CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
        
        @Mapping(target = "seatCount", source = "numberOfSeats")
        @Mapping(target = "personName", source = "person.name")
        @Mapping(target = "personAge", source = "person.age")
        CarDto carToCarDto(Car c);
    
        @Mapping(target = "numberOfSeats", source = "seatCount")
        @Mapping(target = "person.name", source = "personName")
        @Mapping(target = "person.age", source = "personAge")
        Car carDtoToCar(CarDto c);
    }

    @Data
    public static class CarDto {
        
        private String color;
        
        private Integer seatCount;
        
        private String personName;
        
        private Integer personAge;
    }
    
    @Data
    public static  class Car {
        
        private String color;
        
        private Integer numberOfSeats;
        
        private Person person;
    }
    
    @Data
    @AllArgsConstructor
    public static  class Person {
        
        private String name;
        
        private Integer age;
    }
}

多参数无字段名冲突时,自动匹配

多个参数转换为一个对象时,如果这些参数中不存在相同的字段名(即:不存在参数名冲突), 那么可以不指定映射关系,转换时会自动根据字段名进行匹配

特别注意:经本人测试,多参数有字段名冲突时,转换也不一定会报错(所以,如果有多个参数的话,还是建议通过@Mapping指定映射关系

  • 对于下述转换方法generateCarDto1,如果carA类中也有personName字段的话,那么转换会以carA类中的personName字段进行,此时不会报错

    注:经本人测试,此时不论generateCarDto1的方法参数位置如何调整,无论方法参数名如何调整,都是carA类中的personName字段生效

  • 对于下述转换方法generateCarDto1,如果carA类和carB类中都有personName字段的话,那么编译时会报错

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.MappingTarget;
import org.mapstruct.factory.Mappers;

/**
 * 多个对象转换为一个对象时,如果这些参数中不存在相同的字段名(即:不存在参数名冲突),
 * 那么可以不写{@link Mapping}指定映射关系,转换时会自动根据字段名进行匹配
 *
 * <pre>
 *  特别注意:经本人测试,多参数有字段名冲突时,转换也不一定会报错。
 *          所以,如果有多个参数的话,还是建议通过@Mapping指定映射关系!
 *   1. 对于下述转换方法`generateCarDto1`,如果`carA`类中也有`personName`字段的话,
 *      那么转换会以`carA`类中的`personName`字段进行,此时不会报错
 *      注:经本人测试,此时不论`generateCarDto1`的方法参数位置如何调整,无论方法
 *          参数名如何调整,都是`carA`类中的`personName`字段生效
 *   2. 对于下述转换方法`generateCarDto1`,如果`carA`类和`carB`类中都
 *      有`personName`字段的话,那么编译时会报错
 * </pre>
 */
@SuppressWarnings("all")
public class multi_param_auto_match {
    
    public static void main(String[] args) {
        CarA carA = CarA.builder().color("红色").build();
        CarB carB = CarB.builder().seatCount(7).build();
        
        // 输出:multi_param_auto_match.CarDto(color=红色, seatCount=7, personName=张三, personAge=18)
        CarDto carDto = CarMapper.INSTANCE.generateCarDto1(carA, carB, "张三",  18);
        System.out.println(carDto);
        
        // 输出:multi_param_auto_match.CarDto(color=绿色, seatCount=8, personName=李四, personAge=24)
        carA = CarA.builder().color("绿色").build();
        carB = CarB.builder().seatCount(8).build();
        carDto = new CarDto();
        CarMapper.INSTANCE.generateCarDto2(carA, carB, "李四", 24, carDto);
        System.out.println(carDto);
    }
    
    
    @Mapper
    public interface CarMapper {
        
        CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
        
        CarDto generateCarDto1(CarA carA, CarB carB, String personName,  Integer personAge);
        
        void generateCarDto2(CarA carA, CarB carB, String personName, Integer personAge, @MappingTarget CarDto carDto);
    }


    @Data
    public static class CarDto {
        
        private String color;
        
        private Integer seatCount;
        
        private String personName;
        
        private Integer personAge;
    }
    
    @Data
    @Builder
    @AllArgsConstructor
    @NoArgsConstructor
    public static  class CarA {
        
        private String color;
    }
    
    @Data
    @Builder
    @AllArgsConstructor
    @NoArgsConstructor
    public static  class CarB {
        
        private Integer seatCount;
    }
}

多个source

通过多级定位,mapstruct可以实现多个对象转一个对象

public class multi_to_one {
    
    public static void main(String[] args) {
        Car car = new Car();
        car.setColor("green");
        car.setNumberOfSeats(10);
        Person person = new Person("张三", 28);
    
        // 输出:multi_to_one.CarDto(color=green, seatCount=10, personName=张三, personAge=28)
        CarDto carDto1 = CarMapper.INSTANCE.generateCarDto1(car, person);
        System.out.println(carDto1);
    
        // 输出:multi_to_one.CarDto(color=green, seatCount=10, personName=张三, personAge=28)
        CarDto carDto2 = new CarDto();
        CarMapper.INSTANCE.generateCarDto2(car, person, carDto2);
        System.out.println(carDto2);
    }
    
    
    @Mapper
    public interface CarMapper {
        
        CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
        
        /**
         * 因为Car和Person中所有的字段里,只有一个与CarDto#color匹配,所以这里可以省略该@Mapping。
         * 但如果有多个同事匹配时,需要指定@Mapping;否则编译时mapstruct会报错
         */
        @Mapping(target = "seatCount", source = "c.numberOfSeats")
        @Mapping(target = "personName", source = "p.name")
        @Mapping(target = "personAge", source = "p.age")
        CarDto generateCarDto1(Car c, Person p);
    
    
        /**
         * 等价于
         */
        @Mapping(target = "seatCount", source = "c.numberOfSeats")
        @Mapping(target = "personName", source = "p.name")
        @Mapping(target = "personAge", source = "p.age")
        void generateCarDto2(Car c, Person p, @MappingTarget CarDto carDto);
    }


    @Data
    public static class CarDto {
        
        private String color;
        
        private Integer seatCount;
        
        private String personName;
        
        private Integer personAge;
    }
    
    @Data
    public static  class Car {
        
        private String color;
        
        private Integer numberOfSeats;
    }
    
    @Data
    @AllArgsConstructor
    public static  class Person {
        
        private String name;
        
        private Integer age;
    }
}

字段大小写不同时

mapstruct是基于getter/setter方法读写字段,因为java getter/setter是小驼峰式命名,所以对于字段的首字母的大小写不敏感,能赋值成功,如下面的color与Color,但是对于其它位置的大小写敏感,不能赋值成功,如下面的size与siZe;对于is打头的boolean型字段,lombok生成的getter/setter是回保留原有的is的,所以mapstruct解析后girl与isGirl是不匹配的,除非你自己额外添加对应的getter/setter,如下面的boy与isBoy的getter/setter都是getBoy/setBoy。

public class case_sensitive {
    
    public static void main(String[] args) {
        Car car = new Car();
        car.setColor("yellow");
        car.setSiZe(10);
        car.setBoy(true);
        car.setIsGirl(false);
        CarDto carDto = CarMapper.INSTANCE.carToCarDto(car);
        // 输出 case_sensitive.CarDto(color=yellow, size=null, boy=true, girl=null)
        System.out.println(carDto);
    }
    
    @Mapper
    public interface CarMapper {
        
        CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
        
        CarDto carToCarDto(Car car);
    }

    @Data
    public static class CarDto {
        
        private String color;
        
        private Integer size;
        
        private Boolean boy;
    
        /** lombok生成的getter/setter 是 getGirl/setGirl */
        private Boolean girl;
    
        public Boolean getBoy() {
            return boy;
        }
    
        public void setBoy(Boolean boy) {
            this.boy = boy;
        }
    }
    
    @Data
    public static  class Car {
        
        private String Color;
        
        private Integer siZe;
    
        private Boolean isBoy;
    
        /** lombok生成的getter/setter 是 getIsGirl/setIsGirl */
        private Boolean isGirl;
        
        public Boolean getBoy() {
            return isBoy;
        }
    
        public void setBoy(Boolean boy) {
            this.isBoy = boy;
        }
    }
}

忽略字段

@Mapper
public interface CarMapper {
    
    CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
    
    /**
     * 忽略字段 num
     */
    @Mapping(target = "num", ignore = true)
    CarDto carCarDto(Car c);
}

枚举与字符串

public class enum_string {
    
    public static void main(String[] args) {
        Car car = new Car();
        car.setColor(ColorEnum.RED);
        CarDto carDto = CarMapper.INSTANCE.carToCarDto(car);
        // 输出:enum_string.CarDto(color=RED)
        System.out.println(carDto);
    
        carDto = new CarDto();
        carDto.setColor("GREEN");
        car = CarMapper.INSTANCE.carDtoToCar(carDto);
        // 输出:enum_string.Car(color=GREEN)
        System.out.println(car);
    }
    
    
    @Mapper
    public interface CarMapper {
        
        CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
        
        CarDto carToCarDto(Car car);
    
        Car carDtoToCar(CarDto car);
    }

    @Data
    public static class CarDto {
        private String color;
    }
    
    @Data
    public static  class Car {
        private ColorEnum color;
    }
    
    public static enum ColorEnum {
        RED,
        GREEN
    }
}

枚举与枚举

@ValueMapping指定不同枚举值(即:Enum#name())之间的转换

public class enum_enum {
    
    public static void main(String[] args) {
        Car car = new Car();
        car.setColor1(ColorBEnum.RED);
        car.setColor2(ColorBEnum.GREEN);
        car.setColor3(ColorBEnum.VIOLET);
        
        CarDto carDto = CarMapper.INSTANCE.carToCarDto(car);
        // 输出:enum_enum.CarDto(color1=RED, color2=GREEN, color3=PURPLE)
        System.out.println(carDto);
        
    }
    
    
    @Mapper
    public interface CarMapper {
        
        CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
        
        /**
         * - 相同的可以不指定,如这里的RED就可以不指定
         * - 不同的枚举值需要指定,如这里的 PURPLE与VIOLET
         */
        @ValueMapping(target = "RED", source = "RED")
        @ValueMapping(target = "PURPLE", source = "VIOLET")
        CarDto carToCarDto(Car car);
    }

    @Data
    public static class CarDto {
        private ColorAEnum color1;
        private ColorAEnum color2;
        private ColorAEnum color3;
    }
    
    @Data
    public static  class Car {
        private ColorBEnum color1;
        private ColorBEnum color2;
        private ColorBEnum color3;
    }
    
    public static enum ColorAEnum {
        RED,
        PURPLE,
        GREEN
    }
    public static enum ColorBEnum {
        RED,
        VIOLET,
        GREEN
    }
}

隐式类型(自动)转换

提示:这里只罗列了常见的自动转换方式,实际上mapstruct内置支持了很多自动转换,不限于jdk内的类

提示:放心大胆的用即可,若两个类型之间不支持转换,那么在项目编译时mapstruct会报错提示的

  • 基础类型 <=> 包装类型,会自动转换

    • 当 包装类型 => 基础类型 时,若包装类型为null,则在mapstruct转换时(即:程序编译时)会报错
  • 数值类型(int、Integer、long、Long等等)之间,会自动转换

    • 在大数转化为小数时,可能导致精度丢失
    • 当大的数据类型转化为小的数据类型时,若大的数据类型的值超过了小的数据类型的上限,那么可能转化失准,如:long类型的Long.MAX_VALUE值,转化为int类型时,得到的int的值为-1
  • 基础类型(包括它们的包装类型)和 String 之间,会自动转换

    • 数值 => String时,可以指定格式(格式规则同java.text.DecimalFormat),如:
    public static void main(String[] args) {
        List<String> prices = CarMapper.INSTANCE.prices1(Lists.newArrayList(1, 2, 3));
        // 输出:[$1.00, $2.00, $3.00]
        System.out.println(prices);
    
        prices = CarMapper.INSTANCE.prices2(Lists.newArrayList(1, 2, 3));
        // 输出:[1E0, 2E0, 3E0]
        System.out.println(prices);
    }
    
    
    @Mapper
    public interface CarMapper {
    
        CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
    
        @IterableMapping(numberFormat = "$#.00")
        List<String> prices1(List<Integer> prices);
    
        @IterableMapping(numberFormat = "#.##E0")
        List<String> prices2(List<Integer> prices);
    }
    
  • 枚举 <=> 字符串,会自动转换

    • enum => string,通过方法Enum#name

    • string => enum,通过方法Enum#valueOf

  • 大数据类型 <=> 基础类型(包括它们的包装类型) <=> String

    • 大数据类型包括java.math.BigIntegerjava.math.BigDecimal
    • 当 包装类型 => 基础类型 时,若包装类型为null,则在mapstruct转换时(即:程序编译时)会报错
    • 在大数转化为小数时,可能导致精度丢失
    • 当大的数据类型转化为小的数据类型时,若大的数据类型的值超过了小的数据类型的上限,那么可能转化失准,如:long类型的Long.MAX_VALUE值,转化为int类型时,得到的int的值为-1
    • 数值 => String时,可以指定格式(格式规则同java.text.DecimalFormat
  • java.util.Calendar <=> java.sql.*<=> java.util.Date <=> String <=> java.time.*

    • java.time.*包括

      • java.time.ZonedDateTime
      • java.time.LocalDateTime
      • java.time.LocalDate
      • java.time.LocalTime
      • java.time.Instant
      • java.time.Duration
      • java.time.Period
    • java.sql.*包括

      • java.sql.Time
      • java.sql.Date
      • java.sql.Timestamp
    • 指定格式的方式,同java.text.SimpleDateFormat

      public static void main(String[] args) {
          List<String> prices = CarMapper.INSTANCE.convert(Lists.newArrayList(new Date()));
          // 输出:[2022-03-28]
          System.out.println(prices);
      }
      

      @Mapper
      public interface CarMapper {

      CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
      
      @IterableMapping(dateFormat = "yyyy-MM-dd")
      List<String> convert(List<Date> dates);
      

      }

      
      
  • java.util.Currency <=> String

    货币 <=> 字符串

  • list <=> set、set <=> set、list <=> list等集合之间可进行相互转换

    • 可相互转换的集合有
    接口类型mapstruct选择作为实现的类
    IterableArrayList
    CollectionArrayList
    ListArrayList
    SetHashSet
    SortedSetTreeSet
    NavigableSetTreeSet
    MapHashMap
    SortedMapTreeMap
    NavigableMapTreeMap
    ConcurrentMapConcurrentHashMap
    ConcurrentNavigableMapConcurrentSkipListMap
    • 简单示例list <=> set
    public static void main(String[] args) {
        // 输出:[1, 2, 3]
        System.out.println(CarMapper.INSTANCE.convert1(Sets.newHashSet(1, 2, 3)));
        // 输出:[1, 2, 3]
        System.out.println(CarMapper.INSTANCE.convert2(Lists.newArrayList(1, 2, 3, 1)));
    }
    
    
    @Mapper
    public interface CarMapper {
    
        CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
    
        List<String> convert1(Set<Integer> items);
    
        Set<String> convert2(List<Integer> items);
    }
    
  • 更多详见官网

复用spring-bean进行转换支持

@Mapperuses可以指定复用spring容器中已有的spring-bean。

@SpringBootApplication
@SuppressWarnings("all")
public class use_exist_mapper implements ApplicationRunner {
    
    public static void main(String[] args) {
        SpringApplication.run(use_exist_mapper.class, args);
    }
    
    @Autowired
    CarMapper carMapper;
    
    @Override
    public void run(ApplicationArguments args) throws Exception {
        Car car = new Car();
        car.setColor("yellow");
        car.setNumberOfSeats(10);
        car.setPersonList(Lists.newArrayList(new Person(true), new Person(false)));
        CarDto carDto = carMapper.carToCarDto1(car);
        // 输出:use_exist_mapper.CarDto(color=yellow, seatCount=10, personList=[use_exist_mapper.PersonDto(isBoy=true), use_exist_mapper.PersonDto(isBoy=false)])
        System.out.println(carDto);
    
        carDto = carMapper.carToCarDto2(car);
        // 输出:use_exist_mapper.CarDto(color=yellow, seatCount=10, personList=[use_exist_mapper.PersonDto(isBoy=true), use_exist_mapper.PersonDto(isBoy=false)])
        System.out.println(carDto);
    }
    
    @Mapper(componentModel = "spring", uses = {StringMapperA.class})
    public interface CarMapper {
        
        /**
         * 方式一:直接默认使用匹配到的StringMapperA#personToPersonDto方法进行Person => PersonDto 的转换
         */
        @Mapping(target = "seatCount", source = "numberOfSeats")
        CarDto carToCarDto1(Car car);
    
        /**
         * 方式二:直接指定转换方法.
         * <br/>
         * 注:此方式部分ban本又bug,目前可能不够通用。
         */
        @Mapping(target = "seatCount", source = "numberOfSeats")
        @Mapping(target = "personList", expression = "java(stringMapperA.xyz(car123.getPersonList()))")
        CarDto carToCarDto2(Car car123);
    }
    
    @Component
    public static class StringMapperA {
        
        public PersonDto personToPersonDto(Person person) {
            return new PersonDto(person.getBoy());
        }
        
        public List<PersonDto> xyz(List<Person> list) {
            return list.stream().map(p -> new PersonDto(p.getBoy())).collect(Collectors.toList());
        }
    }
    
    @Data
    public static class CarDto {
        private String color;
        private Integer seatCount;
        private List<PersonDto> personList;
    }
    
    @Data
    public static class Car {
        private String color;
        private Integer numberOfSeats;
        private List<Person> personList;
    }
    
    @Data
    @AllArgsConstructor
    public static class PersonDto {
        private Boolean isBoy;
    }
    
    @Data
    @AllArgsConstructor
    public static class Person {
        private Boolean boy;
    }
}

自定义字段转换逻辑 - expression指定静态方法

格式为:

@Mapping(target = "目标字段名", expression = "java({全类名}.{静态方法名}({形参名}.{getter方法})")
public class expression_point_static_method {
    
    public static void main(String[] args) {
        Car car = new Car();
        car.setColor("yellow");
        car.setNumberOfSeats(1);
        CarDto carDto = CarMapper.INSTANCE.carToCarDto(car);
        // 输出:expression_point_static_method.CarDto(color=yellow, seatCount=2)
        System.out.println(carDto);
    }
    
    
    @Mapper
    public static abstract class CarMapper {
        
        public static final CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
        
        /** {@link CarMapper}全类名 */
        public static final String CLASS_LONG_NAME = "com.example.mapstruct.obtain_instance_test.expression_point_static_method.CarMapper";
        
        @Mapping(target = "seatCount",
                expression = "java(" + CLASS_LONG_NAME + ".addOne(car123.getNumberOfSeats()))")
        public abstract CarDto carToCarDto(Car car123);
        
        public static Integer addOne(Integer a) {
            return ++a;
        }
    }
    
    @Data
    public static class CarDto {
        
        private String color;
        
        private Integer seatCount;
    }
    
    @Data
    public static  class Car {
        
        private String color;
        
        private Integer numberOfSeats;
    }
}

优化: 直接写全类名,后期不好维护,可以采用@Mapper的import能力,这样一来,expression中只需要写简类名即可,如:

@Mapper(imports = {CarMapper2.class})
public static abstract class CarMapper2 {

    public static final CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);

    @Mapping(target = "seatCount",
             expression = "java(CarMapper2.addOne(car123.getNumberOfSeats()))")
    public abstract CarDto carToCarDto(Car car123);

    public static Integer addOne(Integer a) {
        return ++a;
    }
}

自定义字段转换逻辑 - expression指定spring-bean的实例方法

提示:mapstruct的@Mapper(uses = {xxx.class})也能达到注入xxx依赖的效果,但是部分版本有bug,使用不稳定,所以可以直接利用下面抽象类的方式使用

@SpringBootApplication
@SuppressWarnings("all")
public class expression_point_instance_method implements ApplicationRunner {
    
    public static void main(String[] args) {
        SpringApplication.run(expression_point_instance_method.class, args);
    }
    
    @Autowired
    CarMapper123 carMapper123;
    
    @Override
    public void run(ApplicationArguments args) throws Exception {
        Car car = new Car();
        car.setColor("yellow");
        car.setNumberOfSeats(10);
        car.setPersonList(Lists.newArrayList(new Person(true), new Person(false)));
        
        CarDto carDto = carMapper123.carToCarDto2(car);
        // 输出:expression_point_instance_method.CarDto(color=yellow, seatCount=10, personList=[expression_point_instance_method.PersonDto(isBoy=true), expression_point_instance_method.PersonDto(isBoy=false)])
        System.out.println(carDto);
    }
    
    @Mapper(componentModel = "spring")
    public static abstract class CarMapper123 {

        @Autowired
        protected StringMapperB stringMapperB;
        
        /**
         * 直接指定转换方法
         * <br/>
         * 注:如果调用的xyz是this里面的方法,那么还可以简写:<code>@Mapping(target = "personList", expression = "java(xyz(car123.getPersonList()))")</code>
         */
        @Mapping(target = "seatCount", source = "numberOfSeats")
        @Mapping(target = "personList", expression = "java(stringMapperB.xyz(car123.getPersonList()))")
        public abstract CarDto carToCarDto2(Car car123);
    }
    
    @Component
    public static class StringMapperB {
        
        public List<PersonDto> xyz(List<Person> list) {
            return list.stream().map(p -> new PersonDto(p.getBoy())).collect(Collectors.toList());
        }
    }
    
    @Data
    public static class CarDto {
        private String color;
        private Integer seatCount;
        private List<PersonDto> personList;
    }
    
    @Data
    public static class Car {
        private String color;
        private Integer numberOfSeats;
        private List<Person> personList;
    }
    
    @Data
    @AllArgsConstructor
    public static class PersonDto {
        private Boolean isBoy;
    }
    
    @Data
    @AllArgsConstructor
    public static class Person {
        private Boolean boy;
    }
}

自定义字段转换逻辑 - 提供已存在的转换方法

当mapstruct要进行Type1 => Type2转换时,首先会先去寻找是否已存在现成的Type1 => Type2的方法,如果已存在,那么直接利用该方法进行对应类型的转换;如果不存在,则自动生成。

判断一个方法是否是Type1 => Type2的转换方法,只需满足:

  1. 该方法的入参类型要为Type1,出参类型要为Type2
  2. 该方法的方法名.equalsIgnoreCase(“Type1ToType2”)

而提供已存在的转换方法又可分为两种:

  • 第一种:已存在的其他mapper方法(由mapstruct生成)
  • 第二种:已存在的自己写的mapper方法(由程序员自己写)
public class handWritten_mapping_logic {
    public static void main(String[] args) {
        Car car = new Car();
        car.setPersonList(Lists.newArrayList(new Person(true), new Person(false)));
        CarDto carDto = CarMapper.INSTANCE.carToCarDto(car);
        // {"color":"yellow","personList":[{"isBoy":true},{"isBoy":false}]}
        System.out.println(JSON.toJSONString(carDto));
        
        carDto = CarMapper2.INSTANCE.carToCarDto(car);
        // {"color":"yellow","personList":[{"isBoy":true},{"isBoy":false}]}
        System.out.println(JSON.toJSONString(carDto));
    }
    
    
    @Mapper
    public interface CarMapper {
        
        CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
        
        /**
         * mapstruct推导过程:
         * 1. Car中的List<Person> => CarDto中的List<PersonDto>
         * 2. 识别泛型,即:Person => PersonDto
         * 3. 发现存在方法 PersonDto personToCarPersonDto(Person person), 则直接调用该方法进行Person => PersonDto
         */
        CarDto carToCarDto(Car car);
    
        /**
         * 示例  第一种:已存在的其他mapper方法(由mapstruct生成)
         */
        @Mapping(source = "boy", target = "isBoy")
        PersonDto personToPersonDto1(Person person);
    }
    
    
    @Mapper
    public interface CarMapper2 {
    
        CarMapper2 INSTANCE = Mappers.getMapper(CarMapper2.class);
        
        CarDto carToCarDto(Car car);
    
        /**
         * 示例  第二种:已存在的自己写的mapper方法(由程序员自己写)
         */
        default PersonDto personToPersonDto1(Person person) {
            return new PersonDto(person.getBoy());
        }
    }
    
    @Data
    public static class CarDto {
        private List<PersonDto> personList;
    }
    
    @Data
    public static  class Car {
        private List<Person> personList;
    }
    
    @Data
    @AllArgsConstructor
    public static class PersonDto {
        private Boolean isBoy;
    }
    
    @Data
    @AllArgsConstructor
    public static  class Person {
        private Boolean boy;
    }
}

mapstruct入门知识学习完毕 !


相关资料

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐