• 彻底干掉 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.3mapstruct-plus.version>
    properties>
    <dependencies>
        <dependency>
            <groupId>io.github.linpeiliegroupId>
            <artifactId>mapstruct-plus-spring-boot-starterartifactId>
            <version>${mapstruct-plus.version}version>
        dependency>
    dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.pluginsgroupId>
                <artifactId>maven-compiler-pluginartifactId>
                <version>3.8.1version>
                <configuration>
                    <source>1.8source>
                    <target>1.8target>
                    <annotationProcessorPaths>
                        <path>
                            <groupId>io.github.linpeiliegroupId>
                            <artifactId>mapstruct-plus-processorartifactId>
                            <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 转换为当前类的接口。同时,还支持 map 中嵌套 Map 转换为自定义类嵌套自定义类的场景。

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

    • String
    • BigDecimal
    • BigInteger
    • Integer
    • Long
    • Double
    • Number
    • Boolean
    • Date
    • LocalDateTime
    • LocalDate
    • LocalTime
    • URI
    • URL
    • 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 mapModel1 = new HashMap<>();
        mapModel1.put("str", "1jkf1ijkj3f");
        mapModel1.put("i1", 111);
        mapModel1.put("l2", 11231);
    
        Map 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 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 的属性,想要转换为用 , 分隔的字符串时,可以通过该配置,来执行转换逻辑:

    @AutoMapper(target = UserDto.class)
    public class User {
    
        @AutoMapping(target = "educations", expression = "java(java.lang.String.join(",", source.getEducationList()))")
        private List 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 说再见了。

  • 相关阅读:
    UNIX环境高级编程 学习笔记 第二十章 数据库函数库
    Python 操作 lxml库与Xpath(爬取网页指定内容)
    Selenium自动化测试(基于Java)
    个人开发的渗透测试工具Satania v1.2更新
    基础课3——自然语言处理的应用
    Java通过报表技术POI实现数据可视化
    工业智能仓储货架|HEGERLS供应电动移动式货架Electric mobile shelf自动立体化仓库货架
    【Java万花筒】服务网格:微服务世界的交通管制中心
    nginx连接前后端分离项目 或 负载均衡映射多个服务器
    智能网联汽车云控系统第5部分:平台服务场景规范
  • 原文地址:https://www.cnblogs.com/linpeilie/p/17181933.html