由于代码分层原因,导致代码中会有多种形如XXXVO、XXXDTO、XXXDO的类,并且经常发生各种VO/DTO/DO之后转换。从而产生很多 vo.setXXX(dto.getXXX()) 的代码。当字段多了之后不仅容易出错,而且有些浪费时间。也会有人使用 BeanUtils.copyProperties() 进行转换,这样虽然节省了代码。但是依旧存在一些问题。
- 使用反射性能不好
- 不同名称直接无法映射。
本文将介绍一款Java实体对象映射框架---MapStruct。
介绍官方文档:https://mapstruct.org/documentation/dev/reference/html/
首页:https://mapstruct.org/
MapStruct是一种基于 Java JSR 269 注释处理器,用于生成类型安全,高性能和无依赖的Bean映射代码。
- 通过getter/setter 进行字段拷贝,而不是反射
- 字段名称相同直接转换,名称不同使用 @Mapping 注解标识
与动态映射框架相比,MapStruct的优势:
- 使用普通的getter/setter方法而非反射,执行更快
- 编译时类型安全
- 清晰的错误提示信息
maven配置
... <properties> <org.mapstruct.version>1.4.0.Beta3</org.mapstruct.version> </properties> ... <dependencies> <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct</artifactId> <version>${org.mapstruct.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>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>${org.mapstruct.version}</version> </path> </annotationProcessorPaths> </configuration> </plugin> </plugins> </build> ...
可配置项
选项 | 描述 | 默认值 |
---|---|---|
mapstruct. suppressGeneratorTimestamp | 设置成true 时,在生成的代码中不生成创建时间戳 | false |
mapstruct.verbose | false | |
mapstruct.defaultComponentModel | 组件模型,取决于如何获取mapper对象。 支持:default、cdi、spring、jsr30 可通过注解配置 @Mapper#componentModel() |
default |
mapstruct. suppressGeneratorVersionInfoComment | 控制在生成的代码中生成版本信息comments | false |
mapstruct.defaultInjectionStrategy | 注入类型,仅适用于cdi、spring、jsr30 支持:field、constructor 可通过注解配置 @Mapper#injectionStrategy() |
field |
mapstruct.unmappedTargetPolicy | 目标属性没有原属性填充时的提示策略 支持:error、warn、ignore 可通过注解配置 @Mapper#unmappedTargetPolicy() |
warn |
基本映射
第一步:定义类,已省略 getter/setter 方法
public class Student { private String stuName; private String stuNumber; private int gender; } public class StudentVO { private String stuName; // 姓名 private String displayStuNumber; // 展示学号 private String gender; // 男 女 }
第二步:创建映射器。只需定义Java接口,并使用注解 @Mapper ,代码如下所示
@Mapper public interface MapStruct101 { @Mappings({}) StudentVO toStudentVO(Student student); }
代码编译之后,生成MapStruct101的实现类。生成代码如下:
@Override public StudentVO toStudentVO(Student student) { if ( student == null ) { return null; } StudentVO studentVO = new StudentVO(); studentVO.setStuName( student.getStuName() ); studentVO.setGender( String.valueOf( student.getGender() ) ); return studentVO; }
通过上面代码得出以下几个结论:
- 同名称的自动转换,如果类型不同也会进行隐式转换。题外音:类型不一致,字段名称一致的情况可能出错,需要注意。
- 字段之间的拷贝是通过 getter/setter 方法,而不是通过反射。题外音:类必需有 getter/setter 方法
- 名称不同的未进行转换(displayStuNumber未转换)
字段名称不同的处理
上面在映射接口我们直接使用了 @Mappings({}),未进行特殊处理,所以只对同名的进行了转换。现在我们增加注解,从而实现名称不同的字段之间的转换。
接口类代码修改如下:
@Mapper public interface MapStruct101 { @Mappings({ @Mapping(source = "stuNumber", target = "displayStuNumber") }) StudentVO toStudentVO(Student student); }
再次编译之后生成代码如下:
@Override public StudentVO toStudentVO(Student student) { if ( student == null ) { return null; } StudentVO studentVO = new StudentVO(); studentVO.setDisplayStuNumber( student.getStuNumber() ); studentVO.setStuName( student.getStuName() ); studentVO.setGender( String.valueOf( student.getGender() ) ); return studentVO; }
我们通过使用 @Mapping 注解的 source 和 target 进行不同名字段的映射。其中 source 代表源字段,target 表示 source 字段映射到的字段。
字段转换时,需要简单处理
上面我们发现 Student 类的 gender 是 int 类型(0表示女,1表示男),StudentVO的 gender 是 String(男或女)。此时并不是直接的字段转换,而是需要映射。 此时我们再次修改映射接口代码如下:
@Mapper public interface MapStruct101 { @Mappings({ @Mapping(source = "stuNumber", target = "displayStuNumber"), @Mapping(target = "gender", expression = "java(student.getGender() == 1 ? \"男\" : \"女\")") }) StudentVO toStudentVO(Student student); }
编译之后生成代码如下:
@Override public StudentVO toStudentVO(Student student) { if ( student == null ) { return null; } StudentVO studentVO = new StudentVO(); studentVO.setStuName( student.getStuName() ); studentVO.setGender( student.getGender() == 1 ? "男" : "女" ); studentVO.setDisplayStuNumber( student.getStuNumber()); return studentVO; }
这样gender字段就变成了 男、女了。我们发现可以使用 @Mapping 注解的 expression 进行字段转换时的简单处理。
字段转换时,需要复杂处理
开发中有时候字段需要进行复杂逻辑处理,多行代码如果写在expression字段显然不合理。我们可以这样处理,修改映射接口如下:(此处还是以性别映射举例)
@Mapper public interface MapStruct101 { @Mappings({ @Mapping(source = "stuNumber", target = "displayStuNumber"), @Mapping(target = "gender", source = "gender", qualifiedByName = "transferGender") }) StudentVO toStudentVO(Student student); @Named("transferGender") default String transferGender(int gender) { return gender == 1 ? "男" : "女"; } }
编译之后代码如下所示:
@Override public StudentVO toStudentVO(Student student) { if ( student == null ) { return null; } StudentVO studentVO = new StudentVO(); studentVO.setStuName( student.getStuName() ); studentVO.setGender( transferGender(student.getGender())); studentVO.setDisplayStuNumber( student.getStuNumber()); return studentVO; } default String transferGender(int gender) { return gender == 1 ? "男" : "女"; }
我们可以使用一个defaut方法进行复杂逻辑的处理,并使用@Named注解进行标注,并在 @Mapping 注解中使用 qualifiedByName 表明使用哪个方法进行处理转换。 这样生成代码之后就会调用指定方法进行转换。
类中包含其他类的列表
此处可以自己写demo验证看看哦。例如学生类中有List<Project>,则只需写出 Project 与 ProjecgVO 的映射即可。代码如下:
类定义如下:
public class Student { private String stuName; private String stuNumber; private int gender; private List<Project> projects; } public class Project { private String projectName; private double projectScore; private String teacherName; } public class StudentVO { private String stuName; private String displayStuNumber; private String gender; private List<ProjectVO> projectVOList; } public class ProjectVO { private String projectName; private double projectScore; private String teacherName; }
映射接口代码:
@Mapper public interface MapStruct101 { @Mappings({ @Mapping(target = "gender", expression = "java(student.getGender() == 1 ? \"男\" : \"女\")") @Mapping(target = "displayStuNumber", source = "stuNumber") @Mapping(target = "projectVOList", source = "projects") }) StudentVO toStudentVO(Student student); }
编译之后生成代码如下:
@Override public StudentVO toStudentVOWithListObject(Student student) { if ( student == null ) { return null; } StudentVO studentVO = new StudentVO(); studentVO.setProjectVOList( projectListToProjectVOList( student.getProjects() ) ); studentVO.setStuName( student.getStuName() ); studentVO.setGender( student.getGender() == 1 ? "男" : "女" ); studentVO.setDisplayStuNumber( student.getStuNumber()); return studentVO; }
其实会自动生成包含类的映射关系,很是方便。
对Builder的支持
现在我们都是用grpc,生成对象都是通过Builder生成的,并没有直接的 setter 方法,这种情况mapstrcut也是支持的,具体生成代码是,会先生成对应的Builder对象,然后在调用 setter 方法。大家可以自行试一下,此处不再举例说明。
引用
映射接口写好了,我们应该如何使用呢?
普通使用,可以通过如下代码:Mappers.getMapper(MapStruct101.class)
@Test public void test() { MapStruct101 mapper = Mappers.getMapper(MapStruct101.class); Teacher teacher = Teacher.builder() .teacherName("张老师") .address("西二旗") .mobilePhone("123445") .build(); TeacherVO teacherVO = mapper.toTeacherVO(teacher); System.out.println(teacherVO); }
spring使用,需要修改组件模型为 spring,可以通过pom.xml的参数修改,也可以通过注解修改。修改之后会把实现类添加 @Component 从而成为一个bean。 此处我们通过修改注解,使用 @Mapper(commentModel = "spring")
@Mapper(componentModel = "spring") public interface MapStruct102 { @Mapping(source = "teacherName", target = "name") @Mapping(source = "mobilePhone", target = "phone") TeacherVO toTeacherVO(Teacher teacher); } // 就可以使用bean注入 @Autowired private MapStruct102 mapStruct102; @Test public void test() { Teacher teacher = Teacher.builder() .teacherName("张老师") .address("西二旗") .mobilePhone("123445") .build(); TeacherVO teacherVO = mapStruct102.toTeacherVO(teacher); System.out.println(teacherVO); }与BeanUtils对比
public class Client3 { public static void main(String[] args) { MapStruct101 mapper = Mappers.getMapper(MapStruct101.class); School school = new School(); school.setSchoolAge(120); school.setAddress("北京"); school.setManager("校长"); school.setSchoolName("北大"); school.setTotalStudentCnt(10000); school.setTotalTeacherCnt(1000); long start = System.currentTimeMillis(); mapper.toSchoolVO(school); long cost = System.currentTimeMillis() - start; System.out.println("cost:" + cost); // 耗时:0 SchoolVO schoolVO = new SchoolVO(); start = System.currentTimeMillis(); BeanUtils.copyProperties(school, schoolVO); cost = System.currentTimeMillis() - start; System.out.println("cost:" + cost); // 耗时:100+ } }
- mapstruct编译时生成代码更快
- mapstruct可以对名称或类型不同的字段,进行处理
- 常见的Bean映射工具性能对比,结论:MapStruct最优
- 基本原理:
4.1 BeanUtils.copyProperties 允许时反射机制
4.2 mapStruct 编译期间,生成代码。可以参考文章:Java-JSR-269-插入式注解处理器、框架原理 和我们使用的lombok原理一样