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 PatientDto
s 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å enum
s:
@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
ochInteger
,float
ochFloat
,long
ochLong
,boolean
ochBoolean
och så vidare - Konvertering mellan alla primitiva typer och alla typer av omslag. Till exempel mellan
int
ochlong
,byte
ochInteger
och så vidare - Konvertering mellan alla primitiva och omslagstyper och
String
. Till exempel konvertering mellanboolean
ochString
,Integer
ochString
,float
ochString
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 Patient
s:
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 source
vä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 String
s, 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 source
s och target
s ä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/