前言
随着软件架构分层越来越多,那么各个层次之间的数据模型就要面临着相互转换的问题,典型的就是我们可以在代码中见到各种O,如DO、DTO、VO等。
如在数据存储层,我们使用DO来抽象一个业务实体;在业务逻辑层,我们使用DTO来表示数据传输对象;到了展示层,我们又把对象封装成VO来与前端进行交互。
先来看一张图
想必大家伙原来都写过类似格式的代码,额···看着就不说了,冗长而繁琐,于是MapStruct这一类框架应运而生 包括:
- Spring BeanUtils
- Cglib BeanCopier
- Apache BeanUtils
- Apache PropertyUtils
- Dozer
简介
MapStruct(https://mapstruct.org/ )是一种代码生成器,它极大地简化了基于"约定优于配置"方法的Java bean类型之间映射的实现。生成的映射代码使用纯方法调用,因此快速、类型安全且易于理解
入门使用
第一步:导入依赖
<!-- Bean转换工具 mapstruct -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.3.1.Final</version>
</dependency>
<!-- Junit测试单元 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<!-- lombok实体类简化 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.10</version>
<scope>provided</scope>
</dependency>
第二步:定义映射接口
写法1.(Spring方式配置)
Spring
方式我们需要在@Mapper
注解内添加componentModel
属性值,配置后在外部可以采用@Autowired
方式注入Mapper实现类完成映射方法调用。
@Mapper(componentModel = "spring")
interface StudentConvert {
/**
* 如果要转换的两个类中源对象属性与目标对象属性的类型和名字一致的时候,会自动映射对应属性。
* 属性一样的可以自动映射,这里设置下不一样的属性
* source--PersonDO
* target--PersonDTO
*/
@Mapping(source = "name", target = "studentName")
StudentDTO infoToVo(StudentInfo info);
}
写法2.(默认配置)
采用
Mappers
通过动态工厂内部反射机制完成Mapper实现类的获取。
@Mapper
public interface StudentConvert2 {
StudentConvert2 STUDENT_CONVERT = Mappers.getMapper(StudentConvert2.class);
@Mapping(source = "name", target = "studentName")
StudentDTO infoToVo(StudentInfo info);
}
编译一下
IDEA快捷键 ctrl+F9 编译一下 ,mapstruct会自动生成这个接口的实现类,如下:
注:这里有个地方需要注意,如果你修改了source或者target对象中的属性,那么需要重新编译一下,麻烦的方法可以使用mvn clean + mvn compile ,简单的ctrl+f9一下,让代码重新编译
@Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2020-08-31T17:51:53+0800",
comments = "version: 1.3.1.Final, compiler: javac, environment: Java 1.8.0_144 (Oracle Corporation)"
)
@Component
public class StudentConvertImpl implements StudentConvert {
@Override
public StudentDTO infoToVo(StudentInfo info) {
if ( info == null ) {
return null;
}
StudentDTO studentDTO = new StudentDTO();
studentDTO.setStudentName( info.getName() );
studentDTO.setId( info.getId() );
studentDTO.setBirthDay( info.getBirthDay() );
return studentDTO;
}
}
测试一下
@Test
public void infoToVoTest() {
StudentInfo studentInfo = new StudentInfo(1,"松鼠", LocalDate.now());
var data = commonConvert.infoToVo(studentInfo);
System.out.println(data);
}
//结果:StudentDTO(id=1, studentName=松鼠, birthDay=2020-08-31)
高级使用
类型不一致
@Data
@AllArgsConstructor
public class StudentInfo {
private Integer id;
private String name;
private LocalDate birthDay;
private Integer sex; // 1、男 2、女
}
@Data
public class StudentDTO {
private Long id;
private String studentName;
private LocalDate birth;
private String sex; // 显示 男、女
}
@Mapper
public interface StudentConvert3 {
StudentConvert3 STUDENT_CONVERT = Mappers.getMapper(StudentConvert3.class);
@Mapping(source = "name", target = "studentName")
@Mapping(source = "birthDay", target = "birth")
@Mapping(target = "sex",expression = "java(sexConvert(info.getSex()))")
StudentDTO infoToVo(StudentInfo info);
// default 需要接口中的方法有方法体
default String sexConvert(Integer sex) {
if (sex == 1) {
return "男";
}
return "女";
}
}
@Test
public void infoToVo3() {
StudentInfo studentInfo = new StudentInfo(1,"松鼠", LocalDate.now(),2);
StudentDTO studentDTO = StudentConvert3.STUDENT_CONVERT.infoToVo(studentInfo);
System.out.println(studentDTO);
}
// 结果 : StudentDTO(id=1, studentName=松鼠, birth=2020-08-31, sex=女)
属性是枚举类型
@AllArgsConstructor
@Getter
public enum SexEnum {
WOMEN(0,"女人"),
MEN(1,"男人")
;
private int code;
private String name;
}
@Mapper
public interface StudentConvert4 {
StudentConvert4 STUDENT_CONVERT = Mappers.getMapper(StudentConvert4.class);
@Mapping(source = "name", target = "studentName")
@Mapping(source = "birthDay", target = "birth")
// @Mapping(source = "sexEnum.name", target = "sex")
@Mapping(source = "sexEnum", target = "sex")
StudentDTO infoToVo(StudentInfo4 info);
}
@Test
public void infoToVo4() {
StudentInfo4 studentInfo = new StudentInfo4(1,"松鼠", LocalDate.now(), SexEnum.WOMEN);
StudentDTO studentDTO = StudentConvert4.STUDENT_CONVERT.infoToVo(studentInfo);
System.out.println(studentDTO);
}
// 结果 : StudentDTO(id=1, studentName=松鼠, birth=2020-08-31, sex=WOMEN)
常用参数
这里只介绍了些常用的参数,更多可以访问相关文档
defaultValue
有时候,在转换过程中,可能因为空值或其他原因使得映射结果不正确,此时可以指定一个默认值,防止程序出错。
@Mappings( {
@Mapping(target = "age", source = "evage", defaultValue = "20")
})
expressions
可以通过表达式来构造一些简单的转化关系。虽然设计的时候想兼容很多语言,不过目前只能写Java代码。
@Mapper
public interface SourceTargetMapper {
SourceTargetMapper INSTANCE = Mappers.getMapper( SourceTargetMapper.class );
@Mapping(target = "timeAndFormat",
expression = "java(sexConvert(info.getSex()))")
Target sourceToTarget(Source s);
}
这里用到演示了如何使用sexConvert对不同类型转换操作,这里可以指定需要使用的Java类的完整包名,否则就按照本类中的方法执行,找不到就编译错误
defaultExpression
默认表达式是 defaultValue和expressions的组合 ,仅当source属性为
null
时才使用它们。
@Mapper( imports = UUID.class )
public interface SourceTargetMapper {
SourceTargetMapper INSTANCE = Mappers.getMapper( SourceTargetMapper.class );
@Mapping(target="id", source="sourceId", defaultExpression = "java( UUID.randomUUID().toString() )")
Target sourceToTarget(Source s);
}
性能
与其他映射框架相比,MapStruct在编译时生成bean映射,这确保了高性能,可以提前将问题反馈出来,也使得开发人员可以彻底的错误检查。
详细的可以看这位兄台的文章《为什么阿里巴巴禁止使用Apache BeanUtils进行属性的Copy》,里面介绍了这些框架性能对比
总结
以上就是关于mapstruct的简单使用及入门,总结下优点就是
- 通过使用普通方法调用而不是反射来快速执行,也就是 会在编译器生成相应的 Impl 方法调用时直接通过简单的 getter/setter调用而不是反射或类似的方式将值从源复制到目标
- 编译时类型安全性 : 只能映射彼此的对象和属性,不能将订单实体意外映射到客户 DTO等
- 在构建时清除错误报告,如 映射不完整 (并非所有目标属性都被映射) 或 映射不正确(无法找到适当的映射方法或类型转换)