MapStruct的介紹及入門使用

liftsail發表於2024-06-01

一、痛點

  程式碼中存在很多Java Bean之間的轉換,編寫對映轉化程式碼是一個繁瑣重複還易出錯的工作。使用BeanUtils工具時,對於欄位名不一致和巢狀型別不一致時,需要手動編寫。並且基於反射,對效能有一定開銷。Spring提供的BeanUtils針對apache的BeanUtils做了很多最佳化,整體效能提升了不少,不過還是使用反射實現,針對複雜場景支援能力不足。

二、MapStruct 機制

MapStruct是編譯期動態生成getter/setter,在執行期直接呼叫框架編譯好的class類實現實體對映。因此安全性高,編譯透過之後,執行期間就不會報錯。其次速度快,執行期間直接呼叫實現類,不會在執行期間使用發射進行轉換。

三、環境搭建

Maven依賴匯入:mapstruct依賴會匯入MapStruct的核心註解。由於MapStruct在編譯時工作,因此需要在<build>標籤中新增外掛maven-compiler-plugin,並在其配置中新增annotationProcessorPaths,該外掛會在構建時生成對應的程式碼。

<properties>
    <org.mapstruct.version>1.4.2.Final</org.mapstruct.version>
  	<lombok.version>1.18.12</lombok.version>
</properties>

