Dybt dybt ned i Mapstruct @ Spring

(Miguel Duque) (14. december 2020)

Foto af Beau Swierstra på Unsplash

At være Java-udvikler ved du bestemt, at kortlægning af POJOer er en standardopgave, når du udvikler flerlagsapplikationer.
Skrivning af disse kortlægninger manuelt, udover at være kedeligt ubehagelig opgave for udviklere, er også fejlbehæftet.

MapStruct er et open source Java-bibliotek, der genererer mapper-klasseimplementeringer under kompilering i en sikker og nem måde.

I løbet af denne artikel følger vi et eksempel på, hvordan man drager fordel af dette kraftfulde bibliotek for i høj grad at reducere den mængde kedelpladekode, der regelmæssigt ville blive skrevet i hånden.

Indeks

  • Grundlæggende tilknytninger
  • Kortlægning af andet egenskabsnavn
  • Sørg for, at hver ejendom er kortlagt
  • Kortlægning af underordnet enhedegenskab
  • Kortlægning af den fulde underordnede enhed – Brug af en anden kortlægger
  • Kortlægning med en tilpasset metode
  • @BeforeMapping @AfterMapping
  • Kortlægning med en yderligere parameter
  • Afhængighedsindsprøjtning på kortlægningsmetoder
  • Opdateringer
  • Patchopdateringer

Grundlæggende tilknytninger

Lad os starte vores ansøgning med en grundlæggende model, der indeholder klassen Læge. Vores service henter denne klasse fra modelaget og returnerer derefter en DoctorDto klasse.

Modelklasse:

@Data
public class Doctor {
private int id;
private String name;
}

Dto-klasse:

@Data
public class DoctorDto {
private int id;
private String name;
}

For at gøre dette skal vi oprette vores Mapper-interface:

@Mapper(componentModel = "spring")
public interface DoctorMapper {
DoctorDto toDto(Doctor doctor);
}

Da begge klasser har de samme egenskabsnavne ( id og navn ), mapstruct inkluderer kortlægning af begge felter i den genererede klasse:

@Component
public class DoctorMapperImpl implements DoctorMapper {

@Override
public DoctorDto toDto(Doctor doctor) {
if ( doctor == null ) {
return null;
}

DoctorDto doctorDto = new DoctorDto();

doctorDto.setId( doctor.getId() );
doctorDto.setName( doctor.getName() );

return doctorDto;
}
}

Ved at tilføje componentModel = “Spring” , den genererede kortlægger vil være en springbønne og kan hentes med @Autowired -noteringen som enhver anden bønne:

@Service
public class DoctorService {

private final DoctorMapper doctorMapper;
private final DoctorRepository doctorRepository;

@Autowired
public DoctorService(DoctorMapper doctorMapper) {
this.doctorMapper = doctorMapper;
this.doctorRepository = doctorRepository;
}

public DoctorDto getDoctor(Integer id) {
Doctor doctor = doctorRepository.findById(id);
return doctorMapper.toDto(doctor);
}
}

Kortlægning af andet egenskabsnavn

Hvis vi inkluderer en egenskab telefon i Læge klasse:

@Data
public class Doctor {
private int id;
private String name;
private String phone;
}

Skal knyttes til kontakt i Læge Dto :

@Data
public class DoctorDto {
private int id;
private String name;
private String contact;
}

Vores kortlægger kan ikke kortlægge det automatisk. For at gøre dette skal vi oprette en regel til denne kortlægning:

@Mapper(componentModel = "spring")
public interface DoctorMapper {

@Mapping(source = "phone", target = "contact")
DoctorDto toDto(Doctor doctor);
}

Sørg for, at hver ejendom er kortlagt

Hvis vi vil for at garantere, at vi ikke glemmer at kortlægge nogen målejendom, kan vi konfigurere indstillingen unmappedTargetPolicy på vores kortlægger:

@Mapper(
componentModel = "spring",
unmappedTargetPolicy = ReportingPolicy.ERROR)
public interface DoctorMapper {

@Mapping(source = "phone", target = "contact")
DoctorDto toDto(Doctor doctor);
}

