Kurzanleitung zu MapStruct

Kurzanleitung zu MapStruct

1. Überblick

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 MapStructlö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.mapstruct
    mapstruct-jdk8
    1.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:


    org.apache.maven.plugins
    maven-compiler-plugin
    3.5.1
    
        1.8
        1.8
        
            
                org.mapstruct
                mapstruct-processor
                1.3.0.Beta2
            
        
    

4. Grundlegende Zuordnung

4.1. POJO erstellen

Erstellen wir zunächst ein einfaches Java-POJO:

public class SimpleSource {
    private String name;
    private String description;
    // getters and setters
}

public class SimpleDestination {
    private String name;
    private String description;
    // getters and setters
}

4.2. Die Mapper-Schnittstelle

@Mapper
public interface SimpleSourceDestinationMapper {
    SimpleDestination sourceToDestination(SimpleSource source);
    SimpleSource destinationToSource(SimpleDestination destination);
}

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:

@Mapper
public interface EmployeeMapper {
    @Mappings({
      @Mapping(target="employeeId", source="entity.id"),
      @Mapping(target="employeeName", source="entity.name")
    })
    EmployeeDTO employeeToEmployeeDTO(Employee entity);
    @Mappings({
      @Mapping(target="id", source="dto.employeeId"),
      @Mapping(target="name", source="dto.employeeName")
    })
    Employee employeeDTOtoEmployee(EmployeeDTO dto);
}

6.3. Der Testfall

Wieder müssen wir testen, ob Quell- und Zielobjektwerte übereinstimmen:

@Test
public void givenEmployeeDTOwithDiffNametoEmployee_whenMaps_thenCorrect() {
    EmployeeDTO dto = new EmployeeDTO();
    dto.setEmployeeId(1);
    dto.setEmployeeName("John");

    Employee entity = mapper.employeeDTOtoEmployee(dto);

    assertEquals(dto.getEmployeeId(), entity.getId());
    assertEquals(dto.getEmployeeName(), entity.getName());
}

Weitere Testfälle finden Sie inGithub project.

7. Bohnen mit Kinderbohnen abbilden

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.

Fügen wir dies dem Mapper hinzu:

DivisionDTO divisionToDivisionDTO(Division entity);

Division divisionDTOtoDivision(DivisionDTO dto);

7.3. Ändern Sie den Testfall

Lassen Sie uns ein paar Testfälle ändern und zu den vorhandenen hinzufügen:

@Test
public void givenEmpDTONestedMappingToEmp_whenMaps_thenCorrect() {
    EmployeeDTO dto = new EmployeeDTO();
    dto.setDivision(new DivisionDTO(1, "Division1"));
    Employee entity = mapper.employeeDTOtoEmployee(dto);
    assertEquals(dto.getDivision().getId(),
      entity.getDivision().getId());
    assertEquals(dto.getDivision().getName(),
      entity.getDivision().getName());
}

8. Zuordnung mit Typkonvertierung

MapStruct bietet auch einige vorgefertigte implizite Typkonvertierungen. In unserem Beispiel werden wir versuchen, ein String-Datum in ein tatsächlichesDate-Objekt zu konvertieren.

Weitere Informationen zur impliziten Typkonvertierung finden Sie inMapStruct reference guide.

8.1. Ändern Sie die Bohnen

Fügen Sie einen Starttermin für unseren Mitarbeiter hinzu:

public class Employee {
    // other fields
    private Date startDt;
    // getters and setters
}
public class EmployeeDTO {
    // other fields
    private String employeeStartDt;
    // getters and setters
}

8.2. Ändern Sie den Mapper

Ändern Sie den Mapper und geben Sie diedateFormat für unser Startdatum an:

@Mappings({
  @Mapping(target="employeeId", source = "entity.id"),
  @Mapping(target="employeeName", source = "entity.name"),
  @Mapping(target="employeeStartDt", source = "entity.startDt",
           dateFormat = "dd-MM-yyyy HH:mm:ss")})
EmployeeDTO employeeToEmployeeDTO(Employee entity);
@Mappings({
  @Mapping(target="id", source="dto.employeeId"),
  @Mapping(target="name", source="dto.employeeName"),
  @Mapping(target="startDt", source="dto.employeeStartDt",
           dateFormat="dd-MM-yyyy HH:mm:ss")})
Employee employeeDTOtoEmployee(EmployeeDTO dto);

8.3. Ändern Sie den Testfall

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:


    org.apache.maven.plugins
    maven-compiler-plugin
    3.5.1
    
        1.8
        1.8
        
            
                org.mapstruct
                mapstruct-processor
                1.3.0.Beta2
            
            
                org.projectlombok
                lombok
            1.18.4
            
        
    

Definieren wir die Quellentität mithilfe von Lombok-Anmerkungen:

@Getter
@Setter
public class Car {
    private int id;
    private String name;
}

Und das Zieldatenübertragungsobjekt:

@Getter
@Setter
public class CarDTO {
    private int id;
    private String name;
}

Die Mapper-Oberfläche hierfür ähnelt unserem vorherigen Beispiel:

@Mapper
public interface CarMapper {
    CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
    CarDTO carToCarDTO(Car car);
}

12. Unterstützung fürdefaultExpression

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:

@Mapper
public interface PersonMapper {
    PersonMapper INSTANCE = Mappers.getMapper(PersonMapper.class);

    @Mapping(target = "id", source = "person.id",
      defaultExpression = "java(java.util.UUID.randomUUID().toString())")
    PersonDTO personToPersonDTO(Person person);
}

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.