3

彻底干掉 BeanUtils,最优雅的 Mapstruct 增强工具全新出炉 - 林培烈

 1 year ago
source link: https://www.cnblogs.com/linpeilie/p/17181933.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

彻底干掉 BeanUtils,最优雅的 Mapstruct 增强工具全新出炉

在现在流行的系统设计中,一般会将对象模型划分为多个层次,例如 VO、DTO、PO、BO 等等。这同时也产生了一个问题,经常需要进行不同层级的模型之间相互转换。

针对这种问题,目前常会采用三种方案:

  1. 调用每个字段的 getter/setter 进行赋值。这个过程,枯燥且乏味,容易出错的同时,极易容易造成代码行数迅速膨胀,可阅读性差。
  2. apache-commons、Spring 等提供的 BeanUtil 工具类,这种工具类使用非常方便,一行代码即可实现映射。但其内部采用反射的方式来实现映射,性能低下,出现问题时,调试困难,当需要个性化转换时,配置麻烦,非常不建议使用,特别是对于性能要求比较高的程序中。
  3. mapstruct:这个框架是基于 Java 注释处理器,定义一个转换接口,在编译的时候,会根据接口类和方法相关的注解,自动生成转换实现类。生成的转换逻辑,是基于 getter/setter 方法的,所以不会像 BeanUtil 等消耗其性能。

上面的三种方法中,最优秀的莫属 mapstruct 了,当然,美中不足的就是,当系统较为复杂,对象较多且结构复杂,又或者有的项目设计中会定义多层对象模型(如 DDD 领域设计),需要定义较多的转换接口和转换方法,这也是一些开发者放弃 Mapstruct 的主要原因。

这里,就要给大家介绍一个 Mapstruct 的增强包 —— Mapstruct Plus,一个注解,可以生成两个类之间的转换接口,使 Java 类型转换更加便捷、优雅,彻底抛弃 BeanUtils。

下面演示如何使用 MapStruct Plus 来映射两个对象。

假设有两个类 UserDtoUser,分别表示数据层对象和业务层对象:

  • UserDto
public class UserDto {
    private String username;
    private int age;
    private boolean young;

    // getter、setter、toString、equals、hashCode
}
  • User
public class User {
    private String username;
    private int age;
    private boolean young;

    // getter、setter、toString、equals、hashCode
}

引入 mapstruct-plus-spring-boot-starter 依赖:

<properties>
    <mapstruct-plus.version>1.1.3</mapstruct-plus.version>