Hvis vi fjerner

@Mapping (source = “phone”, target = “contact”)

med denne konfiguration, vil vores kode mislykkes under kompilering med fejlen:

Ikke-kortlagt målegenskab: “kontakt”. DoctorDto toDto (Doctor doctor);

Hvis vi af en eller anden grund vil ignorere et mål egenskab, kan vi tilføje:

@Mapping (target = “contact”, ignore = true)

På samme måde kan vi også garantere, at alle kilder egenskaber kortlægges ved at konfigurere indstillingen unmappedSourcePolicy .

Kortlægning af underordnet enhedegenskab

Det meste af tiden den klasse, vi har brug for til kort indeholder underordnede objekter . For eksempel:

@Data
public class Doctor {
private int id;
private String name;
private String phone;
private Speciality speciality;
}@Data
public class Speciality {
private int id;
private String name;
}

Og i vores Dto har vi bare brug for navnet i stedet for den fulde specialitet:

@Data
public class DoctorDto {
private int id;
private String name;
private String contact;
private String specialityName;
}

Denne situation er også ligetil med mapstruct :

@Mapping(source = "phone", target = "contact")
@Mapping(source = "speciality.name", target = "specialityName")
DoctorDto toDto(Doctor doctor);

Kortlægning af den fulde underordnede enhed – Brug af en anden kortlægger

Som før har vores Læge klasse et underobjekt Adresse :

@Data
public class Doctor {
private int id;
private String name;
private String phone;
private Speciality speciality;
private Address address;
}

Men i dette tilfælde vil vi kortlægge det til en ny objekt i vores DoctorDto klasse:

@Data
public class DoctorDto {
private int id;
private String name;
private String contact;
private String specialityName;
private AddressDto address;
}

Til udfør kortlægningen mellem adresse og AddressDto-klassen, skal vi oprette en anden kortlæggergrænseflade:

@Mapper(componentModel = "spring")
public interface AddressMapper {
AddressDto toDto(Address address);
}

Derefter i vores DoctorMapper vi skal sørge for, at denne kortlægning bruges til kortlægning af Læge til DoctorDto .Dette kan gøres med indstillingen “ bruger” i kortlægningskonfigurationen:

@Mapper(
componentModel = "spring",
unmappedTargetPolicy = ReportingPolicy.ERROR,
uses = {AddressMapper.class})
public interface DoctorMapper {

@Mapping(source = "phone", target = "contact")
@Mapping(source = "speciality.name", target = "specialityName")
DoctorDto toDto(Doctor doctor);
}

Vi vil se, at vores DoctorMapperImpl vil autowire og bruge vores AddressMapper :

@Component
public class DoctorMapperImpl implements DoctorMapper {

@Autowired
private AddressMapper addressMapper;

@Override
public DoctorDto toDto(Doctor doctor) {
if ( doctor == null ) {
return null;
}

DoctorDto doctorDto = new DoctorDto();

doctorDto.setSpecialityName( doctorSpecialityName(doctor));
doctorDto.setContact( doctor.getPhone() );
doctorDto.setId( doctor.getId() );
doctorDto.setName( doctor.getName() );
doctorDto.setAddress(
addressMapper.toDto( doctor.getAddress() ) );


return doctorDto;
}

...
}

Kortlægning med en brugerdefineret metode

Lad os nu tilføje en liste over patienter til vores Læge klasse:

@Data
public class Doctor {
private int id;
private String name;
private String phone;
private Speciality speciality;
private Address address;
private List patients;
}

Imidlertid på vores DoctorDto vi ønsker kun antallet af patienter:

@Data
public class DoctorDto {
private int id;
private String name;
private String contact;
private String specialityName;
private AddressDto address;
private int numPatients;
}

Denne kortlægning kræver 2 ting:

  • En brugerdefineret metode med @ Navngivet annotering
  • kvalificeretBynavn konfigurationen til Mapping-kommentaren
@Mapper(
componentModel = "spring",
unmappedTargetPolicy = ReportingPolicy.ERROR,
uses = {AddressMapper.class})
public interface DoctorMapper {

@Mapping(source = "phone", target = "contact")
@Mapping(source = "speciality.name", target = "specialityName")
@Mapping(
source = "patients",
target = "numPatients",
qualifiedByName = "countPatients")

DoctorDto toDto(Doctor doctor);

@Named("countPatients")
default int getNumPatients(List patients) {
if(patients == null) {
return 0;
}
return patients.size();
}
}

@BeforeMapping @AfterMapping

Det forrige eksempel (kortlægning fra Liste over patienter til int numPatienter ) kan også gøres med @BeforeMapping og @AfterMapping

@Mapper(
componentModel = "spring",
unmappedTargetPolicy = ReportingPolicy.ERROR,
uses = {AddressMapper.class})
public interface DoctorMapper {

@BeforeMapping
default void setList(Doctor doctor) {
if (doctor != null && doctor.getPatients() == null) {
doctor.setPatients(new ArrayList());
}
}

@Mapping(source = "phone", target = "contact")
@Mapping(source = "speciality.name", target = "specialityName")
@Mapping(target = "numPatients", ignore = true)
DoctorDto toDto(Doctor doctor);

@AfterMapping
default void setNumPatients(Doctor doctor,
@MappingTarget DoctorDto doctorDto) {
doctorDto.setNumPatients(doctor.getPatients().size());
}
}

Disse metoder kaldes i begyndelsen og slutningen af ​​vores genererede kortlægningsmetode:

@Override
public DoctorDto toDto(Doctor doctor) {
setList( doctor );

if ( doctor == null ) {
return null;
}

DoctorDto doctorDto = new DoctorDto();

doctorDto.setSpecialityName( doctorSpecialityName( doctor ) );
doctorDto.setContact( doctor.getPhone() );
doctorDto.setId( doctor.getId() );
doctorDto.setName( doctor.getName() );
doctorDto.setAddress( addressMapper.toDto(doctor.getAddress()));

setNumPatients( doctor, doctorDto );

return doctorDto;
}

Kortlægning med en ekstra parameter

Lad os kontrollere, hvordan du håndterer en situation, hvor din kortlægger har brug for at modtage en ekstra parameter udover din enhed.

I dette tilfælde kan din læge-klasse kun få by-id:

@Data
public class Doctor {
private int id;
private String name;
private String phone;
private Speciality speciality;
private Address address;
private List patients;
private int cityId;
}

Men skal kortlægge det til bynavnet:

@Data
public class DoctorDto {
private int id;
private String name;
private String contact;
private String specialityName;
private AddressDto address;
private int numPatients;
private String cityName;
}

Din tjeneste henter listen over byer og sender dem til kortlæggeren

@Service
public class DoctorService {

private final DoctorMapper doctorMapper;
private final DoctorRepository doctorRepository;

@Autowired
public DoctorService(DoctorMapper doctorMapper) {
this.doctorMapper = doctorMapper;
this.doctorRepository = doctorRepository;
}

public DoctorDto getDoctor(Integer id) {
Doctor doctor = doctorRepository.findById(id);
List cities = getCities();
return doctorMapper.toDto(doctor, cities);
}
}

I vores kortlægger skal vi:

  • markere den yderligere parameter (liste over byer) med @Context kommentar
  • oprette en brugerdefineret metode til at håndtere vores kortlægning
  • anmode om kontekstparameteren (liste over byer) på vores brugerdefinerede ma pping-metode
@Mapping(source = "phone", target = "contact")
@Mapping(source = "speciality.name", target = "specialityName")
@Mapping(target = "numPatients", ignore = true)
@Mapping(source = "cityId",
target = "cityName",
qualifiedByName = "cityName")

DoctorDto toDto(Doctor doctor, @Context List cities);

