java
主页 > 软件编程 > java >

SpringBoot中MapStruct实现优雅的数据复制

2024-09-01 | 佚名 | 点击:

你是否在做项目时遇到过以下情况:

1.为什么选择MapStruct

1.1.常见的属性映射方法

一般来说,不使用MapStruct框架进行属性映射,常有的方法以下两种:

这种方法最朴素,手动编写代码将源对象的属性存入目标对象,需要注意实体类中嵌套属性的判空操作以防止空指针异常。

BeanUtils底层使用的是反射机制实现属性的映射。反射是一种在运行时动态获取类信息、调用方法或访问字段的机制,无法利用JVM的优化机制,因此通常比直接方法调用慢得多。

此外,BeanUtils 只能同属性映射,或者在属性相同的情况下,允许被映射的对象属性少;但当遇到被映射的属性数据类型被修改或者被映射的字段名被修改,则会导致映射失败。

1.2.MapStruct的优势

MapStruct是一个基于注解的Java代码生成器,它通过分析带有@Mapper注解的接口,在编译时自动生成实现该接口的映射器类。这个映射器类包含了用于执行对象之间映射的具体代码。

与常规方法相比,MapStruct具备的优势有:

有开发者对比过两者的性能差距,如下表。这充分体现了MapStruct性能的强大。

对象转换次数 属性个数 BeanUtils耗时 MapStruct耗时
5千万次 6 14秒 1秒
5千万次 15 36秒 1秒
5千万次 25 55秒 1秒

2.MapStruct快速入门

在快速入门中,我们的任务是将dto的数据复制到实体类中。

2.1.导入Maven依赖

1

2

3

4

5

6

7

8

9

10

11

12

<!-- https://mvnrepository.com/artifact/org.mapstruct/mapstruct -->

<dependency>

    <groupId>org.mapstruct</groupId>

    <artifactId>mapstruct</artifactId>

    <version>1.4.2.Final</version>

</dependency>

<!-- https://mvnrepository.com/artifact/org.mapstruct/mapstruct-processor -->

<dependency>

    <groupId>org.mapstruct</groupId>

    <artifactId>mapstruct-processor</artifactId>

    <version>1.4.2.Final</version>

</dependency>

2.2.创建相关对象

注意,实体类要具有get/set方法,这里我使用了lombok的@Data注解来实现。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

import lombok.Data;

 

/**

 * @author modox

 * @date 2024/5/5

 */

@Data

public class Hotel {

    // 酒店名称

    private String hotelName;

 

    // 酒店地址

    private String hotelAddress;

 

    // 所在城市

    private String hotelCity;

 

    // 联系电话

    private String hotelPhone;

}

dto类我使用了@Builder注解,可以快速为对象赋初始值。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

import lombok.Builder;

import lombok.Data;

 

/**

 * @author modox

 * @date 2024/5/5

 */

@Data

@Builder

public class HotelDTO {

    // 酒店名称

    private String name;

 

    // 酒店地址

    private String address;

 

    // 所在城市

    private String city;

}

2.3.创建转换器Converter

使用抽象类来定义转换器,只需中@Mapping注解中填写target和source的字段名,即可实现属性复制。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

import org.mapstruct.Mapper;

import org.mapstruct.Mapping;

import org.mapstruct.Mappings;

 

/**

 * @author modox

 * @date 2024/5/5

 */

@Mapper(componentModel = "spring")

public abstract class TestConverter {

    //酒店详情

    @Mappings({

            @Mapping(target = "hotelName", source = "name"),

            @Mapping(target = "hotelAddress", source = "address"),

            @Mapping(target = "hotelCity", source = "city"),

    })

    public abstract Hotel dto2Hotel(HotelDTO hotelDTO);

}

2.4.测试

在SpringBoot的测试类中测试,这里我使用DTO类的@Builder注解提供的方法为dto赋初值模拟实际开发,通过调用converter的方法实现属性映射。

1

2

3

4

5

6

7

8

9

10

11

12

13

@Test

  public void Test() {

      HotelDTO build = HotelDTO.builder()

              .name("五星级酒店")

              .address("中国")

              .city("北京").build();

 

      TestConverter converter = new TestConverterImpl();

 

      Hotel hotel = converter.dto2Hotel(build);

 

      System.out.println(hotel);

  }

结果如图:

最后,我们可以发现在target包的converter的相同目录下,生成了TestConverter的实现类

里面为我们编写好了映射的代码。

3.MapStruct进阶操作

如果仅是这种简单层级的对象映射,还不足以体现MapStruct的灵活性。下面将介绍MapStruct的进阶技巧。

3.1.嵌套映射

假设我们的Hotel实体类中嵌套了另外一个实体类Master

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

@Data

public class Hotel {

    // 酒店名称

    private String hotelName;

 

    // 酒店地址

    private String hotelAddress;

 

    // 所在城市

    private String hotelCity;

 

    // 联系电话

    private String hotelPhone;

 

    private Master master;

 

    @Data

    @NoArgsConstructor

    @AllArgsConstructor

    public static class Master {

        private String personName;

 

        private Integer personAge;

    }

}

dto对象为:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

@Data

@Builder

public class HotelDTO {

    // 酒店名称

    private String name;

 

    // 酒店地址

    private String address;

 

    // 所在城市

    private String city;

 

    private String personName;

 

    private Integer personAge;

}

我们需要把personName和personAge映射到Hotel实体类的Master中,怎么做?

很简单,只需要在target属性中加上Hotel实体类嵌套实体类的字段名,加字符.,再跟上嵌套类的字段名即可

1

2

3

4

5

6

7

8

9

//酒店详情

@Mappings({

        @Mapping(target = "hotelName", source = "name"),

        @Mapping(target = "hotelAddress", source = "address"),

        @Mapping(target = "hotelCity", source = "city"),

        @Mapping(target = "master.personName", source = "personName"),

        @Mapping(target = "master.personAge", source = "personAge"),

})

public abstract Hotel dto2Hotel(HotelDTO hotelDTO);

结果如图:

3.2.集合映射

如果源对象和目标对象的集合的元素类型都是基本数据类型,直接在target和source中填写字段名即可。

若源对象和目标对象的集合元素类型不同,怎么做?

这个案例我们需要把DTO的personList映射到masterList中。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

@Data

@Builder

public class HotelDTO {

    // 酒店名称

    private String name;

 

    // 酒店地址

    private String address;

 

    private List<HotelDTO.Person> personList;

 

    @Data

    @NoArgsConstructor

    @AllArgsConstructor

    public static class Person {

        private String personName;

 

        private Integer personAge;

    }

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

@Data

public class Hotel {

    // 酒店名称

    private String hotelName;

 

    // 酒店地址

    private String hotelAddress;

 

    private List<Master> masters;

 

    @Data

    @NoArgsConstructor

    @AllArgsConstructor

    public static class Master {

        private String name;

 

        private Integer age;

    }

}

编写converter,这次需要进行两层映射。

第一层将person集合映射到master集合上。

第二层将person对象的属性映射到master对象中。

1

2

3

4

5

6

7

8

9

10

11

12

13

// 酒店详情

@Mappings({

        @Mapping(target = "hotelName", source = "name"),

        @Mapping(target = "hotelAddress", source = "address"),

        @Mapping(target = "masters", source = "personList")

})

public abstract Hotel dto2Hotel(HotelDTO hotelDTO);

 

@Mappings({

        @Mapping(target = "name", source = "personName"),

        @Mapping(target = "age", source = "personAge"),

})

public abstract Hotel.Master toList(HotelDTO.Person person);

结果如图:

查看target包下的代码,可以发现MapStruct除了两层映射外,还帮你自动生成了迭代集合添加元素的代码,从而实现集合元素的复制。

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

@Component

public class TestConverterImpl extends TestConverter {

    public TestConverterImpl() {

    }

        // 第一层映射

    public Hotel dto2Hotel(HotelDTO hotelDTO) {

        if (hotelDTO == null) {

            return null;

        } else {

            Hotel hotel = new Hotel();

            hotel.setMasters(this.personListToMasterList(hotelDTO.getPersonList()));

            hotel.setHotelAddress(hotelDTO.getAddress());

            hotel.setHotelName(hotelDTO.getName());

            return hotel;

        }

    }

 

    // 第二层映射

    public Hotel.Master toList(HotelDTO.Person person) {

        if (person == null) {

            return null;

        } else {

            Hotel.Master master = new Hotel.Master();

            master.setName(person.getPersonName());

            master.setAge(person.getPersonAge());

            return master;

        }

    }

 

    // 调用第二层映射,将person集合的元素添加到master中

    protected List<Hotel.Master> personListToMasterList(List<HotelDTO.Person> list) {

        if (list == null) {

            return null;

        } else {

            List<Hotel.Master> list1 = new ArrayList(list.size());

            Iterator var3 = list.iterator();

 

            while(var3.hasNext()) {

                HotelDTO.Person person = (HotelDTO.Person)var3.next();

                list1.add(this.toList(person));

            }

 

            return list1;

        }

    }

}

4.字段的逻辑处理

4.1.复杂逻辑处理(qualifiedByName和@Named)

这次我们需要把dto中的personName和personAge的list集合映射到实体类的masters集合中。常规的集合映射无法处理这种情况,这时需要使用到qualifiedByName和@Named进行特殊处理。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

@Data

@Builder

public class HotelDTO {

