MapStruct使用指南
這里寫自MapStruct使用指南定義目錄標題
- MapStruct使用指南
- Maven依賴
- MapStruct的核心注釋
- 基本映射
- 不同字段間映射
- 多個源類&不同屬性名稱
- 子對象映射 【可引用其他 Mapper的方法】
- 更新現有實例
- 數據類型轉換
- 日期轉換
- 數字格式轉換
- 枚舉映射
- 集合映射
- List映射
- Set和Map映射
- 集合映射策略
- 進階操作
- 依賴注入
- 添加默認值
- 添加表達式
- 添加自定義方法
- 創建自定義映射器
- @BeforeMapping 和 @AfterMapping
- 映射異常處理
- 映射配置
- 繼承配置
- 繼承逆向配置
MapStruct使用指南
你好! MapStruct使用指南,基本語法知識。 MapStruct官網
Maven依賴
<dependencies><dependency><groupId>org.mapstruct</groupId><artifactId>mapstruct</artifactId><version>${org.mapstruct.version}</version></dependency> </dependencies>附: 這個依賴項會導入MapStruct的核心注釋。由于MapStruct在編譯時工作,并且會集成到像Maven和Gradle這樣的構建工具上,我們還必須在<build中/>標簽中添加一個插件maven-compiler-plugin,并在其配置中添加annotationProcessorPaths,該插件會在構建時生成對應的代碼。
<build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.5.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>參考:https://mapstruct.org/documentation/installation/
MapStruct的核心注釋
import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget;
import org.mapstruct.factory.Mappers;
@Mapping
@MappingTarget
//忽略 categoryId 的轉換
@Mapping(target = “categoryId”,ignore = true),
//源數據類中的集合應用 --> 目標類中的數據引用–轉換List
@Mapping(target = “trees”,source = “colors”),
//嵌套類的屬性簡單傳遞轉換
@Mapping(target = “run”,source = “cart.animal.run”),
時間轉換并格式化
@Mapping(source = “birthday”, target = “birthDateFormat”, dateFormat = “yyyy-MM-dd HH:mm:ss”),
@Mapping:屬性映射,若源對象屬性與目標對象名字一致,會自動映射對應屬性
source:源屬性
target:目標屬性
dateFormat:String 到 Date 日期之間相互轉換,通過 SimpleDateFormat,該值為 SimpleDateFormat的日期格式
ignore: 忽略這個字段
@Mappings:配置多個@Mapping
@MappingTarget 用于更新已有對象
@InheritConfiguration 用于繼承配置
基本映射
我們先從一些基本的映射開始。我們會創建一個Doctor對象和一個DoctorDto。為了方便起見,它們的屬性字段都使用相同的名稱:
public class Doctor {private int id;private String name;// getters and setters or builder }public class DoctorDto {private int id;private String name;// getters and setters or builder }為了在這兩者之間進行映射,我們要創建一個DoctorMapper接口。對該接口使用@Mapper注解,MapStruct就會知道這是兩個類之間的映射器。
@Mapper public interface DoctorMapper {DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class);DoctorDto toDto(Doctor doctor); }這段代碼中創建了一個DoctorMapper類型的實例INSTANCE,在生成對應的實現代碼后,這就是我們調用的“入口”
我們在接口中定義了toDto()方法,該方法接收一個Doctor實例為參數,并返回一個DoctorDto實例。這足以讓MapStruct知道我們想把一個Doctor實例映射到一個DoctorDto實例。
當我們構建/編譯應用程序時,MapStruct注解處理器插件會識別出DoctorMapper接口并為其生成一個實現類。
public class DoctorMapperImpl implements DoctorMapper {@Overridepublic DoctorDto toDto(Doctor doctor) {if ( doctor == null ) {return null;}DoctorDtoBuilder doctorDto = DoctorDto.builder();doctorDto.id(doctor.getId());doctorDto.name(doctor.getName());return doctorDto.build();} }DoctorMapperImpl類中包含一個toDto()方法,將我們的Doctor屬性值映射到DoctorDto的屬性字段中。如果要將Doctor實例映射到一個DoctorDto實例,可以這樣寫:
DoctorDto doctorDto = DoctorMapper.INSTANCE.toDto(doctor);注意:你可能也注意到了上面實現代碼中的DoctorDtoBuilder。因為builder代碼往往比較長,為了簡潔起見,這里省略了builder模式的實現代碼。如果你的類中包含Builder,MapStruct會嘗試使用它來創建實例;如果沒有的話,MapStruct將通過new關鍵字進行實例化。
不同字段間映射
我們需要讓 DoctorMapper 知道這里的不一致。我們可以使用 @Mapping 注解,并設置其內部的 source 和 target 標記分別指向不一致的兩個字段。
@Mapper public interface DoctorMapper {DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class);//將目標對象的 doctor.specialty屬性 拷貝給 specialization @Mapping(source = "doctor.specialty", target = "specialization")DoctorDto toDto(Doctor doctor); }多個源類&不同屬性名稱
我們添加了另一個@Mapping注解,并將其source設置為Education類的degreeName,將target設置為DoctorDto類的degree字段。
@Mapper public interface DoctorMapper {DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class);/* 1:將源對象doctor的specialty賦值給目標對象的specialization屬性 2:將源對象education的degreeName賦值給目標對象的degree屬性3: 其他屬性默認按同名拷貝*/@Mapping(source = "doctor.specialty", target = "specialization")@Mapping(source = "education.degreeName", target = "degree")DoctorDto toDto(Doctor doctor, Education education); }子對象映射 【可引用其他 Mapper的方法】
多數情況下,POJO中不會只包含基本數據類型,其中往往會包含其它類。比如說,一個Doctor類中會有多個患者類:
public class Patient {private int id;private String name;// getters and setters or builder }在Doctor中添加一個患者列表List:
public class Doctor {private int id;private String name;private String specialty;private List<Patient> patientList;// getters and setters or builder }因為Patient需要轉換,為其創建一個對應的DTO:
public class PatientDto {private int id;private String name;// getters and setters or builder }最后,在 DoctorDto 中新增一個存儲 PatientDto的列表:
public class DoctorDto {private int id;private String name;private String degree;private String specialization;private List<PatientDto> patientDtoList;// getters and setters or builder }在修改 DoctorMapper之前,我們先創建一個支持 Patient 和 PatientDto 轉換的映射器接口:
@Mapper public interface PatientMapper {PatientMapper INSTANCE = Mappers.getMapper(PatientMapper.class);PatientDto toDto(Patient patient); }這是一個基本映射器,只會處理幾個基本數據類型。
然后,我們再來修改 DoctorMapper 處理一下患者列表:
@Mapper(uses = {PatientMapper.class}) public interface DoctorMapper {DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class);@Mapping(source = "doctor.patientList", target = "patientDtoList")@Mapping(source = "doctor.specialty", target = "specialization")DoctorDto toDto(Doctor doctor); }因為我們要處理另一個需要映射的類,所以這里設置了@Mapper注解的uses標志,這樣現在的 @Mapper 就可以使用另一個 @Mapper映射器。我們這里只加了一個,但你想在這里添加多少class/mapper都可以。
我們已經添加了uses標志,所以在為DoctorMapper接口生成映射器實現時,MapStruct 也會把 Patient 模型轉換成 PatientDto ——因為我們已經為這個任務注冊了 PatientMapper。
編譯查看最新想實現代碼:
public class DoctorMapperImpl implements DoctorMapper {private final PatientMapper patientMapper = Mappers.getMapper( PatientMapper.class );@Overridepublic DoctorDto toDto(Doctor doctor) {if ( doctor == null ) {return null;}DoctorDtoBuilder doctorDto = DoctorDto.builder();doctorDto.patientDtoList( patientListToPatientDtoList(doctor.getPatientList()));doctorDto.specialization( doctor.getSpecialty() );doctorDto.id( doctor.getId() );doctorDto.name( doctor.getName() );return doctorDto.build();}protected List<PatientDto> patientListToPatientDtoList(List<Patient> list) {if ( list == null ) {return null;}List<PatientDto> list1 = new ArrayList<PatientDto>( list.size() );for ( Patient patient : list ) {list1.add( patientMapper.toDto( patient ) );}return list1;} }顯然,除了toDto()映射方法外,最終實現中還添加了一個新的映射方法——patientListToPatientDtoList()。這個方法是在沒有顯式定義的情況下添加的,只是因為我們把PatientMapper添加到了DoctorMapper中。
該方法會遍歷一個Patient列表,將每個元素轉換為PatientDto,并將轉換后的對象添加到DoctorDto對象內中的列表中。
更新現有實例
有時,我們希望用DTO的最新值更新一個模型中的屬性,對目標對象(我們的例子中是DoctorDto)使用@MappingTarget注解,就可以更新現有的實例.
@Mapper(uses = {PatientMapper.class}) public interface DoctorMapper {DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class);@Mapping(source = "doctorDto.patientDtoList", target = "patientList")@Mapping(source = "doctorDto.specialization", target = "specialty")void updateModel(DoctorDto doctorDto, @MappingTarget Doctor doctor); }重新生成實現代碼,就可以得到updateModel()方法:
public class DoctorMapperImpl implements DoctorMapper {@Overridepublic void updateModel(DoctorDto doctorDto, Doctor doctor) {if (doctorDto == null) {return;}if (doctor.getPatientList() != null) {List<Patient> list = patientDtoListToPatientList(doctorDto.getPatientDtoList());if (list != null) {doctor.getPatientList().clear();doctor.getPatientList().addAll(list);}else {doctor.setPatientList(null);}}else {List<Patient> list = patientDtoListToPatientList(doctorDto.getPatientDtoList());if (list != null) {doctor.setPatientList(list);}}doctor.setSpecialty(doctorDto.getSpecialization());doctor.setId(doctorDto.getId());doctor.setName(doctorDto.getName());} }值得注意的是,由于患者列表是該模型中的子實體,因此患者列表也會進行更新。
數據類型轉換
MapStruct支持source和target屬性之間的數據類型轉換。它還提供了基本類型及其相應的包裝類之間的自動轉換。
自動類型轉換適用于:
基本類型及其對應的包裝類之間。比如, int 和 Integer, float 和 Float, long 和 Long,boolean 和 Boolean 等。
任意基本類型與任意包裝類之間。如 int 和 long, byte 和 Integer 等。
所有基本類型及包裝類與String之間。如 boolean 和 String, Integer 和 String, float 和 String 等。
枚舉和String之間。
Java大數類型(java.math.BigInteger, java.math.BigDecimal) 和Java基本類型(包括其包裝類)與String之間。
其它情況詳見MapStruct官方文檔。
因此,在生成映射器代碼的過程中,如果源字段和目標字段之間屬于上述任何一種情況,則MapStrcut會自行處理類型轉換。
日期轉換
日期進行轉換時,我們也可以使用 dateFormat 設置格式聲明。
@Mapper public interface PatientMapper {@Mapping(source = "dateOfBirth", target = "dateOfBirth", dateFormat = "dd/MMM/yyyy")Patient toModel(PatientDto patientDto); }生成的實現代碼形式大致如下:
public class PatientMapperImpl implements PatientMapper {@Overridepublic Patient toModel(PatientDto patientDto) {if (patientDto == null) {return null;}PatientBuilder patient = Patient.builder();if (patientDto.getDateOfBirth() != null) {patient.dateOfBirth(DateTimeFormatter.ofPattern("dd/MMM/yyyy").format(patientDto.getDateOfBirth()));}patient.id(patientDto.getId());patient.name(patientDto.getName());return patient.build();} }可以看到,這里使用了 dateFormat 聲明的日期格式。如果我們沒有聲明格式的話,MapStruct會使用 LocalDate的默認格式,大致如下:
if (patientDto.getDateOfBirth() != null) {patient.dateOfBirth(DateTimeFormatter.ISO_LOCAL_DATE.format(patientDto.getDateOfBirth())); }數字格式轉換
上面的例子中可以看到,在進行日期轉換的時候,可以通過dateFormat標志指定日期的格式。
除此之外,對于數字的轉換,也可以使用numberFormat指定顯示格式:
// 數字格式轉換示例@Mapping(source = "price", target = "price", numberFormat = "$#.00")枚舉映射
枚舉映射的工作方式與字段映射相同。MapStruct會對具有相同名稱的枚舉進行映射,這一點沒有問題。但是,對于具有不同名稱的枚舉項,我們需要使用@ValueMapping注解。同樣,這與普通類型的@Mapping注解也相似。
我們先創建兩個枚舉。第一個是 PaymentType:
public enum PaymentType {CASH,CHEQUE,CARD_VISA,CARD_MASTER,CARD_CREDIT }比如說,這是一個應用內可用的支付方式,現在我們要根據這些選項創建一個更一般、有限的識圖:
public enum PaymentTypeView {CASH,CHEQUE,CARD }現在,我們創建這兩個enum之間的映射器接口:
@Mapper public interface PaymentTypeMapper {PaymentTypeMapper INSTANCE = Mappers.getMapper(PaymentTypeMapper.class);@ValueMappings({@ValueMapping(source = "CARD_VISA", target = "CARD"),@ValueMapping(source = "CARD_MASTER", target = "CARD"),@ValueMapping(source = "CARD_CREDIT", target = "CARD")})PaymentTypeView paymentTypeToPaymentTypeView(PaymentType paymentType); }這個例子中,我們設置了一般性的CARD值,和更具體的 CARD_VISA, CARD_MASTER 和 CARD_CREDIT 。兩個枚舉間的枚舉項數量不匹配—— PaymentType 有5個值,而 PaymentTypeView 只有3個。
為了在這些枚舉項之間建立橋梁,我們可以使用@ValueMappings注解,該注解中可以包含多個@ValueMapping注解。這里,我們將source設置為三個具體枚舉項之一,并將target設置為CARD。
MapStruct自然會處理這些情況:
public class PaymentTypeMapperImpl implements PaymentTypeMapper {@Overridepublic PaymentTypeView paymentTypeToPaymentTypeView(PaymentType paymentType) {if (paymentType == null) {return null;}PaymentTypeView paymentTypeView;switch (paymentType) {case CARD_VISA: paymentTypeView = PaymentTypeView.CARD;break;case CARD_MASTER: paymentTypeView = PaymentTypeView.CARD;break;case CARD_CREDIT: paymentTypeView = PaymentTypeView.CARD;break;case CASH: paymentTypeView = PaymentTypeView.CASH;break;case CHEQUE: paymentTypeView = PaymentTypeView.CHEQUE;break;default: throw new IllegalArgumentException( "Unexpected enum constant: " + paymentType );}return paymentTypeView;} }CASH和CHEQUE默認轉換為對應值,特殊的 CARD 值通過switch循環處理。
但是,如果你要將很多值轉換為一個更一般的值,這種方式就有些不切實際了。其實我們不必手動分配每一個值,只需要讓MapStruct將所有剩余的可用枚舉項(在目標枚舉中找不到相同名稱的枚舉項),直接轉換為對應的另一個枚舉項。
可以通過 MappingConstants實現這一點:
@ValueMapping(source = MappingConstants.ANY_REMAINING, target = "CARD") PaymentTypeView paymentTypeToPaymentTypeView(PaymentType paymentType);在這個例子中,完成默認映射之后,所有剩余(未匹配)的枚舉項都會映射為CARD:
@Override public PaymentTypeView paymentTypeToPaymentTypeView(PaymentType paymentType) {if ( paymentType == null ) {return null;}PaymentTypeView paymentTypeView;switch ( paymentType ) {case CASH: paymentTypeView = PaymentTypeView.CASH;break;case CHEQUE: paymentTypeView = PaymentTypeView.CHEQUE;break;default: paymentTypeView = PaymentTypeView.CARD;}return paymentTypeView; }還有一種選擇是使用ANY UNMAPPED:
@ValueMapping(source = MappingConstants.ANY_UNMAPPED, target = "CARD") PaymentTypeView paymentTypeToPaymentTypeView(PaymentType paymentType);采用這種方式時,MapStruct不會像前面那樣先處理默認映射,再將剩余的枚舉項映射到target值。而是,直接將所有未通過@ValueMapping注解做顯式映射的值都轉換為target值。
集合映射
簡單來說,使用MapStruct處理集合映射的方式與處理簡單類型相同。
我們創建一個簡單的接口或抽象類并聲明映射方法。 MapStruct將根據我們的聲明自動生成映射代碼。 通常,生成的代碼會遍歷源集合,將每個元素轉換為目標類型,并將每個轉換后元素添加到目標集合中。
List映射
我們先定義一個新的映射方法:
@Mapper public interface DoctorMapper {List<DoctorDto> map(List<Doctor> doctor); }生成的代碼大致如下:
public class DoctorMapperImpl implements DoctorMapper {@Overridepublic List<DoctorDto> map(List<Doctor> doctor) {if ( doctor == null ) {return null;}List<DoctorDto> list = new ArrayList<DoctorDto>( doctor.size() );for ( Doctor doctor1 : doctor ) {list.add( doctorToDoctorDto( doctor1 ) );}return list;}protected DoctorDto doctorToDoctorDto(Doctor doctor) {if ( doctor == null ) {return null;}DoctorDto doctorDto = new DoctorDto();doctorDto.setId( doctor.getId() );doctorDto.setName( doctor.getName() );doctorDto.setSpecialization( doctor.getSpecialization() );return doctorDto;} }可以看到,MapStruct為我們自動生成了從Doctor到DoctorDto的映射方法。
但是需要注意,如果我們在DTO中新增一個字段fullName,生成代碼時會出現錯誤:
警告: Unmapped target property: "fullName".基本上,這意味著MapStruct在當前情況下無法為我們自動生成映射方法。因此,我們需要手動定義Doctor和DoctorDto之間的映射方法。具體參考之前的小節。
Set和Map映射
Set與Map型數據的處理方式與List相似。按照以下方式修改DoctorMapper:
@Mapper public interface DoctorMapper {Set<DoctorDto> setConvert(Set<Doctor> doctor);Map<String, DoctorDto> mapConvert(Map<String, Doctor> doctor); }生成的最終實現代碼如下:
public class DoctorMapperImpl implements DoctorMapper {@Overridepublic Set<DoctorDto> setConvert(Set<Doctor> doctor) {if ( doctor == null ) {return null;}Set<DoctorDto> set = new HashSet<DoctorDto>( Math.max( (int) ( doctor.size() / .75f ) + 1, 16 ) );for ( Doctor doctor1 : doctor ) {set.add( doctorToDoctorDto( doctor1 ) );}return set;}@Overridepublic Map<String, DoctorDto> mapConvert(Map<String, Doctor> doctor) {if ( doctor == null ) {return null;}Map<String, DoctorDto> map = new HashMap<String, DoctorDto>( Math.max( (int) ( doctor.size() / .75f ) + 1, 16 ) );for ( java.util.Map.Entry<String, Doctor> entry : doctor.entrySet() ) {String key = entry.getKey();DoctorDto value = doctorToDoctorDto( entry.getValue() );map.put( key, value );}return map;}protected DoctorDto doctorToDoctorDto(Doctor doctor) {if ( doctor == null ) {return null;}DoctorDto doctorDto = new DoctorDto();doctorDto.setId( doctor.getId() );doctorDto.setName( doctor.getName() );doctorDto.setSpecialization( doctor.getSpecialization() );return doctorDto;} }與List映射類似,MapStruct自動生成了Doctor轉換為DoctorDto的映射方法。
集合映射策略
很多場景中,我們需要對具有父子關系的數據類型進行轉換。通常來說,會有一個數據類型(父),其字段是另一個數據類型(子)的集合。
對于這種情況,MapStruct提供了一種方法來選擇如何將子類型設置或添加到父類型中。具體來說,就是@Mapper注解中的collectionMappingStrategy屬性,該屬性可以取值為ACCESSOR_ONLY, SETTER_PREFERRED, ADDER_PREFERRED 或TARGET_IMMUTABLE。
這些值分別表示不同的為子類型集合賦值的方式。默認值是ACCESSOR_ONLY,這意味著只能使用訪問器來設置子集合。
當父類型中的Collection字段setter方法不可用,但我們有一個子類型add方法時,這個選項就派上用場了;另一種有用的情況是父類型中的Collection字段是不可變的。
我們新建一個類:
public class Hospital {
private List doctors;
// getters and setters or builder
}
同時定義一個映射目標DTO類,同時定義子類型集合字段的getter、setter和adder:
public class HospitalDto {private List<DoctorDto> doctors;// 子類型集合字段getterpublic List<DoctorDto> getDoctors() {return doctors;}// 子類型集合字段setterpublic void setDoctors(List<DoctorDto> doctors) {this.doctors = doctors;}// 子類型數據adderpublic void addDoctor(DoctorDto doctorDTO) {if (doctors == null) {doctors = new ArrayList<>();}doctors.add(doctorDTO);} }創建對應的映射器:
@Mapper(uses = DoctorMapper.class) public interface HospitalMapper {HospitalMapper INSTANCE = Mappers.getMapper(HospitalMapper.class);HospitalDto toDto(Hospital hospital); }生成的最終實現代碼為:
public class HospitalMapperImpl implements HospitalMapper {@Overridepublic HospitalDto toDto(Hospital hospital) {if ( hospital == null ) {return null;}HospitalDto hospitalDto = new HospitalDto();hospitalDto.setDoctors( doctorListToDoctorDtoList( hospital.getDoctors() ) );return hospitalDto;} }可以看到,在默認情況下采用的策略是ACCESSOR_ONLY,使用setter方法setDoctors()向HospitalDto對象中寫入列表數據。
相對的,如果使用 ADDER_PREFERRED 作為映射策略:
@Mapper(collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED,uses = DoctorMapper.class) public interface HospitalMapper {HospitalMapper INSTANCE = Mappers.getMapper(HospitalMapper.class);HospitalDto toDto(Hospital hospital); }此時,會使用adder方法逐個將轉換后的子類型DTO對象加入父類型的集合字段中。
public class CompanyMapperAdderPreferredImpl implements CompanyMapperAdderPreferred {private final EmployeeMapper employeeMapper = Mappers.getMapper( EmployeeMapper.class );@Overridepublic CompanyDTO map(Company company) {if ( company == null ) {return null;}CompanyDTO companyDTO = new CompanyDTO();if ( company.getEmployees() != null ) {for ( Employee employee : company.getEmployees() ) {companyDTO.addEmployee( employeeMapper.map( employee ) );}}return companyDTO;} }如果目標DTO中既沒有setter方法也沒有adder方法,會先通過getter方法獲取子類型集合,再調用集合的對應接口添加子類型對象。
可以在參考文檔中看到不同類型的DTO定義(是否包含setter方法或adder方法),采用不同的映射策略時,所使用的添加子類型到集合中的方式。
目標集合實現類型
MapStruct支持將集合接口作為映射方法的目標類型。
在這種情況下,在生成的代碼中會使用一些集合接口默認實現。 例如,上面的示例中,List的默認實現是ArrayList。
常見接口及其對應的默認實現如下:
你可以在參考文檔中找到MapStruct支持的所有接口列表,以及每個接口對應的默認實現類型。
進階操作
依賴注入
到目前為止,我們一直在通過getMapper()方法訪問生成的映射器:
DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class);但是,如果你使用的是Spring,只需要簡單修改映射器配置,就可以像常規依賴項一樣注入映射器。
修改 DoctorMapper 以支持Spring框架:
@Mapper(componentModel = "spring") public interface DoctorMapper {}在@Mapper注解中添加(componentModel = “spring”),是為了告訴MapStruct,在生成映射器實現類時,我們希望它能支持通過Spring的依賴注入來創建。現在,就不需要在接口中添加 INSTANCE 字段了。
這次生成的 DoctorMapperImpl 會帶有 @Component 注解:
@Component public class DoctorMapperImpl implements DoctorMapper {}只要被標記為@Component,Spring就可以把它作為一個bean來處理,你就可以在其它類(如控制器)中通過@Autowire注解來使用它:
@Controller public class DoctorController() {@Autowiredprivate DoctorMapper doctorMapper; }如果你不使用Spring, MapStruct也支持Java CDI:
@Mapper(componentModel = "cdi") public interface DoctorMapper {}添加默認值
@Mapping 注解有兩個很實用的標志就是常量 constant 和默認值 defaultValue 。無論source如何取值,都將始終使用常量值; 如果source取值為null,則會使用默認值。
修改一下 DoctorMapper ,添加一個 constant 和一個 defaultValue :
@Mapper(uses = {PatientMapper.class}, componentModel = "spring") public interface DoctorMapper {@Mapping(target = "id", constant = "-1")@Mapping(source = "doctor.patientList", target = "patientDtoList")@Mapping(source = "doctor.specialty", target = "specialization", defaultValue = "Information Not Available")DoctorDto toDto(Doctor doctor); }如果specialty不可用,我們會替換為"Information Not Available"字符串,此外,我們將id硬編碼為-1。
生成代碼如下:
@Component public class DoctorMapperImpl implements DoctorMapper {@Autowiredprivate PatientMapper patientMapper;@Overridepublic DoctorDto toDto(Doctor doctor) {if (doctor == null) {return null;}DoctorDto doctorDto = new DoctorDto();if (doctor.getSpecialty() != null) {doctorDto.setSpecialization(doctor.getSpecialty());}else {doctorDto.setSpecialization("Information Not Available");}doctorDto.setPatientDtoList(patientListToPatientDtoList(doctor.getPatientList()));doctorDto.setName(doctor.getName());doctorDto.setId(-1);return doctorDto;} }可以看到,如果 doctor.getSpecialty() 返回值為null,則將specialization設置為我們的默認信息。無論任何情況,都會對 id賦值,因為這是一個constant。
添加表達式
MapStruct甚至允許在@Mapping注解中輸入Java表達式。你可以設置 defaultExpression ( source 取值為 null時生效),或者一個expression(類似常量,永久生效)。
在 Doctor 和 DoctorDto兩個類中都加了兩個新屬性,一個是 String 類型的 externalId ,另一個是LocalDateTime類型的 appointment ,兩個類大致如下:
public class Doctor {private int id;private String name;private String externalId;private String specialty;private LocalDateTime availability;private List<Patient> patientList;// getters and setters or builder }public class DoctorDto {private int id;private String name;private String externalId;private String specialization;private LocalDateTime availability;private List<PatientDto> patientDtoList;// getters and setters or builder }修改 DoctorMapper:
@Mapper(uses = {PatientMapper.class}, componentModel = "spring", imports = {LocalDateTime.class, UUID.class}) public interface DoctorMapper {@Mapping(target = "externalId", expression = "java(UUID.randomUUID().toString())")@Mapping(source = "doctor.availability", target = "availability", defaultExpression = "java(LocalDateTime.now())")@Mapping(source = "doctor.patientList", target = "patientDtoList")@Mapping(source = "doctor.specialty", target = "specialization")DoctorDto toDtoWithExpression(Doctor doctor); }可以看到,這里將 externalId的值設置為 java(UUID.randomUUID().toString()) ,如果源對象中沒有 availability 屬性,則會把目標對象中的 availability 設置為一個新的 LocalDateTime對象。
由于表達式只是字符串,我們必須在表達式中指定使用的類。但是這里的表達式并不是最終執行的代碼,只是一個字母的文本值。因此,我們要在 @Mapper 中添加 imports = {LocalDateTime.class, UUID.class} 。
添加自定義方法
到目前為止,我們一直使用的策略是添加一個“占位符”方法,并期望MapStruct能為我們實現它。其實我們還可以向接口中添加自定義的default方法,也可以通過default方法直接實現一個映射。然后我們可以通過實例直接調用該方法,沒有任何問題。
為此,我們創建一個 DoctorPatientSummary類,其中包含一個 Doctor 及其 Patient列表的匯總信息:
public class DoctorPatientSummary {private int doctorId;private int patientCount;private String doctorName;private String specialization;private String institute;private List<Integer> patientIds;// getters and setters or builder }接下來,我們在 DoctorMapper中添加一個default方法,該方法會將 Doctor 和 Education 對象轉換為一個 DoctorPatientSummary:
@Mapper public interface DoctorMapper {default DoctorPatientSummary toDoctorPatientSummary(Doctor doctor, Education education) {return DoctorPatientSummary.builder().doctorId(doctor.getId()).doctorName(doctor.getName()).patientCount(doctor.getPatientList().size()).patientIds(doctor.getPatientList().stream().map(Patient::getId).collect(Collectors.toList())).institute(education.getInstitute()).specialization(education.getDegreeName()).build();} }這里使用了Builder模式創建DoctorPatientSummary對象。
在MapStruct生成映射器實現類之后,你就可以使用這個實現方法,就像訪問任何其它映射器方法一樣:
DoctorPatientSummary summary = doctorMapper.toDoctorPatientSummary(dotor, education);創建自定義映射器
前面我們一直是通過接口來設計映射器功能,其實我們也可以通過一個帶 @Mapper 的 abstract 類來實現一個映射器。MapStruct也會為這個類創建一個實現,類似于創建一個接口實現。
我們重寫一下前面的示例,這一次,我們將它修改為一個抽象類:
@Mapper public abstract class DoctorCustomMapper {public DoctorPatientSummary toDoctorPatientSummary(Doctor doctor, Education education) {return DoctorPatientSummary.builder().doctorId(doctor.getId()).doctorName(doctor.getName()).patientCount(doctor.getPatientList().size()).patientIds(doctor.getPatientList().stream().map(Patient::getId).collect(Collectors.toList())).institute(education.getInstitute()).specialization(education.getDegreeName()).build();} }你可以用同樣的方式使用這個映射器。由于限制較少,使用抽象類可以在創建自定義實現時給我們更多的控制和選擇。另一個好處是可以添加@BeforeMapping和@AfterMapping方法。
@BeforeMapping 和 @AfterMapping
為了進一步控制和定制化,我們可以定義 @BeforeMapping 和 @AfterMapping方法。顯然,這兩個方法是在每次映射之前和之后執行的。也就是說,在最終的實現代碼中,會在兩個對象真正映射之前和之后添加并執行這兩個方法。
可以在 DoctorCustomMapper中添加兩個方法:
@Mapper(uses = {PatientMapper.class}, componentModel = "spring") public abstract class DoctorCustomMapper {@BeforeMappingprotected void validate(Doctor doctor) {if(doctor.getPatientList() == null){doctor.setPatientList(new ArrayList<>());}}@AfterMappingprotected void updateResult(@MappingTarget DoctorDto doctorDto) {doctorDto.setName(doctorDto.getName().toUpperCase());doctorDto.setDegree(doctorDto.getDegree().toUpperCase());doctorDto.setSpecialization(doctorDto.getSpecialization().toUpperCase());}@Mapping(source = "doctor.patientList", target = "patientDtoList")@Mapping(source = "doctor.specialty", target = "specialization")public abstract DoctorDto toDoctorDto(Doctor doctor); }基于該抽象類生成一個映射器實現類:、
@Component public class DoctorCustomMapperImpl extends DoctorCustomMapper {@Autowiredprivate PatientMapper patientMapper;@Overridepublic DoctorDto toDoctorDto(Doctor doctor) {validate(doctor);if (doctor == null) {return null;}DoctorDto doctorDto = new DoctorDto();doctorDto.setPatientDtoList(patientListToPatientDtoList(doctor.getPatientList()));doctorDto.setSpecialization(doctor.getSpecialty());doctorDto.setId(doctor.getId());doctorDto.setName(doctor.getName());updateResult(doctorDto);return doctorDto;} }可以看到, validate() 方法會在 DoctorDto 對象實例化之前執行,而updateResult()方法會在映射結束之后執行。
映射異常處理
異常處理是不可避免的,應用程序隨時會產生異常狀態。MapStruct提供了對異常處理的支持,可以簡化開發者的工作。
考慮這樣一個場景,我們想在 Doctor 映射為DoctorDto之前校驗一下 Doctor 的數據。我們新建一個獨立的 Validator 類進行校驗:
public class Validator {public int validateId(int id) throws ValidationException {if(id == -1){throw new ValidationException("Invalid value in ID");}return id;} }我們修改一下 DoctorMapper 以使用 Validator 類,無需指定實現。跟之前一樣, 在@Mapper使用的類列表中添加該類。我們還需要做的就是告訴MapStruct我們的 toDto() 會拋出 throws ValidationException:
@Mapper(uses = {PatientMapper.class, Validator.class}, componentModel = "spring") public interface DoctorMapper {@Mapping(source = "doctor.patientList", target = "patientDtoList")@Mapping(source = "doctor.specialty", target = "specialization")DoctorDto toDto(Doctor doctor) throws ValidationException; }最終生成的映射器代碼如下:
@Component public class DoctorMapperImpl implements DoctorMapper {@Autowiredprivate PatientMapper patientMapper;@Autowiredprivate Validator validator;@Overridepublic DoctorDto toDto(Doctor doctor) throws ValidationException {if (doctor == null) {return null;}DoctorDto doctorDto = new DoctorDto();doctorDto.setPatientDtoList(patientListToPatientDtoList(doctor.getPatientList()));doctorDto.setSpecialization(doctor.getSpecialty());doctorDto.setId(validator.validateId(doctor.getId()));doctorDto.setName(doctor.getName());doctorDto.setExternalId(doctor.getExternalId());doctorDto.setAvailability(doctor.getAvailability());return doctorDto;} }MapStruct自動將doctorDto的id設置為Validator實例的方法返回值。它還在該方法簽名中添加了一個throws子句。
注意,如果映射前后的一對屬性的類型與Validator中的方法出入參類型一致,那該字段映射時就會調用Validator中的方法,所以該方式請謹慎使用。
映射配置
MapStruct為編寫映射器方法提供了一些非常有用的配置。多數情況下,如果我們已經定義了兩個類型之間的映射方法,當我們要添加相同類型之間的另一個映射方法時,我們往往會直接復制已有方法的映射配置。
其實我們不必手動復制這些注解,只需要簡單的配置就可以創建一個相同/相似的映射方法。
繼承配置
我們回顧一下“更新現有實例”,在該場景中,我們創建了一個映射器,根據DoctorDto對象的屬性更新現有的Doctor對象的屬性值:
@Mapper(uses = {PatientMapper.class}) public interface DoctorMapper {DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class);@Mapping(source = "doctorDto.patientDtoList", target = "patientList")@Mapping(source = "doctorDto.specialization", target = "specialty")void updateModel(DoctorDto doctorDto, @MappingTarget Doctor doctor); }假設我們還有另一個映射器,將 DoctorDto轉換為 Doctor :
@Mapper(uses = {PatientMapper.class, Validator.class}) public interface DoctorMapper {@Mapping(source = "doctorDto.patientDtoList", target = "patientList")@Mapping(source = "doctorDto.specialization", target = "specialty")Doctor toModel(DoctorDto doctorDto); }這兩個映射方法使用了相同的注解配置, source和 target都是相同的。其實我們可以使用@InheritConfiguration注釋,從而避免這兩個映射器方法的重復配置。
如果對一個方法添加 @InheritConfiguration 注解,MapStruct會檢索其它的已配置方法,尋找可用于當前方法的注解配置。一般來說,這個注解都用于mapping方法后面的update方法,如下所示:
@Mapper(uses = {PatientMapper.class, Validator.class}, componentModel = "spring") public interface DoctorMapper {@Mapping(source = "doctorDto.specialization", target = "specialty")@Mapping(source = "doctorDto.patientDtoList", target = "patientList")Doctor toModel(DoctorDto doctorDto);@InheritConfigurationvoid updateModel(DoctorDto doctorDto, @MappingTarget Doctor doctor); }繼承逆向配置
還有另外一個類似的場景,就是編寫映射函數將***Model*** 轉為 DTO,以及將 DTO 轉為 Model。如下面的代碼所示,我們必須在兩個函數上添加相同的注釋。
@Mapper(componentModel = "spring") public interface PatientMapper {@Mapping(source = "dateOfBirth", target = "dateOfBirth", dateFormat = "dd/MMM/yyyy")Patient toModel(PatientDto patientDto);@Mapping(source = "dateOfBirth", target = "dateOfBirth", dateFormat = "dd/MMM/yyyy")PatientDto toDto(Patient patient); }兩個方法的配置不會是完全相同的,實際上,它們應該是相反的。將***Model*** 轉為 DTO,以及將 DTO 轉為 Model——映射前后的字段相同,但是源屬性字段與目標屬性字段是相反的。
我們可以在第二個方法上使用@InheritInverseConfiguration注解,避免寫兩遍映射配置:
@Mapper(componentModel = "spring") public interface PatientMapper {@Mapping(source = "dateOfBirth", target = "dateOfBirth", dateFormat = "dd/MMM/yyyy")Patient toModel(PatientDto patientDto);@InheritInverseConfigurationPatientDto toDto(Patient patient); }這兩個Mapper生成的代碼是相同的。
總結
以上是生活随笔為你收集整理的MapStruct使用指南的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 昨夜的雨图片
- 下一篇: html函数splice,js数组的常用