@Named("cityName")
default String getCityName(int cityId, @Context List cities) {
return cities.stream()
.filter(city -> city.getId() == cityId)
.findAny()
.map(City::getName)
.orElse(null);
}

Afhængighedsinjektion på kortlægningsmetoder

Du vil sandsynligvis befinde dig i situationer, hvor dine tilpassede kortlægningsmetoder kræver en anden bønne (en anden kortlægger, et arkiv, en tjeneste osv.).

I disse situationer bliver du nødt til at autowire den bønne til din kortlægger, så lad os se et eksempel på, hvordan du gør det.

I dette eksempel vil vores Patient klasse være en abstrakt klasse:

@Data
public abstract class Patient {
private int id;
private String name;
private int age;
}

Som indeholder to implementeringer:

public class Man extends Patient {
}public class Woman extends Patient {
}

Som vi tidligere har set, er dette tilstanden for vores Læge enhed:

@Data
public class Doctor {
private int id;
private String name;
private String phone;
private Speciality speciality;
private Address address;
private List patients;
private int cityId;
}

Vores PatientDto kræver en særskilt liste for hver konkret klasse:

@Data
public class DoctorDto {
private int id;
private String name;
private String contact;
private String specialityName;
private AddressDto address;
private int numPatients;
private String cityName;
private PatientsDto patients;
}

Og

@Data
public class PatientsDto {
private List men = new ArrayList();
private List women = new ArrayList();

public void addMan(ManDto manDto) {
men.add(manDto);
}

public void addWoman(WomanDto womanDto) {
women.add(womanDto);
}
}

Så for at kortlægge disse konkrete klasser, skal vi starte med creatin g to kortlæggere:

@Mapper(componentModel = "spring")
public interface WomanMapper {
WomanDto toDto(Woman woman);
}@Mapper(componentModel = "spring")
public interface ManMapper {
ManDto toDto(Man man);
}

Men at kortlægge fra Liste over patienter til Patienter til patienter har vi også brug for en anden kortlægger, der bruger de nyoprettede kortlæggere ( WomanMapper og ManMapper ), som ender med at blive brugt af vores DoctorMapper.

Da vores PatientsMapper vil har brug for WomanMapper og ManMapper i stedet for at oprette en grænseflade skal vi oprette en abstrakt klasse:

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

@Autowired
private ManMapper manMapper;

@Autowired
private WomanMapper womanMapper;

public PatientsDto toDto(List patients) {
PatientsDto patientsDto = new PatientsDto();
for (Patient patient : patients) {
if (patient instanceof Man) {
patientsDto.addMan(
manMapper.toDto((Man) patient));
} else if (patient instanceof Woman) {
patientsDto.addWoman(
womanMapper.toDto((Woman) patient));
}
}
return patientsDto;
}
}

Endelig skal vi tilføje nogle konfigurationer for at få vores DoctorMapper til at bruge PatientsMapper :

@Mapper(
componentModel = "spring",
unmappedTargetPolicy = ReportingPolicy.ERROR,
uses = {AddressMapper.class, PatientsMapper.class})
public interface DoctorMapper {
...
}

Da variablen patienter har samme navn på klasserne Enhed og Dto, behøver vi ikke angive noget andet.

Dette vil være slutresultatet af de genererede klasser:

