背景
由於代碼分層原因,導致代碼中會有多種形如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原理一樣