<dependencies>
    <dependency>
        <groupid>org.mapstruct</groupid>
        <artifactid>mapstruct</artifactid>
        <version>${org.mapstruct.version}</version>
    </dependency>
    <dependency>
        <groupid>org.projectlombok</groupid>
        <artifactid>lombok</artifactid>
        <version>${lombok.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>
                  	<!--下面這個 專案中不使用 Lombok的話 不用加-->
                    <path>
                        <groupid>org.projectlombok</groupid>
                        <artifactid>lombok</artifactid>
                        <version>${lombok.version}</version>
                    </path>
                </annotationprocessorpaths>
            </configuration>
        </plugin>
    </plugins>
</build>

四、使用

單一物件轉化

建立對映:如下兩個類進行物件之間的轉換

public class Student {
    private int id;
    private String name;
    // 兩個類中存在不同的屬性名,需要在Mapper介面中設定source和target
    private String book;  
    // getters and setters or builder
}

public class StudentDto {
    private int id;
    private String name;
    // 兩個類中存在不同的屬性名,需要在Mapper介面中設定source和target
    private String letter; 
    // getters and setters or builder
}

兩者之間進行對映,需要建立一個StudentMapper介面並使用@Mapper註解,MapStruct就知道這是兩個類之間的對映器。

@Mapper
public interface StudentMapper {
    StudentMapper INSTANCE = Mappers.getMapper(StudentMapper.class);

    // 兩個類中存在不同的屬性名,需要在Mapper介面中設定source和target
    @Mapping(source = "student.book", target = "letter")
    StudentDto toDto(Student student);
}

當我們需要將Student屬性對映到StudentDto

StudentDto studentDto = StudentMapper.INSTANCE.toDto(student);

當我們構建/編譯應用程式時,MapStruct註解處理器外掛會識別出StudentMapper介面並生成StudentMapperImpl實體類:如果型別中包含BuilderMapStruct會嘗試使用它來構建例項,如果沒有MapStruct將透過new關鍵字進行例項化。

public class StudentMapperImpl implements StudentMapper {
    @Override
    public StudentDto toDto(Student student) {
        if ( student == null ) {
            return null;
        }
        StudentDtoBuilder studentDto = StudentDto.builder();

        studentDto.id(student.getId());
        studentDto.name(student.getName());
        // ....
        return studentDto.build();
    }
}

多個物件轉換為一個物件

public class Student {
    private int id;
    private String name;
    // getters and setters or builder
}

public class StudentDto {
    private int id;
    private int classId;
    private String name;
    // getters and setters or builder
}

public class ClassInfo {
    private int id;
    private int classId;
    private String className;
    // getters and setters or builder
}

StudentMapper介面更新如下:如果兩個屬性中包含相同的欄位時,需要透過sourcetarget指定具體使用哪個類的屬性。

@Mapper
public interface StudentMapper {
    StudentMapper INSTANCE = Mappers.getMapper(StudentMapper.class);

    @Mapping(source = "student.id", target = "id")    
    StudentDto toDto(Student student, ClassInfo classInfo);
}

子物件對映

多數情況下,POJO中不會只包含基本資料型別,其中往往會包含其它類。比如說,一個Student類中包含ClassInfo類:

public class Student {
    private int id;
    private String name;
    private ClassInfo classInfo;
    // getters and setters or builder
}

public class StudentDto {
    private int id;
    private String name;
    private ClassInfoDto classInfoDto;
    // getters and setters or builder
}

public class ClassInfo {
    private int classId;
    private String className;
    // getters and setters or builder
}

public class ClassInfoDto {
    private int classId;
    private String className;
    // getters and setters or builder
}

在修改StudentMapper之前,我們先建立一個ClassInfoMapper轉換器:

@Mapper
public interface ClassInfoMapper {
    ClassInfoMapper INSTANCE = Mappers.getMapper(ClassInfoMapper.class);
    ClassInfoDto dto(ClassInfo classInfo);
}

建立完ClassInfoMapper之後,我們再修改StudentMapper:新增uses標識,這樣StudentMapper就能夠使用ClassInfoMapper對映器

@Mapper(uses = {ClassInfoMapper.class})
public interface StudentMapper {
    StudentMapper INSTANCE = Mappers.getMapper(StudentMapper.class);

    @Mapping(source="student.classInfo", target="classInfoDto")
    StudentDto toDto(Student student, ClassInfo classInfo);
}

我們看先編譯後的程式碼:新增了一個新的對映方法classInfoDtoToclassInfo。這個方法如果沒有顯示定義的情況下生成,因為我們將ClassInfoMapper物件新增到了StudenMapper中。

public class StudentMapperImpl implements StudentMapper {
    private final ClassInfoMapper classInfoMapper = Mappers.getMapper( ClassInfoMapper.class );

    @Override
    public StudentDto toDto(Student student) {
        if ( student == null ) {
            return null;
        }
        StudentDtoBuilder studentDto = StudentDto.builder();

        studentDto.id(student.getId());
        studentDto.name(student.getName());
        studentDto.classInfo = (classInfoDtoToclassInfo(student.calssInfo))
        // ....
        return studentDto.build();
    }

    protected ClassInfoDto classInfoDtoToclassInfo(ClassInfo classInfo) {
        if ( classInfo == null ) {
            return null;
        }
        ClassInfoDto classInfoDto = classInfoMapper.toDto(classInfo);
        return classInfoDto;
    }
}

資料型別對映
自動型別轉換適用於一下幾種情況:
【1】基本型別及其包裝類之間的轉換:int和Integer,float與Float,long與Long,boolean與Boolean等。
【2】任意基本型別與任意包裝類之間。如int和long,byte和Integer等。
【3】所有基本型別及包裝類與String之間。如boolean和String,Integer和String等。
【4】列舉和String之間。
【5】Java大數型別java.math.BigInteger, java.math.BigDecimal和Java基本型別(包括其包裝類)與String之間。

日期轉換:指定格式

public class Student {
    private int id;
    private String name;
    private LocalDate birth;
    // getters and setters or builder
}

public class StudentDto {
    private int id;
    private String name;
    pprivate String birth;
    // getters and setters or builder
}

建立對映器

@Mapper
public interface StudentMapper {
    StudentMapper INSTANCE = Mappers.getMapper(StudentMapper.class);

    // 也可以指定數字的格式
    // @Mapping(source = "price", target = "price", numberFormat = "$#.00")
    @Mapping(source = "birth", target = "birth", dataFormat = "dd/MM/yyyy")    
    StudentDto toDto(Student student, ClassInfo classInfo);
}

List對映

定義一個新的對映方法

@Mapper
public interface StudentMapper {
    List<StudentDto> map(List<Student> student);
}

自動生成的程式碼如下:

public class StudentMapperImpl implements StudentMapper {

    @Override
    public List<StudentDto> map(List<Student> student) {
        if ( student == null ) {
            return null;
        }

        List<StudentDto> list = new ArrayList<StudentDto>( student.size() );
        for ( Student student1 : student ) {
            list.add( studentToStudentDto( student1 ) );
        }

        return list;
    }

    protected StudentDto studentToStudentDto(Student student) {
        if ( student == null ) {
            return null;
        }

        StudentDto studentDto = new StudentDto();

        studentDto.setId( student.getId() );
        studentDto.setName( student.getName() );

        return studentDto;
    }
}

SetMap型資料的處理方式與List相似:

@Mapper
public interface StudentMapper {

    Set<StudentDto> setConvert(Set<Student> student);

    Map<String, StudentDto> mapConvert(Map<String, Student> student);
}

新增預設值
@Mapping註解有兩個很實用的標誌就是常量constant和預設值defaultValue。無論source如何取值,都將始終使用常量值,如果source取值為null,則會使用預設值。修改一下StudentMapper,新增一個constant和一個defaultValue:

@Mapper(componentModel = "spring")
public interface StudentMapper {
    @Mapping(target = "id", constant = "-1")
    @Mapping(source = "student.name", target = "name", defaultValue = "zzx")
    StudentDto toDto(Student student);
}

如果name不可用,我們會替換為zzx字串,此外,我們將id硬編碼為-1

@Component
public class StudentMapperImpl implements StudentMapper {

    @Override
    public StudentDto toDto(Student student) {
        if (student == null) {
            return null;
        }

        StudentDto studentDto = new StudentDto();

        if (student.getName() != null) {
            studentDto.setName(student.getName());
        }
        else {
            studentDto.setName("zzx");
        }
        studentDto.setId(-1);

        return studentDto;
    }
}

新增表示式

MapStruct甚至允許在@Mapping註解中輸入Java表示式。你可以設定defaultExpression

@Mapper(componentModel = "spring", imports = {LocalDateTime.class, UUID.class})
public interface StudentMapper {

    @Mapping(target = "id", expression = "java(UUID.randomUUID().toString())")
    @Mapping(source = "student.availability", target = "availability", defaultExpression = "java(LocalDateTime.now())")
    StudentDto toDtoWithExpression(Student student);
}

五、依賴注入

如果你使用的是Spring,只需要修改對映器配置,在Mapper註解中新增componentModel = "spring",告訴MapStruct在生成對映器實現類時,支援透過Spring的依賴注入來建立,就不需要在介面中新增INSTANCE欄位了。這次生成的StudentMapperImpl會帶有@Component註解,就可以在其它類中透過@Autowire註解來使用它。

@Mapper(componentModel = "spring")
public interface StudentMapper {}

如果你不使用Spring, MapStruct也支援Java CDI

@Mapper(componentModel = "cdi")
public interface StudentMapper {}

轉載自https://blog.csdn.net/zhengzhaoyang122/article/details/132657876

相關文章