    // 酒店名称

    private String name;

 

    // 酒店地址

    private String address;

 

    private List<String> personName;

 

    private List<Integer> personAge;

 

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

@Data

public class Hotel {

    // 酒店名称

    private String hotelName;

 

    // 酒店地址

    private String hotelAddress;

 

    // 主人

    private List<Master> masters;

 

    @Data

    @NoArgsConstructor

    @AllArgsConstructor

    public static class Master {

        private String personName;

 

        private Integer personAge;

    }

}

这就需要拿到两个list的数据,进行手动处理了。在@Mapping注解的qualifiedByName属性指定方法名定位处理逻辑的方法,@Named(“dtoToMasters”)。

利用stream流进行处理。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

// 酒店详情

@Mappings({

        @Mapping(target = "hotelName", source = "name"),

        @Mapping(target = "hotelAddress", source = "address"),

        @Mapping(target = "masters", source = "hotelDTO", qualifiedByName = "dtoToMasters")

})

public abstract Hotel dto2Hotel(HotelDTO hotelDTO);

 

@Named("dtoToMasters")

List<Hotel.Master> dtoToMasters(HotelDTO hotelDTO) {

    List<String> personNames = hotelDTO.getPersonName();

    List<Integer> personAges = hotelDTO.getPersonAge();

 

    if (personNames != null && personAges != null && personNames.size() == personAges.size()) {

        return IntStream.range(0, personNames.size())

                .mapToObj(i -> new Hotel.Master(personNames.get(i), personAges.get(i)))

                .collect(Collectors.toList());

    }

 

    // 如果列表长度不匹配或其他错误情况,可以返回空列表或抛出异常

    return Collections.emptyList();

}

返回结果:

4.2.额外逻辑处理(ignore和@AfterMapping)

@Mappings的ignore属性,也可以对一个字段(不能是集合)进行额外逻辑处理。通常搭配@AfterMapping注解使用。

这个案例中,我们需要根据DTO的mount属性判断是否大于15,如果大于,则判断hotel实体类的isSuccess为true

1

2

3

4

5

6

7

8

9

10

11

@Data

public class Hotel {

    // 酒店名称

    private String hotelName;

 

    // 酒店地址

    private String hotelAddress;

 

    // 酒店生意是否兴隆

    private Boolean isSuccess;

}

1

2

3

4

5

6

7

8

9

10

11

@Data

@Builder

public class HotelDTO {

    // 酒店名称

    private String name;

 

    // 酒店地址

    private String address;

 

    private Integer mount;

}

编写converter,注意@AfterMapping注解下的方法的参数列表,需要使用@MappingTarget注解指明目标对象,

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

// 酒店详情

@Mappings({

        @Mapping(target = "hotelName", source = "name"),

        @Mapping(target = "hotelAddress", source = "address"),

        @Mapping(target = "isSuccess", ignore = true)

})

public abstract Hotel dto2Hotel(HotelDTO hotelDTO);

 

@AfterMapping

void isSuccess(HotelDTO hotelDTO, @MappingTarget Hotel hotel) {

    if (hotelDTO.getMount() == null) {

        return;

    }

    boolean b = hotelDTO.getMount() > 15;

    hotel.setIsSuccess(b);

}

测试方法

1

2

3

4

5

6

7

8

9

10

11

12

13

@Test

public void Test() {

    HotelDTO build = HotelDTO.builder()

            .name("五星级酒店")

            .address("中国")

            .mount(18).build();

 

    TestConverter converter = new TestConverterImpl();

 

    Hotel hotel = converter.dto2Hotel(build);

 

    System.out.println(hotel);

}

返回结果

4.3.简单逻辑处理(expression)

expression可以在注解中编写简单的处理逻辑

在这个案例中我需要在实体类的nowTime字段获取当前时间。

1

2

3

4

5

6

7

8

9

10

@Data

public class Hotel {

    // 酒店名称

    private String hotelName;

 

    // 酒店地址

    private String hotelAddress;

 

    private LocalDateTime nowTime;

}

直接在expression属性中使用方法获取当前时间。

1

2

3

4

5

6

7

// 酒店详情

@Mappings({

        @Mapping(target = "hotelName", source = "name"),

        @Mapping(target = "hotelAddress", source = "address"),

        @Mapping(expression = "java(java.time.LocalDateTime.now())", target = "nowTime")

})

public abstract Hotel dto2Hotel(HotelDTO hotelDTO);

结果如下

原文链接:
相关文章
最新更新