@Component
public class DoctorMapperImpl implements DoctorMapper {

@Autowired
private AddressMapper addressMapper;
@Autowired
private PatientsMapper patientsMapper;


@Override
public DoctorDto toDto(Doctor doctor, List cities) {
if (doctor == null) {
return null;
}

DoctorDto doctorDto = new DoctorDto();

doctorDto.setNumPatients(
getNumPatients(doctor.getPatients()));
doctorDto.setCityName(
getCityName(doctor.getCityId(), cities));
doctorDto.setSpecialityName(doctorSpecialityName(doctor));
doctorDto.setContact(doctor.getPhone());
doctorDto.setId(doctor.getId());
doctorDto.setName(doctor.getName());
doctorDto.setAddress(
addressMapper.toDto(doctor.getAddress()));
doctorDto.setPatients(
patientsMapper.toDto(doctor.getPatients()));


return doctorDto;
}
...
}@Component
public class ManMapperImpl implements ManMapper {

@Override
public ManDto toDto(Man man) {
if ( man == null ) {
return null;
}

ManDto manDto = new ManDto();

manDto.setId( man.getId() );
manDto.setName( man.getName() );
manDto.setAge( man.getAge() );

return manDto;
}
}@Component
public class WomanMapperImpl implements WomanMapper {

@Override
public WomanDto toDto(Woman woman) {
if ( woman == null ) {
return null;
}

WomanDto womanDto = new WomanDto();

womanDto.setId( woman.getId() );
womanDto.setName( woman.getName() );
womanDto.setAge( woman.getAge() );

return womanDto;
}
}

Opdateringer

Mapstruct giver også er en nem måde at håndtere opdateringer på. Hvis vi vil opdatere vores Læge enhed med oplysningerne om DoctorDto , vi skal bare oprette:

@Mapping(source = "contact", target = "phone")
@Mapping(source = "specialityName", target = "speciality.name")
void updateEntity(DoctorDto doctorDto,
@MappingTarget Doctor doctor);

Som vi kan se på den genererede implementering, vil den kortlægge alle variabler (null eller ikke null):

@Override
public void updateEntity(DoctorDto doctorDto, Doctor doctor) {
if ( doctorDto == null ) {
return;
}

if ( doctor.getSpeciality() == null ) {
doctor.setSpeciality( new Speciality() );
}
doctorDtoToSpeciality( doctorDto, doctor.getSpeciality() );
doctor.setPhone( doctorDto.getContact() );
doctor.setId( doctorDto.getId() );
doctor.setName( doctorDto.getName() );
if ( doctorDto.getAddress() != null ) {
if ( doctor.getAddress() == null ) {
doctor.setAddress( new Address() );
}
addressDtoToAddress( doctorDto.getAddress(), doctor.getAddress() );
}
else {
doctor.setAddress( null );
}
}

Patch-opdateringer

Som vi har set i det foregående eksempel, vil standardopdateringsmetoden kortlæg hver ejendom, selvom den er nul.Så hvis du støder på dig selv i en situation, hvor du bare vil udføre en patchopdatering (kun opdater de ikke-nulværdier), skal du bruge nullValuePropertyMappingStrategy :

@BeanMapping(nullValuePropertyMappingStrategy = 
NullValuePropertyMappingStrategy.IGNORE)

@Mapping(source = "contact", target = "phone")
@Mapping(source = "specialityName", target = "speciality.name")
void updatePatchEntity(DoctorDto doctorDto,
@MappingTarget Doctor doctor);

Den genererede metode udfører nulkontrol inden opdatering af værdierne:

@Override
public void updatePatchEntity(DoctorDto doctorDto, Doctor doctor) {
if ( doctorDto == null ) {
return;
}

if ( doctorDto.getContact() != null ) {
doctor.setPhone( doctorDto.getContact() );
}
doctor.setId( doctorDto.getId() );
if ( doctorDto.getName() != null ) {
doctor.setName( doctorDto.getName() );
}
if ( doctorDto.getAddress() != null ) {
if ( doctor.getAddress() == null ) {
doctor.setAddress( new Address() );
}
addressDtoToAddress(
doctorDto.getAddress(),
doctor.getAddress() );
}
if ( doctor.getSpeciality() == null ) {
doctor.setSpeciality( new Speciality() );
}
doctorDtoToSpeciality1( doctorDto, doctor.getSpeciality() );
}

Konklusion

Denne artikel beskrev, hvordan man drager fordel af Mapstruct-biblioteket for at reducere vores kedelpladekode betydeligt på en sikker og elegant måde.

Som det ses i eksemplerne, tilbyder Mapstruct et stort sæt funktioner og konfigurationer, som giver os mulighed for at skabe fra grundlæggende til komplekse kortlæggere på en nem og hurtig måde.