Hin und Her mit MapStruct

Ein neues privates Projekt nimmt langsam Formen an und die ersten Features sind auch schon skizziert. In der Fachdomäne sind einige der Objekte und Rollen identifiziert, u.a. Person, Mitarbeiter, Leitung, Vertretung, Einrichtung, Firma und Geschäftsführung. Notwendig ist eine Persistenzschicht und das Backend soll vom Frontend durch eine REST Schnittstelle getrennt sein.

Da sich das Entwickler-Team gut mit Spring Boot auskennt, ist die Wahl der Backend Technologie auch schon erfolgt und die Arbeit kann beginnen. Damit die einzelnen Schichten ordentlich voneinander getrennt sind, sollen die Entity Objekte für die Persistenz nicht innerhalb der Controller Methoden verwendet werden.

@Entity 
@NoArgsConstructor, @AllArgsConstructor 
@Getter @Setter(lombok.accessors.chain=true)
public class Einrichtung {
    @Id @GeneratedValue(strategy=GenerationType.AUTO)
  private Long id;
  private String name;
  private String description;
  private String street;
  private String number;
  private String zip;
  private String city;
}

Die kurze Entity ist hier kein Fehler, sondern dem absichtsvollem Nutzen des Projekt Lombok geschuldet. Projekt Lombok nimmt dem Entwickler Arbeit ab, indem es zur Compile-Time die fehlenden Getter, Setter und Konstruktoren erzeugt. Was Lombok im Detail zu erzeugen hat, wird über spezielle Annotationen gesteuert. In diesem Fall @Setter und @Getter und die @NoArgsConstructor und @AllArgsConstructor Konstruktoren.

Häufig mache ich einen Bogen um Projekt Lombok , weil mir persönlich Sourcecode missfällt, der in good old Java einfach keinen Sinn macht. Weitere Möglichkeiten von Projekt Lombok, zur Generierung der toString, equals und hashCode Methoden, lasse ich hier bewusst weg. Es sollen keine großen Dinge mit diesen Entitäten geschehen. Rein in die Persistenz, raus aus der Persistenz, mehr soll nicht sein.

Für den REST Controller benötigen wir Data Transfer Objects (DTO), in die Spring Boot die Daten aus den Request einfügt und sie für die Responses wieder ausliest.

@Setter @Getter
@NoArgsConstructor, @AllArgsConstructor 
public class EinrichtungDto {
    @Null
    private Long id;
    @NotNull @Size(min=1, max=100)
    private String name;
    @NotNull @Size(min=1, max=4000)
    private String description;
    @NotNull @Size(min=1, max=100)
    private String street;
    @NotNull @Size(min=1, max=10)
    private String number;
    @NotNull @Size(min=5, max=5)
    private String zip;
    @NotNull @Size(min=1, max=100)
    private String city;  
}

Diese Java Klasse enthält die gleichen Attribute wie ihr Gegenstück für die Persistenz. Auch die Projekt Lombok Annotation sind vorhanden, in diesem Fall sind aber die Attribute mit Annotationen aus dem Bean Validation Framework versehen. Damit können wir sicherstellen, dass die Controller korrekt gefüllte DTO an die Services übergeben.

Wie aber kommen jetzt die Daten aus dem DTO in das Entity und wieder zurück? In diversen Projekten wird das Problem über Junior-Entwickler gelöst. Sie dürfen die Konvertierungsmethoden schreiben und bei jeder Änderung im Klassenmodel, diese Methoden und die dazugehörigen Unit-Tests, ändern.

public static EinrichtungDto toDto(Einrichtung entity) {
  EinrichtungDto dto= new EinrichtungDto();
  dto.setName(entity.getName());
  dto.setDescription(entity.getDescription());
  dto.setStreet(entity.getStreet());
  dto.setNumber(entity.getNumber());
  dto.setZip(entity.getZip());
  dto.setCity(entity.getCity());
}

Das sieht nach öder, stumpfsinniger und fehleranfälliger Arbeit aus, die ein Computer sicher besser und schneller erledigen könnte. Und mit dieser Erkenntnis betritt nur endlich MapStruct die Bühne.

Dieses Framework generiert den Sourcecode für die Konvertierungsmethoden basierend auf einer InterfaceDefinition. In diesem Fall sieht das Mapper Interface etwa folgendermaßen aus.

@Mapper(componentModel = "spring")
public interface EinrichtungMapper {
    EinrichtungDto map(Einrichtung entity);
    Einrichtung map(EinrichtungDto dto);
}

Die @Mapper Annotation signalisiert dem MapStruct Präprozessor, dass dieses Interface Konvertierungsmethoden definiert. In diesem Fall eine Methode um aus einer Entity eine DTO zu erzeugen und eine Methode für den umgekehrten Weg von DTO zu Entity. Mit dem zusätzlichen Parameter an der @Mapper Annotation wird außerdem MapStruct darauf hingewiesen unsere Mapper für das Dependency Injection von Spring bereit zu machen.

Damit das alles funktionieren kann, müssen wir MapStruct noch zur Arbeit bewegen. Daher fügen wir folgende Konfiguration in unsere pom.xml ein.

<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>1.3.0.FINAL</version>
          </path>
           <path>
             <groupId>org.projectlombok</groupId>
             <artifactId>lombok</artifactId>
         <version>1.18.(</version>
      </path>
    </annotationProcessorPaths>
  </configuration>
</plugin>

Einsetzen können wir den neuen Mapper z.B. in unseren EinrichtungService, der u.a. die Daten zu Einrichtungen aus der Persistenz liest.

@Service
class EinrichtungService {
  private final EinrichtungMapper mapper;
  private final EinrichtungRepository repository;
  
  EinrichtungDto getEinrichtung(long id) {
    Einrichtung entity = repository.findById(id).orElseThrow(new EnrichtungNotFoundException(id);
    return mapper.map(entity):
  }
  ... 
}

Für existierende Projekte oder besondere Anforderungen bietet MapStruct eine Fülle weiterer interessanter Feature. Es können Attribute mit unterschiedlichen Namen gemappt werden, die Werte können in andere Typen konvertiert werden, nicht zu mappende Attribute können ignoriert werden, fehlende Werte durch Defaultwerte ersetzt, Mapper können verschachtelt, dekoriert und bei Bedarf auch selbst geschriebene Mappings verwendet werden.

Wer glückliche Junior-Entwickler sehen möchte, der setzt MapStruct ein.