Java數據對象映射庫MapStruct介紹(2)

隨著微服務和分布式應用的廣泛采用,出于服務的獨立性和數據的安全性方面的考慮膳汪,每個服務都會按照自己的需要定義業(yè)務數據對象唯蝶,這樣當服務相互調用時就要經常進行數據對象之間的映射。目前遗嗽,有很多實現數據對象映射的庫粘我,本文介紹一種高性能的映射庫MapStruct

https://mapstruct.org/documentation/stable/reference/html/

Java數據對象映射庫MapStruct介紹(1)

MapStruct簡介

MapStruct是在編譯時根據定義(接口)生成映射類(實現)痹换,自動生成需要手工編寫數據映射代碼征字,通過直接調用復制數據,不需要通過反射娇豫,因此速度非吵捉快。

本文將介紹一些MapStruct的高級功能冯痢,包括:

  • 更新已經存在的實例
  • 添加自定義映射方法
  • 創(chuàng)建自定義映射器
  • 異常處理
  • 復用映射關系(繼承氮昧,反向繼承)

更新已經存在的實例

有時候,我們想用源數據對象更新已經存在的數據對象浦楣,而不是新建對象袖肥,這時需要用@MappingTarget指定目標對象。

DoctorMapper中添加新的映射方法振劳。

@Mapper(componentModel = "spring", imports = { LocalDateTime.class, UUID.class })
public interface DoctorMapper {
  /*添加了新的映射方法椎组,用@MappingTarget更新目標對象*/
  @Mapping(source = "doctorDto.patientDtoList", target = "patientList")
  @Mapping(source = "doctorDto.specialization", target = "specialty")
  void updateModel(DoctorDto doctorDto, @MappingTarget Doctor doctor); // @MappingTarget指定了要更新的數據對象
}

生成的代碼將目標對象作為參數傳入映射方法。

  public 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.setAvailability(doctorDto.getAvailability());
    doctor.setExternalId(doctorDto.getExternalId());
    doctor.setId(doctorDto.getId());
    doctor.setName(doctorDto.getName());
  }

注意:patientList也進行了更新历恐。

添加自定義映射方法

之前我們是添加接口方法讓MapStruct自動生成實現代碼寸癌,也可以用default關鍵字在接口中添加手工編寫的方法专筷。

@Data
public class DoctorPatientSummary {
  private int doctorId;
  private int patientCount;
  private String doctorName;
  private String specialization;
  private String institute;
  private List<Integer> patientIds;
}

DoctorMapper中添加自定義的映射方法。

@Mapper(componentModel = "spring", imports = { LocalDateTime.class, UUID.class })
public interface DoctorMapper {
  /*省略了之前定義的映射函數*/

  /*手工指定映射關系*/
  default DoctorPatientSummary toDoctorPatientSummary(Doctor doctor, Education education) {
    DoctorPatientSummary summary = new DoctorPatientSummary();
    summary.setDoctorId(doctor.getId());
    summary.setDoctorName(doctor.getName());
    summary.setPatientCount(doctor.getPatientList().size());
    summary.setPatientIds(doctor.getPatientList().stream().map(Patient::getId).collect(Collectors.toList()));
    summary.setInstitute(education.getInstitute());
    summary.setSpecialization(education.getDegreeName());

    return summary;
  }
}

MapStruct自動生成的實現類實現了映射接口蒸苇,因此可以在實現類的實例中直接調用該方法磷蛹。

創(chuàng)建自定義映射器

除了通過接口定義映射,還可以用抽象類(abstract)實現填渠,MapStruct也會自動生成實現類弦聂。

@Mapper(componentModel = "spring")
public abstract class DoctorCustomMapper {

  public DoctorPatientSummary toDoctorPatientSummary(Doctor doctor, Education education) {
    DoctorPatientSummary summary = new DoctorPatientSummary();
    summary.setDoctorId(doctor.getId());
    summary.setDoctorName(doctor.getName());
    summary.setPatientCount(doctor.getPatientList().size());
    summary.setPatientIds(doctor.getPatientList().stream().map(Patient::getId).collect(Collectors.toList()));
    summary.setInstitute(education.getInstitute());
    summary.setSpecialization(education.getDegreeName());

    return summary;
  }
}

自動生成的實現類鸟辅。

@Component
public class DoctorCustomMapperImpl extends DoctorCustomMapper {}

通過抽象類實現的映射類還可以用@BeforeMapping@AfterMapping添加映射前和映射后執(zhí)行的方法氛什。

@Mapper(componentModel = "spring")
public abstract class DoctorCustomMapper {

  @BeforeMapping
  protected void validate(Doctor doctor) {
    if (doctor.getPatientList() == null) {
      doctor.setPatientList(new ArrayList<>());
    }
  }

  @AfterMapping
  protected void updateResult(@MappingTarget DoctorDto doctorDto) {
    doctorDto.setName(doctorDto.getName().toUpperCase());
    doctorDto.setDegree(doctorDto.getDegree().toUpperCase());
    doctorDto.setSpecialization(doctorDto.getSpecialization().toUpperCase());
  }

