Mapstruct (https://mapstruct.org/) ist eine meiner liebsten Bibliotheken im Alltag. Erleichtert diese Bibliothek das Mapping zwischen Java Objekten (z.B. JPA Entities und DTOs) enorm. Ein Interface deklarieren. Methoden-Signatur schreiben. (oft schon) Fertig.
Sollten wir im Projekt ValueObjects einsetzen, also ein (Wrapper-) Objekte, die unveränderliche Werte darstellen, bedarf es nun immer händisches Eingreifen, um die konkreten Werte dieser ValueObjects in eine flache DTO Struktur zu bringen. Zumindest für die einfachen Fälle, mit nur einem Attribut, bietet es sich an einen generischen Mapper zu registrieren.
Vorab schauen wir uns die Ausgangslage an. Wir definieren ein Interface für unsere einfachen ValueObject-Typen und verwenden diesen, hier am Beispiel einer CustomerId:
public interface ValueObject<T> {
T value();
}
public record CustomerId(UUID value) implements ValueObject<UUID> {
}
public record Customer(CustomerId customerId, String name) {
}
public record CustomerDto(UUID customerId, String name) {
}
Ziel ist es ja nun, ohne viel Aufwand, unser Mapping zu realisieren. Im Idealfall sieht das nun so aus:
@Mapper(componentModel = MappingConstants.ComponentModel.JAKARTA_CDI, uses = ValueObjectMapper.class)
public abstract class DemoMapper {
public abstract Customer mapToCustomer(CustomerDto dto);
public abstract CustomerDto mapToDto(Customer customer);
}
In der ersten Zeile sehen wir bereits die Lösung. Wir inkludieren einen weiteren Mapper, der sich generischen um die Umwandlung unserer (zumindest einfachen) ValueObjects kümmert. Dieser könnten jetzt so aussehen.
@Mapper(componentModel = MappingConstants.ComponentModel.JAKARTA_CDI)
public abstract class ValueObjectMapper {
public <T> T mapToValue(ValueObject<T> valueObject) {
return Optional.ofNullable(valueObject)
.map(ValueObject::value)
.orElse(null);
}
public <V extends ValueObject<T>, T> V mapToObject(T value, @TargetType Class<V> valueObjectClass) {
if (value == null) {
return null;
}
try {
Constructor<V> constructor = valueObjectClass.getDeclaredConstructor(value.getClass());
constructor.setAccessible(true);
return constructor.newInstance(value);
} catch (NoSuchMethodException e) {
throw new IllegalArgumentException("Kein passender Konstruktor gefunden für " + valueObjectClass.getName(), e);
} catch (ReflectiveOperationException e) {
throw new RuntimeException("Fehler beim Erzeugen der Instanz von " + valueObjectClass.getName(), e);
}
}
}
Damit lassen sich wieder unsere Modelle mappen, ohne dass wir übermäßig viel Mapping-Deklarationen hinzufügen müssen.







