推薦一款Java實體映射工具---mapstruct


背景

由於代碼分層原因,導致代碼中會有多種形如XXXVO、XXXDTO、XXXDO的類,並且經常發生各種VO/DTO/DO之后轉換。從而產生很多 vo.setXXX(dto.getXXX()) 的代碼。當字段多了之后不僅容易出錯,而且有些浪費時間。也會有人使用 BeanUtils.copyProperties() 進行轉換,這樣雖然節省了代碼。但是依舊存在一些問題。

  1. 使用反射性能不好
  2. 不同名稱直接無法映射。

本文將介紹一款Java實體對象映射框架---MapStruct。

介紹

官方文檔:https://mapstruct.org/documentation/dev/reference/html/
首頁:https://mapstruct.org/

MapStruct是一種基於 Java JSR 269 注釋處理器,用於生成類型安全,高性能和無依賴的Bean映射代碼。

  1. 通過getter/setter 進行字段拷貝,而不是反射
  2. 字段名稱相同直接轉換,名稱不同使用 @Mapping 注解標識

與動態映射框架相比,MapStruct的優勢:

  1. 使用普通的getter/setter方法而非反射,執行更快
  2. 編譯時類型安全
  3. 清晰的錯誤提示信息

使用

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;
}

通過上面代碼得出以下幾個結論:

  1. 同名稱的自動轉換,如果類型不同也會進行隱式轉換。題外音:類型不一致,字段名稱一致的情況可能出錯,需要注意。
  2. 字段之間的拷貝是通過 getter/setter 方法,而不是通過反射。題外音:類必需有 getter/setter 方法
  3. 名稱不同的未進行轉換(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 注解的 sourcetarget 進行不同名字段的映射。其中 source 代表源字段,target 表示 source 字段映射到的字段。

字段轉換時,需要簡單處理

上面我們發現 Student 類的 genderint 類型(0表示女,1表示男),StudentVOgenderString(男或女)。此時並不是直接的字段轉換,而是需要映射。 此時我們再次修改映射接口代碼如下:

@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>,則只需寫出 ProjectProjecgVO 的映射即可。代碼如下:
類定義如下:

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+
    }
}
  1. mapstruct編譯時生成代碼更快
  2. mapstruct可以對名稱或類型不同的字段,進行處理
  3. 常見的Bean映射工具性能對比,結論:MapStruct最優
  4. 基本原理:
    4.1 BeanUtils.copyProperties 允許時反射機制
    4.2 mapStruct 編譯期間,生成代碼。可以參考文章:Java-JSR-269-插入式注解處理器框架原理 和我們使用的lombok原理一樣


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM