Zephyrnet-logotyp

Guide till MapStruct i Java - Advanced Mapping Library

Datum:


Beskrivning

Eftersom mikroservicer och distribuerade applikationer snabbt tar över utvecklingsvärlden - är dataintegritet och säkerhet viktigare än någonsin. En säker kommunikationskanal och begränsad dataöverföring mellan dessa löst kopplade system är av största vikt. För det mesta behöver slutanvändaren eller tjänsten inte komma åt hela datan från en modell utan bara vissa specifika delar.

Dataöverföringsobjekt (DTO) tillämpas regelbundet i dessa applikationer. DTO: er är bara objekt som innehåller den begärda informationen om ett annat objekt. Vanligtvis är informationen begränsad i omfattning. Eftersom DTO: er är en återspegling av de ursprungliga föremålen - mappare mellan dessa klasser spelar en nyckelroll i konverteringsprocessen.

I den här artikeln kommer vi att dyka in MapStruct - en omfattande mapper för Java Beans.

Innehåll:

MapStruct

MapStruct är en Java-baserad kodgenerator med öppen källkod som skapar kod för kartläggning av implementationer.

Den använder anteckningsbearbetning för att generera implementeringar av mapper klass under sammanställningen och minskar avsevärt mängden pannplåtkod som regelbundet skulle skrivas för hand.

MapStruct Beroenden

Om du använder Maven, installera MapStruct genom att lägga till beroendet:

<dependencies> <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct</artifactId> <version>${org.mapstruct.version}</version> </dependency>
</dependencies>

Detta beroende kommer att importera de viktigaste MapStruct-anteckningarna. Eftersom MapStruct fungerar på kompileringstid och är kopplat till byggare som Maven och Gradle, måste vi också lägga till ett plugin till <build>:

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

Om du använder Gradle, installera MapStruct är så enkelt som:

plugins { id 'net.ltgt.apt' version '0.20'
} apply plugin: 'net.ltgt.apt-idea'
apply plugin: 'net.ltgt.apt-eclipse' dependencies { compile "org.mapstruct:mapstruct:${mapstructVersion}" annotationProcessor "org.mapstruct:mapstruct-processor:${mapstructVersion}"
}

Smakämnen net.ltgt.apt plugin ansvarar för annoteringsbehandlingen. Du kan tillämpa apt-idea och apt-eclipse plugins beroende på din IDE.

Du kan kolla in den senaste versionen på Maven Central.

Grundläggande kartläggningar

Låt oss börja med några grundläggande kartläggningar. Vi har en Doctor modell och en DoctorDto. Deras fält kommer att ha samma namn för vår bekvämlighet:

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

Och:

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

För att göra en kartläggning mellan dessa två skapar vi en DoctorMapper gränssnitt. Genom att kommentera det med @Mapper, MapStruct vet att detta är en kartläggning mellan våra två klasser:

@Mapper
public interface DoctorMapper { DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class); DoctorDto toDto(Doctor doctor);
}

Vi har en INSTANCE of DoctorMapper typ. Detta kommer att vara vår "startpunkt" till instansen när vi skapar implementeringen.

Vi har definierat en toDto() metod i gränssnittet, som accepterar a Doctor instans och returnerar a DoctorDto exempel. Detta räcker för att MapStruct ska veta att vi vill kartlägga en Doctor instans till en DoctorDto exempel.

När vi bygger / sammanställer applikationen, kommer MapStruct-annotationsprocessorns plugin att hämta upp DoctorMapper gränssnitt och generera en implementering för det:

public class DoctorMapperImpl implements DoctorMapper { @Override public DoctorDto toDto(Doctor doctor) { if ( doctor == null ) { return null; } DoctorDtoBuilder doctorDto = DoctorDto.builder(); doctorDto.id(doctor.getId()); doctorDto.name(doctor.getName()); return doctorDto.build(); }
}

Smakämnen DoctorMapperImpl klass innehåller nu en toDto() metod som kartlägger vår Doctor fält till DoctorDto fält.

För att kartlägga en Doctor instans till en DoctorDto exempel skulle vi göra:

DoctorDto doctorDto = DoctorMapper.INSTANCE.toDto(doctor);

Notera: Du kanske har märkt en DoctorDtoBuilder i genomförandet ovan. Vi har utelämnat implementeringen för korthet eftersom byggare tenderar att vara långa. MapStruct kommer att försöka använda din byggare om den finns i klassen. Om inte, kommer det bara att instansera det via new nyckelord.

Om du vill läsa mer om Builder Design Pattern i Java, vi har täckt dig!

Kartläggning av olika käll- och målfält

Ofta har en modell och en DTO inte samma fältnamn. Det kan vara små variationer på grund av att gruppmedlemmar tilldelar sina egna versioner, och hur du vill packa informationen för tjänsten som krävde DTO.

MapStruct ger support för att hantera dessa situationer via @Mapping anteckning.

Olika egendomsnamn

Låt oss uppdatera Doctor klass för att inkludera en specialty:

public class Doctor { private int id; private String name; private String specialty;
}

Och för DoctorDto, låt oss lägga till en specialization fält:

public class DoctorDto { private int id; private String name; private String specialization;
}

Nu måste vi låta vårt DoctorMapper känner till denna avvikelse. Vi gör det genom att ställa in source och target flaggor av @Mapping kommentar med båda dessa varianter:

@Mapper
public interface DoctorMapper { DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class); @Mapping(source = "doctor.specialty", target = "specialization") DoctorDto toDto(Doctor doctor);
}

Smakämnen specialty fältet av Doctor klass motsvarar specialization fältet av DoctorDto klass.

Efter sammanställning av koden har annotationsprocessorn genererat denna implementering:

public class DoctorMapperImpl implements DoctorMapper {
@Override public DoctorDto toDto(Doctor doctor) { if (doctor == null) { return null; } DoctorDtoBuilder doctorDto = DoctorDto.builder(); doctorDto.specialization(doctor.getSpecialty()); doctorDto.id(doctor.getId()); doctorDto.name(doctor.getName()); return doctorDto.build(); }
}

Flera källklasser

Ibland räcker det inte med en enda klass för att bygga en DTO. Ibland vill vi samla värden från flera klasser till en enda DTO för slutanvändaren. Detta görs också genom att ange lämpliga flaggor i @Mapping anteckning:

Låt oss skapa en annan modell Education:

public class Education { private String degreeName; private String institute; private Integer yearOfPassing;
}

Och lägg till ett nytt fält i DoctorDto:

public class DoctorDto { private int id; private String name; private String degree; private String specialization;
}

Nu ska vi uppdatera DoctorMapper gränssnitt:

@Mapper
public interface DoctorMapper { DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class); @Mapping(source = "doctor.specialty", target = "specialization") @Mapping(source = "education.degreeName", target = "degree") DoctorDto toDto(Doctor doctor, Education education);
}

Vi har lagt till en annan @Mapping kommentar där vi har angett källan som degreeName av Education klass och target som degree fältet av DoctorDto klass.

Om Education och Doctor klasser innehåller fält med samma namn - vi måste låta kartläggaren veta vilken som ska användas eller så kommer det att undvika. Om båda modellerna innehåller en id, vi måste välja vilket id kommer att kartläggas till DTO-egenskapen.

Kartlägga barnenheter

I de flesta fall innehåller POJO inte bara primitiva datatyper. I de flesta fall innehåller de andra klasser. Till exempel a Doctor kommer att ha 1..n patienter:

public class Patient { private int id; private String name;
}

Och låt oss göra en List av dem för Doctor:

public class Doctor { private int id; private String name; private String specialty; private List<Patient> patientList;
}

Eftersom Patient data kommer att överföras, vi skapar också en DTO för det:

public class PatientDto { private int id; private String name;
}

Och slutligen, låt oss uppdatera DoctorDto med en List av den nyligen skapade PatientDto:

public class DoctorDto { private int id; private String name; private String degree; private String specialization; private List<PatientDto> patientDtoList;
}

Innan vi ändrar något i DoctorMapper, vi måste göra en mapper som konverterar mellan Patient och PatientDto klasser:

@Mapper
public interface PatientMapper { PatientMapper INSTANCE = Mappers.getMapper(PatientMapper.class); PatientDto toDto(Patient patient);
}

Det är en grundläggande kartläggare som bara kartlägger ett par primitiva datatyper.

Låt oss uppdatera vår nu DoctorMapper att inkludera läkarnas patienter:

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

Eftersom vi arbetar med en annan klass som kräver kartläggning har vi ställt in uses flaggan @Mapper anteckning. Detta @Mapper använder en annan @Mapper. Du kan lägga så många klasser / kartläggare här som du vill - vi har bara en.

Eftersom vi har lagt till denna flagga när vi skapade mapper-implementeringen för DoctorMapper gränssnittet kommer MapStruct också att konvertera Patient modell till en PatientDto - eftersom vi har registrerat PatientMapper för denna uppgift.

Nu kommer kompilering av applikationen att resultera i en ny implementering:

public class DoctorMapperImpl implements DoctorMapper { private final PatientMapper patientMapper = Mappers.getMapper( PatientMapper.class ); @Override public 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; }
}

Uppenbarligen en ny mapper - patientListToPatientDtoList() har lagts till, förutom toDto() mapper. Detta görs utan uttrycklig definition, helt enkelt för att vi har lagt till PatientMapper till DoctorMapper.

Metoden iterates över en lista med Patient modeller, konverterar dem till PatientDtos och lägger till dem i en lista som finns i en DoctorDto objekt.

Uppdatering av befintliga instanser

Ibland vill vi uppdatera en modell med de senaste värdena från en DTO. Använda @MappingTarget kommentar på målobjektet (Doctor i vårt fall) kan vi uppdatera befintliga instanser.

Låt oss lägga till en ny @Mapping till vår DoctorMapper som accepterar Doctor och DoctorDto instanser. De DoctorDto instansen kommer att vara datakällan, medan Doctor kommer att vara målet:

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

Nu, efter att ha skapat implementeringen igen, har vi fått updateModel() metod:

public class DoctorMapperImpl implements DoctorMapper { @Override 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.setId(doctorDto.getId()); doctor.setName(doctorDto.getName()); }
}

Det som är värt att notera är att patientlistan också uppdateras, eftersom det är en underordnad enhet i modulen.

Beroende på injektion

Hittills har vi fått åtkomst till de genererade kartläggarna via getMapper() metod:

DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class);

Men om du använder Vår, kan du uppdatera din mapper-konfiguration och injicera den som ett vanligt beroende.

Låt oss uppdatera vår DoctorMapper att arbeta med våren:

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

Lägga (componentModel = "spring") i @Mapper annotation berättar MapStruct att när vi genererar mapperimplementeringsklassen, skulle vi vilja att den ska skapas med stöd för injektionsinjektion via Spring. Nu behöver du inte lägga till INSTANCE fältet till gränssnittet.

Den genererade DoctorMapperImpl kommer nu att ha @Component anteckning:

@Component
public class DoctorMapperImpl implements DoctorMapper {}

En gång markerad som en @Component, Våren kan plocka upp den som en böna och det är du fri till @Autowire det i en annan klass som en controller:

@Controller
public class DoctorController() { @Autowired private DoctorMapper doctorMapper;
}

Om du inte använder Spring har MapStruct stöd för Java CDI också:

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

Kartlägga enums

Mapping Enums fungerar på samma sätt som kartläggningsfält gör. MapStruct kommer att kartlägga de med samma namn utan problem. För enums med olika namn kommer vi dock att använda @ValueMapping anteckning. Återigen, det här liknar @Mapping kommentar med vanliga typer.

Låt oss skapa två enums, den första är PaymentType:

public enum PaymentType { CASH, CHEQUE, CARD_VISA, CARD_MASTER, CARD_CREDIT
}

Detta är, till exempel, tillgängliga betalningsalternativ i en applikation. Och nu, låt oss ha en mer allmän, begränsad bild av dessa alternativ:

public enum PaymentTypeView { CASH, CHEQUE, CARD
}

Låt oss nu göra ett mapper-gränssnitt mellan dessa två enums:

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

Här har vi en general CARD värde och mer specifikt CARD_VISA, CARD_MASTER och CARD_CREDIT värden. Det finns ett missförhållande med antalet värden - PaymentType har 6 värden, medan PaymentTypeView har bara 3.

För att brygga mellan dessa kan vi använda @ValueMappings annotation, som accepterar multipel @ValueMapping annoteringar. Här kan vi ställa in källan till att vara något av de tre specifika fallen och målet som CARD värde.

MapStruct hanterar dessa ärenden:

public class PaymentTypeMapperImpl implements PaymentTypeMapper { @Override public 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 och CHEQUE har sina motsvarande värden som standard, medan det specifika CARD värdet hanteras genom a switch slinga.

Men det här tillvägagångssättet kan bli opraktiskt när du har många värden som du vill tilldela en mer generell. Istället för att tilldela var och en manuellt kan vi helt enkelt låta MapStruct gå igenom alla tillgängliga kvarvarande värden och kartlägga dem alla till en annan.

Detta görs via MappingConstants:

@ValueMapping(source = MappingConstants.ANY_REMAINING, target = "CARD")
PaymentTypeView paymentTypeToPaymentTypeView(PaymentType paymentType);

Här, efter att standardmappningarna är gjorda, kommer alla återstående (inte matchande) värden att mappas till 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;
}

Ett annat alternativ skulle vara att använda ANY_UNMAPPED:

@ValueMapping(source = MappingConstants.ANY_UNMAPPED, target = "CARD")
PaymentTypeView paymentTypeToPaymentTypeView(PaymentType paymentType);

I det här fallet, istället för att kartlägga standardvärden först, följt med att kartlägga de återstående till ett enda mål - MapStruct kommer bara att kartlägga alla obegränsade värden till målet.

Kartlägga datatyper

MapStruct stöder konvertering av datatyp mellan source och target egenskaper. Det ger också automatisk typkonvertering mellan primitiva typer och motsvarande omslag.

Konvertering av automatisk typ gäller för:

  • Konvertering mellan primitiva typer och deras respektive omslagstyper. Till exempel konvertering mellan int och Integer, float och Float, long och Long, boolean och Boolean och så vidare
  • Konvertering mellan alla primitiva typer och alla typer av omslag. Till exempel mellan int och long, byte och Integer och så vidare
  • Konvertering mellan alla primitiva och omslagstyper och String. Till exempel konvertering mellan boolean och String, Integer och String, float och String och så vidare

Så under mapperkodgenerering om typkonvertering mellan källa och målfält faller under något av ovanstående scenarier, kommer MapStrcut att hantera själva typkonvertering.

Låt uppdatera vår PatientDto att inkludera ett fält för lagring av dateofBirth:

public class PatientDto { private int id; private String name; private LocalDate dateOfBirth;
}

Å andra sidan, säg vår Patient objektet har en dateOfBirth av typ String:

public class Patient { private int id; private String name; private String dateOfBirth;
}

Nu ska vi göra en kartläggning mellan dessa två:

@Mapper
public interface PatientMapper { @Mapping(source = "dateOfBirth", target = "dateOfBirth", dateFormat = "dd/MMM/yyyy") Patient toModel(PatientDto patientDto);
}

När vi konverterar mellan datum kan vi också använda dateFormat flagga för att ställa in formatspecifikationen. Den genererade implementeringen kommer att se ut som:

public class PatientMapperImpl implements PatientMapper { @Override public 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(); }
}

Observera att MapStruct har använt mönstret från dateFormat flagga. Om vi ​​inte specificerade formatet, skulle det ha ställts in till standardformatet för a LocalDate:

if (patientDto.getDateOfBirth() != null) { patient.dateOfBirth(DateTimeFormatter.ISO_LOCAL_DATE .format(patientDto.getDateOfBirth()));
}

Lägga till anpassade metoder

Hittills har vi lagt till en platshållningsmetod som vi vill att MapStruct ska implementera för oss. Vad vi också kan göra är att lägga till en anpassad default metod till gränssnittet också. Genom att lägga till en default kan vi lägga till implementeringen direkt också. Vi kan komma åt det via instansen utan problem.

För detta, låt oss göra en DoctorPatientSummary, som innehåller en sammanfattning mellan a Doctor och en lista över deras Patients:

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

Nu, i vår DoctorMapper, lägger vi till en default metod som istället för att kartlägga a Doctor till en DoctorDto, konverterar Doctor och Education föremål i a 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(); }
}

Detta objekt är byggt från Doctor och Education objekt med hjälp av Builder Design-mönstret.

Den här implementeringen kommer att användas efter att mapperimplementeringsklassen genereras av MapStruct. Du kan komma åt det precis som du vill använda någon annan mapper-metod:

DoctorPatientSummary summary = doctorMapper.toDoctorPatientSummary(dotor, education);

Skapa anpassade kartläggare

Hittills har vi använt gränssnitt för att skapa ritningar för kartläggare. Vi kan också göra ritningar med abstract klasser, kommenterade med @Mapper anteckning. MapStruct skapar en implementering för den här klassen, liknande att skapa en gränssnittsimplementering.

Låt oss skriva om det föregående exemplet, men den här gången gör vi det till abstract klass:

@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(); }
}

Du kan använda denna implementering på samma sätt som du skulle använda en gränssnittsimplementering. Använder sig av abstract klasser ger oss mer kontroll och alternativ när vi skapar anpassade implementationer på grund av mindre begränsningar. En annan fördel är förmågan att lägga till @BeforeMapping och @AfterMapping metoder.

@BeforeMapping och @AfterMapping

För ytterligare kontroll och anpassning kan vi definiera @BeforeMapping och @AfterMapping metoder. Uppenbarligen körs dessa före och efter varje kartläggning. Det vill säga att dessa metoder kommer att läggas till och exekveras före och efter den faktiska kartläggningen mellan två objekt inom implementeringen.

Låt oss lägga till dessa metoder till vår DoctorCustomMapper:

@Mapper(uses = {PatientMapper.class}, 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()); } @Mapping(source = "doctor.patientList", target = "patientDtoList") @Mapping(source = "doctor.specialty", target = "specialization") public abstract DoctorDto toDoctorDto(Doctor doctor);
}

Låt oss nu skapa en mapper baserad på den här klassen:

@Component
public class DoctorCustomMapperImpl extends DoctorCustomMapper { @Autowired private PatientMapper patientMapper; @Override public 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; }
}

Smakämnen validate() metoden körs före DoctorDto objektet instanseras, och updateResult() metoden körs efter att kartläggningen är klar.

Lägga till standardvärden

Ett användbart par flaggor du kan använda med @Mapping kommentar är konstanter och standardvärden. EN constant värdet kommer alltid att användas, oavsett sourcevärde. EN default värdet kommer att användas om source värdet är null.

Låt oss uppdatera vår DoctorMapper med en constant och default:

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

Om specialiteten inte är tillgänglig tilldelar vi Information Not Available i stället. Vi har också kodat hårddisken id att vara -1.

Låt oss generera kartläggaren:

@Component
public class DoctorMapperImpl implements DoctorMapper { @Autowired private PatientMapper patientMapper; @Override public 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; }
}

If doctor.getSpecialty() återgår null, ställer vi in ​​specialiseringen till vårt standardmeddelande. De id är inställd oavsett eftersom det är en constant.

Lägga till Java Expressions

MapStruct går så långt som att du kan mata in Java-uttryck som flaggor till @Mapping anteckning. Du kan antingen ställa in en defaultExpression (om source värdet är null) eller en expression vilket är konstant.

Låt oss lägga till en externalId vilket kommer att vara en String och en appointment som kommer att vara av LocalDateTime skriv till vår Doctor och DoctorDto.

Vår Doctor modellen kommer att se ut:

public class Doctor { private int id; private String name; private String externalId; private String specialty; private LocalDateTime availability; private List<Patient> patientList;
}

Och DoctorDto kommer att se ut som:

public class DoctorDto { private int id; private String name; private String externalId; private String specialization; private LocalDateTime availability; private List<PatientDto> patientDtoList;
}

Och nu, låt oss uppdatera vår 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);
}

Här har vi tilldelat värdet för java(UUID.randomUUID().toString()) till externalId, medan vi villkorligt ställer in tillgängligheten till en ny LocalDateTime, om availability är inte närvarande.

Eftersom uttrycka är bara Strings, måste vi ange klasser som används i uttryck. Det här är inte en kod som utvärderas, det är ett bokstavligt textvärde. Således har vi lagt till imports = {LocalDateTime.class, UUID.class} till @Mapper anteckning.

Det genererade kortet ser ut som:

@Component
public class DoctorMapperImpl implements DoctorMapper { @Autowired private PatientMapper patientMapper; @Override public DoctorDto toDtoWithExpression(Doctor doctor) { if (doctor == null) { return null; } DoctorDto doctorDto = new DoctorDto(); doctorDto.setSpecialization(doctor.getSpecialty()); if (doctor.getAvailability() != null) { doctorDto.setAvailability(doctor.getAvailability()); } else { doctorDto.setAvailability(LocalDateTime.now()); } doctorDto.setPatientDtoList(patientListToPatientDtoList(doctor .getPatientList())); doctorDto.setId(doctor.getId()); doctorDto.setName(doctor.getName()); doctorDto.setExternalId(UUID.randomUUID().toString()); return doctorDto; }
}

Smakämnen externalId är satt till:

doctorDto.setExternalId(UUID.randomUUID().toString());

Medan om availability is null, det är inställt på:

doctorDto.setAvailability(LocalDateTime.now());

Undantagshantering under kartläggning

Undantagshantering är oundvikligt. Ansökningar har exceptionella tillstånd hela tiden. MapStruct ger support för att inkludera undantagshantering ganska sömlöst, vilket gör ditt jobb som dev mycket enklare.

Låt oss överväga ett scenario där vi vill validera vårt Doctor modell medan du mappar den till DoctorDto. Låt oss göra en separat Validator klass för detta:

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

Nu vill vi uppdatera vår DoctorMapper att använda Validator klass, utan att vi behöver specificera genomförandet. Som vanligt lägger vi till klasserna i listan över klasser som används av @Mapper, och allt vi behöver göra är att berätta för MapStruct att vårt toDto() metod 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;
}

Låt oss nu skapa en implementering för denna mapper:

@Component
public class DoctorMapperImpl implements DoctorMapper { @Autowired private PatientMapper patientMapper; @Autowired private Validator validator; @Override public 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 har automatiskt ställt in ID för doctorDto med resultatet av Validator exempel. Den har också lagt till en throws klausul för metoden.

Kartlägga konfigurationer

MapStruct ger en mycket användbar konfiguration för att skriva kartläggningsmetoder. Merparten av tiden replikeras de mappningskonfigurationer som vi anger för en mapper-metod när du lägger till en annan mapper-metod för liknande typer.

Istället för att konfigurera dessa manuellt kan vi konfigurera liknande typer för att ha samma / liknande kartläggningsmetoder.

Arv konfiguration

Låt oss se över scenariot i Uppdatering av befintliga instanser, där vi skapade en mapper för att uppdatera värdena på en befintlig Doctor modell från en DoctorDto objekt:

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

Säg att vi har en annan mapper som genererar en Doctor från en DoctorDto:

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

Båda dessa kartläggningsmetoder använder samma konfiguration. De sources och targets är samma. Istället för att upprepa konfigurationerna för båda kartläggningsmetoderna, kan vi använda @InheritConfiguration anteckning.

Genom att kommentera en metod med @InheritConfiguration annotation, MapStruct letar efter en annan, redan konfigurerad metod vars konfiguration också kan tillämpas på den här. Vanligtvis används denna kommentar för uppdateringsmetoder efter en kartläggningsmetod, precis som om vi använder den:

@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); @InheritConfiguration void updateModel(DoctorDto doctorDto, @MappingTarget Doctor doctor);
}

Arv inverse konfiguration

Ett annat liknande scenario är att skriva mapperfunktioner för att kartlägga Modell till DTO och DTO till Modell, liksom i koden nedan var vi måste ange samma källmålskartläggning för båda funktionerna:

Dina konfigurationer kommer inte alltid att vara det samma. Till exempel kan de vara omvända. Kartlägga en modell till en DTO och en DTO till en modell - du använder samma fält, men omvänd. Så här ser det ut typiskt:

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

Istället för att skriva detta två gånger kan vi använda @InheritInverseConfiguration kommentar på den andra metoden:

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

Den genererade koden från båda mapperimplementeringarna kommer att vara densamma.

Slutsats

I den här artikeln utforskade vi MapStruct - ett bibliotek för att skapa mapper klasser, från grundläggande nivå mappningar till anpassade metoder och anpassade kartläggare. Vi tittade också på olika alternativ som tillhandahålls av MapStruct inklusive beroendeinjicering, kartläggning av datatyp, mappningar av enum och användning av uttryck.

MapStruct tillhandahåller ett kraftfullt integrationsplugin för att minska mängden kod som en användare måste skriva och gör processen för att skapa kartläggare enkel och snabb.

Källkoden för provkoden kan hittas här..

Källa: https://stackabuse.com/guide-to-mapstruct-in-java-advanced-mapping-library/

plats_img

Senaste intelligens

plats_img