  /*抽象方法,由MapStruct生成實現代碼*/
  @Mapping(source = "doctor.patientList", target = "patientDtoList")
  @Mapping(source = "doctor.specialty", target = "specialization")
  public abstract DoctorDto toDoctorDto(Doctor doctor);

  /*省略其它定義*/
}
@Component
public class DoctorCustomMapperImpl extends DoctorCustomMapper {
  public DoctorDto toDoctorDto(Doctor doctor) {
    validate(doctor); // 調用了@BeforeMapping注解的方法
    if (doctor == null)
      return null; 
    DoctorDto doctorDto = new DoctorDto();
    doctorDto.setPatientDtoList(patientListToPatientDtoList(doctor.getPatientList()));
    doctorDto.setSpecialization(doctor.getSpecialty());
    doctorDto.setAvailability(doctor.getAvailability());
    doctorDto.setExternalId(doctor.getExternalId());
    doctorDto.setId(doctor.getId());
    doctorDto.setName(doctor.getName());
    updateResult(doctorDto);
    return doctorDto; // 調用了@AfterMapping注解的方法
  }
 /*省略了生成的其它代碼*/
}

異常處理

數據對象轉換過程中難免會發(fā)生異常匪凉,MapStruct也支持進行異常處理枪眉。

假設我們有一個進行數據檢查的類Validator,檢查失敗會拋出自定義異常ValidationException再层。

public class Validator {
  public int validateId(int id) throws ValidationException {
    if (id == -1) {
      throw new ValidationException("Invalid value in ID");
    }
    return id;
  }
}

在定義的映射方法上添加拋出異常贸铜。

@Mapper(componentModel = "spring", uses = { PatientMapper.class, Validator.class }, imports = { LocalDateTime.class, UUID.class })
public interface DoctorMapper {

  /*省略自動生成的內容*/
  DoctorDto toDto(Doctor doctor, Education education) throws ValidationException; // 方法拋出異常

  /*省略自動生成的內容*/
}
@Component
public class DoctorMapperImpl implements DoctorMapper {
  @Autowired
  private PatientMapper patientMapper;
  
  @Autowired
  private Validator validator;
  
  public DoctorDto toDto(Doctor doctor, Education education) throws ValidationException {
    if (doctor == null && education == null)
      return null; 
    DoctorDto doctorDto = new DoctorDto();
    if (doctor != null) {
      if (doctor.getAvailability() != null) {
        doctorDto.setAvailability(doctor.getAvailability());
      } else {
        doctorDto.setAvailability(LocalDateTime.now());
      } 
      if (doctor.getSpecialty() != null) {
        doctorDto.setSpecialization(doctor.getSpecialty());
      } else {
        doctorDto.setSpecialization("沒有指定");
      } 
      doctorDto.setPatientDtoList(patientListToPatientDtoList(doctor.getPatientList()));
      doctorDto.setId(this.validator.validateId(doctor.getId())); // 這里會檢查id的合法性,不合法拋異常
      doctorDto.setName(doctor.getName());
    } 
    if (education != null)
      doctorDto.setDegree(education.getDegreeName()); 
    doctorDto.setExternalId(UUID.randomUUID().toString());
    return doctorDto;
  }
  

復用映射關系

有時候多個映射方法使用相同的映射關系聂受,例如:生成新數據對象或更新現有數據對象的方法蒿秦。

@Mapper(uses = { PatientMapper.class }, componentModel = "spring")
public interface DoctorInheritMapper {
  @Mapping(source = "doctorDto.specialization", target = "specialty")
  @Mapping(source = "doctorDto.patientDtoList", target = "patientList")
  Doctor toModel(DoctorDto doctorDto);

  @InheritConfiguration
  void updateModel(DoctorDto doctorDto, @MappingTarget Doctor doctor);
}
@Component
public class DoctorInheritMapperImpl implements DoctorInheritMapper {
  public Doctor toModel(DoctorDto doctorDto) {
    if (doctorDto == null)
      return null; 
    Doctor doctor = new Doctor();
    doctor.setSpecialty(doctorDto.getSpecialization());
    doctor.setPatientList(patientDtoListToPatientList(doctorDto.getPatientDtoList()));
    doctor.setAvailability(doctorDto.getAvailability());
    doctor.setExternalId(doctorDto.getExternalId());
    doctor.setId(doctorDto.getId());
    doctor.setName(doctorDto.getName());
    return doctor;
  }
  /*這里按照繼承的映射關系生成了代碼*/
  public void updateModel(DoctorDto doctorDto, Doctor doctor) {
    if (doctorDto == null)
      return; 
    doctor.setSpecialty(doctorDto.getSpecialization());
    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.setAvailability(doctorDto.getAvailability());
    doctor.setExternalId(doctorDto.getExternalId());
    doctor.setId(doctorDto.getId());
    doctor.setName(doctorDto.getName());
  }
  /*省略生成的其他代碼*/
}

還有一種用到繼承的情況是需要按照一套規(guī)則進行雙向的數據對象轉換。

@Mapper(componentModel = "spring")
public interface PatientMapper {
  @Mapping(source = "dateOfBirth", target = "dateOfBirth", dateFormat = "dd/MMM/yyyy")
  PatientDto toDto(Patient patient);

