一、背景
在代码开发中,我们通常都会使用分层架构,在分层架构中都会使用模型转换,在不同的层使用不同的模型。以 DDD 分层模型为例,如下:
常见的对象拷贝工具有:Apache BeanUtils、Spring BeanUtils、cglib BeanCopier、HuTool BeanUtils、MapStruct、getter & setter
下方表格是对以上对象拷贝工具的对比,以供大家在选择使用拷贝工具时作参考
拷贝工具 | 性能 | 使用场景 | 人力成本 |
---|---|---|---|
Apache BeanUtils | 基于反射实现,性能较差 | 简单的对象拷贝 | 参数顺序和其它的工具正好相反,导致使用不顺手,容易产生问题,便捷 |
Spring BeanUtils | 基于内省+反射,借助getter/setter方法实现属性拷贝,性能比apache高 | 简单的对象拷贝 | 直接使用Spring 包下的BeanUtils.copyProperties()即可,便捷 |
cglib BeanCopier | 通过动态代理的方式来实现属性拷贝、性能高效 | 简单的对象拷贝 | 直接使用BeanCopier自带方法即可,便捷 |
HuTool BeanUtils | 性能介于apache和Spring之间 | 简单的对象拷贝 | 需要额外引入HuTool的依赖,不便捷 |
MapStruct | 基于getter/setter方法实现属性拷贝,在编译时自动生成实现类的代码,性能媲美getter & sette | 可直接进行集合转换、能够实现深度拷贝 | 需要额外声明bean的转换接口,不太便捷 |
getter & setter | 性能最高 | 简单的对象拷贝 | 需手动拷贝,不便捷 |
以上是几种拷贝工具的对比,个人比较建议:简单对象拷贝使用BeanCopier,复杂对象封装(如集合转换,两个实体合为一个实体,vo和dto字段不一致等)可以考虑使用MapStruct对象封装工具。
二、项目使用案例
1.依赖引入
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.5.3.Final</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.5.3.Final</version>
</dependency>
2.项目使用
需求:
1.将ConfigStationDTO对象转换为StationVO对象(ConfigStationDTO的id字段需对应StationVO的sid字段)
2.将ConfigStationDTOList集合转换为StationVOList集合
3.将ConfigStationDTO和ConfigSceneDTO对象合并转换为StationVO对象
代码:
1.具体实现
首先需要定义一个转换接口并继承封装好的CommonConvert接口(CommonConvert接口中已经定义了四个方法,分别是:vo转dto、dto转vo、dtoList转voList、voList转dtoList)
public interface CommonConvert<D, V> {
/**
* DTO转VO
*/
V toVO(D dto);
/**
* VO转DTO
*/
D toDTO(V vo);
/**
* DTO集合转VO集合
*/
List<V> toVOList(List<D> dtoList);
/**
* VO集合转DTO集合
*/
List<D> toDTOList(List<V> voList);
}
定义具体的转换接口
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface ConfigStationConvert extends CommonConvert<ConfigStationDTO, StationVO> {
/**
* 如有字段不一致,重写转换方法,使用@Mappings映射,如下
*/
@Override
@Mappings({
@Mapping(source = "id", target = "sid")
})
StationVO toVO(ConfigStationDTO dto);
/**
* 如有两个实体合并到一个实体的需求,案例如下
* 注意事项:如果入参中的两个实体都有转换后对象中的字段,必须指定使用哪个实体的字段
*/
@Mappings({
@Mapping(source = "stationDTO.uuid", target = "uuid"),
@Mapping(source = "stationDTO.projectUuid", target = "projectUuid"),
@Mapping(source = "configSceneDTO.name", target = "name"),
@Mapping(source = "configSceneDTO.description", target = "description"),
@Mapping(source = "configSceneDTO.createTime", target = "createTime")
})
StationVO toVO(ConfigStationDTO stationDTO, ConfigSceneDTO configSceneDTO);
}
备注:1.@Mapper()中componentModel = "spring"是用来将此接口交给spring管理,unmappedTargetPolicy = ReportingPolicy.IGNORE是用来忽略未匹配字段
2.重写的toVO()方法能够满足第1个需求,@Mappings注解是用来做参数转换的,source是入参中的字段,target是出参中的字段
3.CommonConvert中toVOList()方法可以满足第2个需求
4.manytoVO()方法能够满足第3个需求
2.测试case
@SpringBootTest
public class TestMapStructDemo {
// 因为我们定义接口时已经交由spring进行管理,所以此处可以直接使用@Autowired注入
@Autowired
private ConfigStationConvert configStationConvert;
@Test
void contextLoads() {
System.out.println("123");
ConfigStationDTO configStationDTO = new ConfigStationDTO();
configStationDTO.setId(123L);
configStationDTO.setUuid(1111111111111111L);
configStationDTO.setName("站1");
configStationDTO.setDescription("站1");
StationVO stationVO1 = configStationConvert.toVO(configStationDTO);
System.out.println("第一个需求字段不一致转换结果 = " + stationVO1);
ConfigStationDTO configStationDTO2 = new ConfigStationDTO();
configStationDTO2.setUuid(222222222222222222L);
configStationDTO2.setName("站2");
configStationDTO2.setDescription("站2");
ConfigStationDTO configStationDTO3 = new ConfigStationDTO();
configStationDTO3.setUuid(333333333333333333L);
configStationDTO3.setName("站3");
configStationDTO3.setDescription("站3");
ArrayList<ConfigStationDTO> configStationDTOS = Lists.newArrayList(configStationDTO2, configStationDTO3);
List<StationVO> stationVOS = configStationConvert.toVOList(configStationDTOS);
System.out.println("第二个需求list转换结果 = " + stationVOS);
ConfigSceneDTO configSceneDTO = new ConfigSceneDTO();
configSceneDTO.setName("场景名");
StationVO stationVO = configStationConvert.manytoVO(configStationDTO, configSceneDTO);
System.out.println("第三个需求多对象合并结果 = " + stationVO);
}
}
3.测试结果
第一个需求字段不一致转换结果 = StationVO(sid=123, uuid=1111111111111111, projectUuid=null, name=站1, description=站1, destinationCode=null, createTime=null, updateTime=null, customAttributes=null)
第二个需求list转换结果 = [StationVO(sid=null, uuid=222222222222222222, projectUuid=null, name=站2, description=站2, destinationCode=null, createTime=null, updateTime=null, customAttributes=null), StationVO(sid=null, uuid=333333333333333333, projectUuid=null, name=站3, description=站3, destinationCode=null, createTime=null, updateTime=null, customAttributes=null)]
第三个需求多对象合并结果 = StationVO(sid=null, uuid=1111111111111111, projectUuid=null, name=场景名, description=null, destinationCode=null, createTime=null, updateTime=null, customAttributes=null)
三、注意事项
使用过程中踩了两个坑,贴在这里以供大家借鉴
1.自定义转换接口存放的文件夹命名一定不能是mapper,因为mybatis-plus的mapper-scan会扫描mapper文件夹,此时@Mapper注解就会和mybatis-plus冲突
2.如果使用多对象合并为一个对象时,入参中的两个实体都有将转换的对象中的字段,必须指定使用哪个实体的字段。
如:ConfigStationDTO中有uuid字段,ConfigSceneDTO中也有uuid字段,此时不指定@Mapping(source = “stationDTO.uuid”, target = “uuid”),,编译器编译时就会报无法匹配的错误
3.如果你项目中也使用了 Lombok,需要注意一下 Lombok 的版本至少是 1.18.10 或者以上才行,否则会出现编译失败的情况。
四、小结
以上就是MapStruct工具类的一些使用心得和踩坑总结。
附:
Idea插件:搜索MapStruct Support安装即可,可以在使用MapStruct时获得更加丰富代码提示。
官网地址:mapStruct官网地址