In diesem Artikel untersuchen wir die Verwendung vonMapStruct, einem Java Bean-Mapper.
Diese API enthält Funktionen, die automatisch zwei Java-Beans zuordnen. Mit MapStruct müssen wir nur die Oberfläche erstellen, und die Bibliothek erstellt während der Kompilierungszeit automatisch eine konkrete Implementierung.
2. MapStruct und Transfer Object Pattern
Bei den meisten Anwendungen werden Sie eine Menge Boilerplate-Code bemerken, der POJOs in andere POJOs konvertiert.
Eine übliche Art der Konvertierung findet beispielsweise zwischen persistenzgestützten Entitäten und DTOs statt, die an die Clientseite gesendet werden.
Das ist das Problem, dass MapStruct–löst. Das manuelle Erstellen von Bean-Mappern ist zeitaufwändig. Die Bibliothekcan generate bean mapper classes automatically.
3. Maven
Fügen wir die folgende Abhängigkeit zu unseren Mavenpom.xmlhinzu:
org.mapstructmapstruct-jdk81.3.0.Beta2
Die neueste stabile Version vonMapstruct und seineprocessor sind beide im Maven Central Repository verfügbar.
Fügen wir auch den AbschnittannotationProcessorPaths zum Konfigurationsteil des Pluginsmaven-compiler-pluginhinzu.
Dasmapstruct-processor wird verwendet, um die Mapper-Implementierung während des Builds zu generieren:
public class SimpleSource {
private String name;
private String description;
// getters and setters
}
public class SimpleDestination {
private String name;
private String description;
// getters and setters
}
Beachten Sie, dass wir für unsereSimpleSourceDestinationMapper keine Implementierungsklasse erstellt haben, da MapStruct diese für uns erstellt.
4.3. Der neue Mapper
Wir können die MapStruct-Verarbeitung auslösen, indem wirmvn clean install ausführen.
Dadurch wird die Implementierungsklasse unter/target/generated-sources/annotations/ generiert.
Hier ist die Klasse, die MapStruct automatisch für uns erstellt:
public class SimpleSourceDestinationMapperImpl
implements SimpleSourceDestinationMapper {
@Override
public SimpleDestination sourceToDestination(SimpleSource source) {
if ( source == null ) {
return null;
}
SimpleDestination simpleDestination = new SimpleDestination();
simpleDestination.setName( source.getName() );
simpleDestination.setDescription( source.getDescription() );
return simpleDestination;
}
@Override
public SimpleSource destinationToSource(SimpleDestination destination){
if ( destination == null ) {
return null;
}
SimpleSource simpleSource = new SimpleSource();
simpleSource.setName( destination.getName() );
simpleSource.setDescription( destination.getDescription() );
return simpleSource;
}
}
4.4. Ein Testfall
Nachdem alles generiert wurde, schreiben wir einen Testfall, der zeigt, dass die Werte inSimpleSourcemit den Werten inSimpleDestinationübereinstimmen.
public class SimpleSourceDestinationMapperIntegrationTest {
private SimpleSourceDestinationMapper mapper
= Mappers.getMapper(SimpleSourceDestinationMapper.class);
@Test
public void givenSourceToDestination_whenMaps_thenCorrect() {
SimpleSource simpleSource = new SimpleSource();
simpleSource.setName("SourceName");
simpleSource.setDescription("SourceDescription");
SimpleDestination destination = mapper.sourceToDestination(simpleSource);
assertEquals(simpleSource.getName(), destination.getName());
assertEquals(simpleSource.getDescription(),
destination.getDescription());
}
@Test
public void givenDestinationToSource_whenMaps_thenCorrect() {
SimpleDestination destination = new SimpleDestination();
destination.setName("DestinationName");
destination.setDescription("DestinationDescription");
SimpleSource source = mapper.destinationToSource(destination);
assertEquals(destination.getName(), source.getName());
assertEquals(destination.getDescription(),
source.getDescription());
}
}
5. Mapping mit Abhängigkeitsinjektion
Als Nächstes erhalten wir eine Instanz eines Mappers in MapStruct, indem wir lediglichMappers.getMapper(YourClass.class). aufrufen
Natürlich ist dies eine sehr manuelle Methode, um die Instanz zu erhalten. Eine viel bessere Alternative wäre, den Mapper direkt dort einzuspielen, wo wir ihn benötigen (wenn unser Projekt eine Dependency-Injection-Lösung verwendet).
Luckily MapStruct has solid support for both Spring and CDI (Contexts and Dependency Injection).
Um Spring IoC in unserem Mapper zu verwenden, müssen wir dascomponentModel-Sattribut zu@Mapper mit dem Wertspring hinzufügen, und für CDI wärecdi.
5.1. Ändern Sie den Mapper
Fügen SieSimpleSourceDestinationMapper den folgenden Code hinzu:
@Mapper(componentModel = "spring")
public interface SimpleSourceDestinationMapper
6. Zuordnen von Feldern mit unterschiedlichen Feldnamen
In unserem vorherigen Beispiel konnte MapStruct unsere Beans automatisch zuordnen, da sie dieselben Feldnamen haben. Was ist, wenn eine Bohne, die wir abbilden wollen, einen anderen Feldnamen hat?
In unserem Beispiel erstellen wir eine neue Bean mit den NamenEmployee undEmployeeDTO.
6.1. Neue POJOs
public class EmployeeDTO {
private int employeeId;
private String employeeName;
// getters and setters
}
public class Employee {
private int id;
private String name;
// getters and setters
}
6.2. Die Mapper-Schnittstelle
Wenn wir verschiedene Feldnamen zuordnen, müssen wir das Quellfeld dem Zielfeld konfigurieren und dazu die Annotation@Mappingshinzufügen. Diese Annotation akzeptiert ein Array von@Mapping Annotation, mit der wir das Ziel- und Quellattribut hinzufügen.
In MapStruct können wir auch die Punktnotation verwenden, um ein Mitglied einer Bean zu definieren:
Als Nächstes zeigen wir, wie Sie eine Bean mit Verweisen auf andere Beans zuordnen.
7.1. Ändern Sie das POJO
Fügen wir demEmployee-Objekt eine neue Bean-Referenz hinzu:
public class EmployeeDTO {
private int employeeId;
private String employeeName;
private DivisionDTO division;
// getters and setters omitted
}
public class Employee {
private int id;
private String name;
private Division division;
// getters and setters omitted
}
public class Division {
private int id;
private String name;
// default constructor, getters and setters omitted
}
7.2. Ändern Sie den Mapper
Hier müssen wir eine Methode hinzufügen, umDivision inDivisionDTO umzuwandeln und umgekehrt; Wenn MapStruct feststellt, dass der Objekttyp konvertiert werden muss und die zu konvertierende Methode in derselben Klasse vorhanden ist, wird sie automatisch verwendet.
MapStruct bietet auch einige vorgefertigte implizite Typkonvertierungen. In unserem Beispiel werden wir versuchen, ein String-Datum in ein tatsächlichesDate-Objekt zu konvertieren.
Fügen wir ein paar weitere Testfälle hinzu, um sicherzustellen, dass die Konvertierung korrekt ist:
private static final String DATE_FORMAT = "dd-MM-yyyy HH:mm:ss";
@Test
public void givenEmpStartDtMappingToEmpDTO_whenMaps_thenCorrect() throws ParseException {
Employee entity = new Employee();
entity.setStartDt(new Date());
EmployeeDTO dto = mapper.employeeToEmployeeDTO(entity);
SimpleDateFormat format = new SimpleDateFormat(DATE_FORMAT);
assertEquals(format.parse(dto.getEmployeeStartDt()).toString(),
entity.getStartDt().toString());
}
@Test
public void givenEmpDTOStartDtMappingToEmp_whenMaps_thenCorrect() throws ParseException {
EmployeeDTO dto = new EmployeeDTO();
dto.setEmployeeStartDt("01-04-2016 01:00:00");
Employee entity = mapper.employeeDTOtoEmployee(dto);
SimpleDateFormat format = new SimpleDateFormat(DATE_FORMAT);
assertEquals(format.parse(dto.getEmployeeStartDt()).toString(),
entity.getStartDt().toString());
}
9. Mapping mit einer abstrakten Klasse
Manchmal möchten wir unseren Mapper möglicherweise so anpassen, dass er über die @Mapping-Funktionen hinausgeht.
Beispielsweise möchten wir zusätzlich zur Typkonvertierung möglicherweise die Werte auf eine Weise transformieren, wie in unserem folgenden Beispiel.
In diesem Fall können wir eine abstrakte Klasse erstellen und Methoden implementieren, die angepasst werden sollen, und die von MapStruct generierten abstrakten Methoden belassen.
9.1. Grundmodell
In diesem Beispiel verwenden wir die folgende Klasse:
public class Transaction {
private Long id;
private String uuid = UUID.randomUUID().toString();
private BigDecimal total;
//standard getters
}
und ein passendes DTO:
public class TransactionDTO {
private String uuid;
private Long totalInCents;
// standard getters and setters
}
Der schwierige Teil hier ist die Umrechnung desBigDecimaltotal-Dollarbetrags inLong totalInCents.
9.2. Mapper definieren
Wir können dies erreichen, indem wir unsereMapper as als abstrakte Klasse erstellen:
@Mapper
abstract class TransactionMapper {
public TransactionDTO toTransactionDTO(Transaction transaction) {
TransactionDTO transactionDTO = new TransactionDTO();
transactionDTO.setUuid(transaction.getUuid());
transactionDTO.setTotalInCents(transaction.getTotal()
.multiply(new BigDecimal("100")).longValue());
return transactionDTO;
}
public abstract List toTransactionDTO(
Collection transactions);
}
Hier haben wir unsere vollständig angepasste Zuordnungsmethode für eine einzelne Objektkonvertierung implementiert.
Auf der anderen Seite haben wir die Methode verlassen, mit derCollectionauf einenList-Sabstrakt abgebildet werden soll, sodassMapStruct ie für uns implementieren wird.
9.3. Generiertes Ergebnis
Da wir die Methode bereits implementiert haben, um einzelneTransactionaufTransactionDTO abzubilden, erwarten wir, dassMapstructie in der zweiten Methode verwendet. Folgendes wird generiert:
@Generated
class TransactionMapperImpl extends TransactionMapper {
@Override
public List toTransactionDTO(Collection transactions) {
if ( transactions == null ) {
return null;
}
List list = new ArrayList<>();
for ( Transaction transaction : transactions ) {
list.add( toTransactionDTO( transaction ) );
}
return list;
}
}
Wie wir in Zeile 12 sehen können, setztMapStruct unsere Implementierung in der von ihr generierten Methode außer Kraft.
10. Anmerkungen vor und nach der Zuordnung
Hier ist eine weitere Möglichkeit, die Funktionen von@Mappingmithilfe der Anmerkungen von@BeforeMapping und@AfterMappinganzupassen. The annotations are used to mark methods that are invoked right before and after the mapping logic.
Sie sind sehr nützlich in Szenarien, in denen wir diesebehavior to be applied to all mapped super-types möchten.
Schauen wir uns ein Beispiel an, das die UntertypenCar;ElectricCar, undBioDieselCarCarDTO zuordnet.
Während der Zuordnung möchten wir den Begriff der Typen dem AufzählungsfeldFuelType im DTO zuordnen. Nach Abschluss der Zuordnung möchten wir den Namen des DTO in Großbuchstaben ändern.
10.1. Grundmodell
In diesem Beispiel werden die folgenden Klassen verwendet:
public class Car {
private int id;
private String name;
}
Untertypen vonCar:
public class BioDieselCar extends Car {
}
public class ElectricCar extends Car {
}
DieCarDTO mit einem AufzählungsfeldtypFuelType:
public class CarDTO {
private int id;
private String name;
private FuelType fuelType;
}
public enum FuelType {
ELECTRIC, BIO_DIESEL
}
10.2. Mapper definieren
Lassen Sie uns nun fortfahren und unsere abstrakte Mapper-Klasse schreiben, dieCarCarDTO zuordnet:
@Mapper
public abstract class CarsMapper {
@BeforeMapping
protected void enrichDTOWithFuelType(Car car, @MappingTarget CarDTO carDto) {
if (car instanceof ElectricCar) {
carDto.setFuelType(FuelType.ELECTRIC);
}
if (car instanceof BioDieselCar) {
carDto.setFuelType(FuelType.BIO_DIESEL);
}
}
@AfterMapping
protected void convertNameToUpperCase(@MappingTarget CarDTO carDto) {
carDto.setName(carDto.getName().toUpperCase());
}
public abstract CarDTO toCarDto(Car car);
}
@MappingTarget ist eine Parameteranmerkung, die mitpopulates the target mapping DTO right before the mapping logic is executedin case of @BeforeMapping undright after in case of @AfterMapping mit Anmerkungen versehen ist.
10.3. Ergebnis
DieCarsMapper defined above generatestheimplementation:
@Generated
public class CarsMapperImpl extends CarsMapper {
@Override
public CarDTO toCarDto(Car car) {
if (car == null) {
return null;
}
CarDTO carDTO = new CarDTO();
enrichDTOWithFuelType(car, carDTO);
carDTO.setId(car.getId());
carDTO.setName(car.getName());
convertNameToUpperCase(carDTO);
return carDTO;
}
}
Beachten Sie, wiethe annotated methods invocations surround the mapping logicin der Implementierung enthalten ist.
11. Unterstützung für Lombok
In der aktuellen Version von MapStruct wurde die Unterstützung von Lombok angekündigt. So we can easily map a source entity and a destination using Lombok.
Um die Lombok-Unterstützung zu aktivieren, müssen wirthe dependency im Pfad des Anmerkungsprozessors hinzufügen. Nun haben wir den Mapstruct-Prozessor und Lombok im Maven-Compiler-Plugin:
Ab Version 1.3.0,we can use the defaultExpression attribute of the @Mapping annotation to specify an expression that determines the value of the destination field if the source field is null. Dies ist zusätzlich zu der vorhandenen Attributfunktionalität vondefaultValue.
Die Quellentität:
public class Person {
private int id;
private String name;
}
Das Zieldatenübertragungsobjekt:
public class PersonDTO {
private int id;
private String name;
}
Wenn das Feldid der Quellentitätnull, ist, möchten wir ein zufälligesid generieren und es dem Ziel zuweisen, wobei andere Eigenschaftswerte unverändert bleiben:
Fügen wir einen Testfall hinzu, um die Ausführung des Ausdrucks zu überprüfen:
@Test
public void givenPersonEntitytoPersonWithExpression_whenMaps_thenCorrect()
Person entity = new Person();
entity.setName("Micheal");
PersonDTO personDto = PersonMapper.INSTANCE.personToPersonDTO(entity);
assertNull(entity.getId());
assertNotNull(personDto.getId());
assertEquals(personDto.getName(), entity.getName());
}
13. Fazit
Dieser Artikel enthielt eine Einführung in MapStruct. Wir haben die meisten Grundlagen der Mapping-Bibliothek und deren Verwendung in unseren Anwendungen vorgestellt.
Die Implementierung dieser Beispiele und Tests finden Sie im ProjektGithub. Dies ist ein Maven-Projekt, daher sollte es einfach zu importieren und auszuführen sein.