  @InheritInverseConfiguration
  Patient toModel(PatientDto patientDto);
}
@Component
public class PatientMapperImpl implements PatientMapper {
  public PatientDto toDto(Patient patient) {
    if (patient == null)
      return null; 
    PatientDto patientDto = new PatientDto();
    if (patient.getDateOfBirth() != null)
      patientDto.setDateOfBirth(LocalDate.parse(patient.getDateOfBirth(), DateTimeFormatter.ofPattern("dd/MMM/yyyy"))); 
    patientDto.setId(patient.getId());
    patientDto.setName(patient.getName());
    return patientDto;
  }
  
  /*根據繼承的反向規(guī)則生成的代碼*/
  public Patient toModel(PatientDto patientDto) {
    if (patientDto == null)
      return null; 
    Patient patient = new Patient();
    if (patientDto.getDateOfBirth() != null)
      patient.setDateOfBirth(DateTimeFormatter.ofPattern("dd/MMM/yyyy").format(patientDto.getDateOfBirth())); 
    patient.setId(patientDto.getId());
    patient.setName(patientDto.getName());
    return patient;
  }
}

總結

MapStruct是一個非常強大的數據對象映射庫蛋济,除了減少手工代碼的編寫量棍鳖,還為我們提供了一個進行數據對象映射的基礎框架。MapStruct還有很多用法碗旅,需要不斷研究和實踐渡处。

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市祟辟,隨后出現的幾起案子医瘫,更是在濱河造成了極大的恐慌,老刑警劉巖旧困,帶你破解...
    沈念sama閱讀 217,406評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件醇份,死亡現場離奇詭異,居然都是意外死亡吼具,警方通過查閱死者的電腦和手機僚纷,發(fā)現死者居然都...
    沈念sama閱讀 92,732評論 3 393
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來馍悟,“玉大人畔濒,你說我怎么就攤上這事÷嘀洌” “怎么了侵状?”我有些...
    開封第一講書人閱讀 163,711評論 0 353
  • 文/不壞的土叔 我叫張陵赞弥,是天一觀的道長。 經常有香客問我趣兄,道長绽左,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,380評論 1 293
  • 正文 為了忘掉前任艇潭,我火速辦了婚禮拼窥,結果婚禮上,老公的妹妹穿的比我還像新娘蹋凝。我一直安慰自己鲁纠,他們只是感情好,可當我...
    茶點故事閱讀 67,432評論 6 392
  • 文/花漫 我一把揭開白布鳍寂。 她就那樣靜靜地躺著改含,像睡著了一般。 火紅的嫁衣襯著肌膚如雪迄汛。 梳的紋絲不亂的頭發(fā)上捍壤,一...
    開封第一講書人閱讀 51,301評論 1 301
  • 那天,我揣著相機與錄音鞍爱,去河邊找鬼鹃觉。 笑死,一個胖子當著我的面吹牛睹逃,可吹牛的內容都是我干的盗扇。 我是一名探鬼主播,決...
    沈念sama閱讀 40,145評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼唯卖,長吁一口氣:“原來是場噩夢啊……” “哼粱玲!你這毒婦竟也來了?” 一聲冷哼從身側響起拜轨,我...
    開封第一講書人閱讀 39,008評論 0 276
  • 序言:老撾萬榮一對情侶失蹤抽减,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后橄碾,有當地人在樹林里發(fā)現了一具尸體卵沉,經...
    沈念sama閱讀 45,443評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,649評論 3 334
  • 正文 我和宋清朗相戀三年法牲,在試婚紗的時候發(fā)現自己被綠了史汗。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,795評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡拒垃,死狀恐怖停撞,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤戈毒,帶...
    沈念sama閱讀 35,501評論 5 345
  • 正文 年R本政府宣布艰猬,位于F島的核電站,受9級特大地震影響埋市,放射性物質發(fā)生泄漏冠桃。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,119評論 3 328
  • 文/蒙蒙 一道宅、第九天 我趴在偏房一處隱蔽的房頂上張望食听。 院中可真熱鬧,春花似錦污茵、人聲如沸樱报。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽肃弟。三九已至,卻和暖如春零蓉,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背穷缤。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評論 1 269
  • 我被黑心中介騙來泰國打工敌蜂, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人津肛。 一個月前我還...
    沈念sama閱讀 47,899評論 2 370
  • 正文 我出身青樓章喉,卻偏偏與公主長得像,于是被迫代替她去往敵國和親身坐。 傳聞我的和親對象是個殘疾皇子秸脱,可洞房花燭夜當晚...
    茶點故事閱讀 44,724評論 2 354

推薦閱讀更多精彩內容