</properties>
<dependencies>
    <dependency>
        <groupId>io.github.linpeilie</groupId>
        <artifactId>mapstruct-plus-spring-boot-starter</artifactId>
        <version>${mapstruct-plus.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>
                        <groupId>io.github.linpeilie</groupId>
                        <artifactId>mapstruct-plus-processor</artifactId>
                        <version>${mapstruct-plus.version}</version>
                    </path>
                </annotationProcessorPaths>
            </configuration>
        </plugin>
    </plugins>
</build>

指定对象映射关系

User 或者 UserDto 上面增加注解 —— @AutoMapper,并设置 targetType 为对方类。

@AutoMapper(target = UserDto.class)
public class User {
    // ...
}
@SpringBootTest
public class QuickStartTest {

    @Autowired
    private Converter converter;

    @Test
    public void test() {
        User user = new User();
        user.setUsername("jack");
        user.setAge(23);
        user.setYoung(false);

        UserDto userDto = converter.convert(user, UserDto.class);
        System.out.println(userDto);    // UserDto{username='jack', age=23, young=false}

        assert user.getUsername().equals(userDto.getUsername());
        assert user.getAge() == userDto.getAge();
        assert user.isYoung() == userDto.isYoung();

        User newUser = converter.convert(userDto, User.class);

        System.out.println(newUser);    // User{username='jack', age=23, young=false}

        assert user.getUsername().equals(newUser.getUsername());
        assert user.getAge() == newUser.getAge();
        assert user.isYoung() == newUser.isYoung();
    }

}

引入依赖后,使用 Mapstruct Plus 步骤非常简单。

  1. 给需要转换的类添加 AutoMapper 注解
  2. 获取 Converter 实例,调用 convert 方法即可

完全兼容 Mapstruct

Mapst实现了增强操作,如果之前已经使用了 Mapstruct,可以直接替换相关依赖。

单注解即可实现两个对象相互转换

例如快速开始中,只在 User 类上面增加注解 @AutoMapper,Mapstruct Plus 除了会生成 User -> UserDto 的转换接口,默认还会生成 UserDto -> User 的转换接口。

编译后,可以查看生成的类,如下:

image.png

自定义对象类型的属性自动转换

当两个需要转换的对象 AADto,其中的属性 BBDto 是自定义类型,并且也定义了 @AutoMapper 注解的话,在生成转换 AADto 时,会自动依赖属性 BBDto 的转换接口,实现其相应的属性转换。

例如:
分别有两组对象模型:汽车(Car) 和座椅配置(SeatConfiguration),其中 Car 依赖 SeatConfiguration。

两组对象结构一致,只不过代表层级不一样,这里拿 dto 层的对象定义举例:

  • CarDto
@AutoMapper(target = Car.class)
public class CarDto {
    private SeatConfigurationDto seatConfiguration;
}
  • SeatConfiguration
@AutoMapper(target = SeatConfiguration.class)
public class SeatConfigurationDto {
    private int seatCount;
}
@Test
public void carConvertTest() {
    CarDto carDto = new CarDto();
    SeatConfigurationDto seatConfigurationDto = new SeatConfigurationDto();
    seatConfigurationDto.setSeatCount(4);
    carDto.setSeatConfiguration(seatConfigurationDto);

    final Car car = converter.convert(carDto, Car.class);
    System.out.println(car);    // Car(seatConfiguration=SeatConfiguration(seatCount=4))

    assert car.getSeatConfiguration() != null;
    assert car.getSeatConfiguration().getSeatCount() == 4;
}

单个对象对多个对象进行转换

Mapstruct Plus 提供了 @AutoMappers 注解,支持配置多个目标对象,进行转换。

例如:有三个对象,UserUserDtoUserVOUser 分别要和其他两个对象进行转换。则可以按如下配置:

@Data
@AutoMappers({
    @AutoMapper(target = UserDto.class),
    @AutoMapper(target = UserVO.class)
})
public class User {
    // ...
}

Map 转对象

Mapstruct Plus 提供了 @AutoMapMapper 注解,支持生成 Map<String, Object> 转换为当前类的接口。同时,还支持 map 中嵌套 Map<String, Object> 转换为自定义类嵌套自定义类的场景。

其中,map 中的 value 支持的类型如下:

  • String
  • BigDecimal
  • BigInteger
  • Integer
  • Double
  • Number
  • Boolean
  • LocalDateTime
  • LocalDate
  • LocalTime
  • Calendar
  • Currency
  • 自定义类(自定义类也需要增加 @AutoMapMapper 注解

有如下两个接口:

  • MapModelA
@Data
@AutoMapMapper
public class MapModelA {

    private String str;
    private int i1;
    private Long l2;
    private MapModelB mapModelB;

}
  • MapModelB
@Data
@AutoMapMapper
public class MapModelB {

    private Date date;
    
}
@Test
public void test() {
    Map<String, Object> mapModel1 = new HashMap<>();
    mapModel1.put("str", "1jkf1ijkj3f");
    mapModel1.put("i1", 111);
    mapModel1.put("l2", 11231);

    Map<String, Object> mapModel2 = new HashMap<>();
    mapModel2.put("date", DateUtil.parse("2023-02-23 01:03:23"));

    mapModel1.put("mapModelB", mapModel2);

    final MapModelA mapModelA = converter.convert(mapModel1, MapModelA.class);
    System.out.println(mapModelA);  // MapModelA(str=1jkf1ijkj3f, i1=111, l2=11231, mapModelB=MapModelB(date=2023-02-23 01:03:23))
}

支持自定义转换

自定义属性转换

Mapstruct Plus 提供了 @AutoMapping 注解,该注解在编译后,会变为 Mapstruct 中的 @Mapping 注解,已经实现了几个常用的注解属性。

指定不同名称属性字段映射转换

例如,Car 类中属性 wheels 属性,转换 CarDto 类型时,需要将该字段映射到 wheelList 上面,可以在 wheels 上面增加如下注解:

@AutoMapper(target = CarDto.class)
public class Car {
    @AutoMapping(target = "wheelList")
    private List<String> wheels;
}

自定义时间格式化

在将 Date 类型的属性,转换为 String 类型时,可以通过 dateFormat 来指定时间格式化:

@AutoMapper(target = Goods.class)
public class GoodsDto {

    @AutoMapping(target = "takeDownTime", dateFormat = "yyyy-MM-dd HH:mm:ss")
    private Date takeDownTime;

}

自定义数字格式化

当数字类型(doublelongBigDecimal)转换为 String 类型时,可以通过 numberFormat 指定 java.text.DecimalFormat 所支持的格式:

@AutoMapper(target = Goods.class)
public class GoodsDto {

    @AutoMapping(target = "price", numberFormat = "$#.00")
    private int price;

}

自定义 Java 表达式

@AutoMapping 提供了 expression 属性,支持配置一段可执行的 Java 代码,来执行具体的转换逻辑。

例如,当一个 List<String> 的属性,想要转换为用 , 分隔的字符串时,可以通过该配置,来执行转换逻辑:

@AutoMapper(target = UserDto.class)
public class User {

    @AutoMapping(target = "educations", expression = "java(java.lang.String.join(",", source.getEducationList()))")
    private List<String> educationList;

}

自定义类型转换器

@AutoMapping 注解提供了 uses 属性,引入自定义的类型转换器,来提供给当前类转换时使用。

实例场景:

项目中会有字符串用 , 分隔,在一些类中,需要根据逗号拆分为字符串集合。针对于这种场景,可以有两种方式:首先可以指定字段映射时的表达式,但需要对每种该情况的字段,都添加表达式,复杂且容易出错。

第二,就可以自定义一个类型转换器,通过 uses 来使用

public interface StringToListString {
    default List<String> stringToListString(String str) {
        return StrUtil.split(str);
    }
}

@AutoMapper(target = User.class, uses = StringToListStringConverter.class)
public class UserDto {

    private String username;
    private int age;
    private boolean young;
    @AutoMapping(target = "educationList")
    private String educations;
    // ......
}

@SpringBootTest
public class QuickStartTest {

    @Autowired
    private Converter converter;

    @Test
    public void ueseTest() {
        UserDto userDto = new UserDto();
        userDto.setEducations("1,2,3");

        final User user = converter.convert(userDto, User.class);
        System.out.println(user.getEducationList());    // [1, 2, 3]

        assert user.getEducationList().size() == 3;
    }
}

更多特性,可以查看官方文档

Mapstruct Plus 相对于 Mapstruct 来说,继承了其高性能的特点,同时增强了其便携性和快速开发的特性,在系统模型设计较好(属性及类型基本一致)的情况下,开发成本极低,是时候和 BeanUtils 说再见了。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK