Mapping av java-objekter med MapStruct

Christian Aarthun
Systek
Published in
5 min readMar 5, 2018

--

https://farm4.staticflickr.com/3794/12416217355_7b3f04b31b_z_d.jpg

For javautviklere, og spesielt for oss som jobber med integrasjoner, er det visse deler av kodingen som både er kjedelig, tidkrevende, og som ofte resulterer i feil. Mapping av objekter (typisk mellom DTOer og interne domeneobjekter) er et slikt eksempel. Slik trenger det derimot ikke å være.

MapStruct is a code generator that greatly simplifies the implementation of mappings between Java bean types based on a convention over configuration approach.

Når du skal velge et rammeverk for en programmeringsoppgave er det flere kriterier du må ta hensyn til:

  • Trenger du i det hele tatt et rammeverk?
  • Vil rammeverket gjøre oppgaven lettere — både mtp. selve utviklingen og senere vedlikehold? Herunder inngår kodens lesbarhet og testbarhet.
  • Vil rammeverket gjøre at programvarens ytelse er like bra, eller marginalt dårligere, som uten dette rammeverket?

I økosystemet til Java finnes det et mylder av rammeverk, og det er (for) lett å inkludere nok en avhengighet. De følgende kodeeksemplene illustrerer derimot at MapStruct er et rammeverk som rettferdiggjør en plass på classpath, sett fra et rent kodeperspektiv, dersom formålet er å mappe javaobjekter. Det er lett å google oversikter for mapping-rammeverk hvis du ønsker å se på flere alternativer [1] [2].

MapStruct er relativt nytt, og den siste stable release er fra oktober 2017.

Oppsett i maven

I et maven-prosjekt må du ha følgende konfigurering. MapStruct kan selvfølgelig også benyttes i andre byggesystemer [3]. Merk at du må ha versjon 3.5 av maven-compiler-plugin eller høyere. I seksjonen for compilerArgs har du muligheten til å endre standard oppførsel på en rekke deler av MapStruct [4].

<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-jdk8</artifactId>
<version>1.2.0.Final</version>
</dependency>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.7.0</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.20</version>
</path>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.2.0.Final</version>
</path>
</annotationProcessorPaths>
<compilerArgs>
<compilerArg>
-Amapstruct.defaultComponentModel=spring
</compilerArg>
</compilerArgs>
</configuration>
</plugin>

Kodeeksempler

I de følgende kodeeksemplene jobber vi med et internt domeneobjekt (Car) som vi ønsker å mappe til et annet objekt (CarDto) før vi eksempelvis skal sende det i en request. For enkelhets skyld bruker vi Lombok for å slippe å kode gettere, settere og buildere samt Junit for enhetstester. Eksemplene er en videreutvikling av dokumentasjonen som MapStruct tilbyr.

@Data
@Builder
public class Car {

private CarBrand brand;
private String model;
private int numberOfSeats;
private LocalDateTime handoverDateTime;
private boolean hybrid;

@Getter
@AllArgsConstructor
public enum CarBrand {
LEXUS("Lexus"),
BMW("BMW"),
FORD("Ford");

private String brandName;
}
}
@Data
public class CarDto {

private String brand;
private String model;
private int seatCount;
private String deliveredToOwnerDate;
private String hybrid;

}

Selve mappe-logikken ligger i et interface annotert med “@Mapper”. Eksemplene omhandler mapping fra- og til et felt, defaultverdier, type-konversjon, javauttrykk og dato/tid-format. Siden MapStruct genererer kode under kompileringen kan man både inspisere og debugge koden. Ulempen, spesielt i en tidlig utviklingsfase er at man må kompilere koden hver gang man endrer i mapper-interfacet.

@Mapper
public interface CarMapper {

CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);

@Mappings({
@Mapping(target = "brand", source = "brand.brandName"),
@Mapping(target = "seatCount", source = "numberOfSeats"), @Mapping(target = "model", source = "model", defaultValue = "LC500h"), @Mapping(target = "hybrid",
expression = "java(com.example.demo.CarUtil.isHybrid(car.isHybrid()))"),
@Mapping(target = "deliveredToOwnerDate", source = "handoverDateTime", dateFormat = "yyyy-MM-dd")
})
CarDto carToCarDto(Car car);

}

Både Car og CarDto har feltet model. Dersom vi ikke ønsker ytterligere håndtering av felter med samme navn, kan feltene utelates fra mapperen.

@Test
public void testThatSourceIsMappedToTargetWhenNamesAreEqual() {
Car car = Car.builder().model("RC300h").build();

CarDto carDto = CarMapper.INSTANCE.carToCarDto(car);

assertEquals("RC300h", carDto.getModel());
}

I mapperen har vi definert at feltet model skal ha en default-verdi dersom det ikke finnes.

@Test
public void testThatDefaultIsUsedWhenNoProperty() {
Car car = Car.builder().build();

CarDto carDto = CarMapper.INSTANCE.carToCarDto(car);

assertEquals("LC500h", carDto.getModel());
}

Antall seter er definert i feltene Car.numberofSeats og CarDto.seatCount.

@Test
public void testThatSourceIsMappedToTargetWhenDifferentPropertyName() {
Car car = Car.builder().numberOfSeats(4).build();

CarDto carDto = CarMapper.INSTANCE.carToCarDto(car);

assertEquals(4, carDto.getSeatCount());
}

Feltet Car.brand er definert som en enum i CarBrand. I mapperen har vi beskrevet at feltet Car.brand.brandName skal mappes til strengen CarDto.brand. Dvs. at “dot-notasjon” også fungerer.

@Test
public void testThatMapperUsesTypeConversion() {
Car car = Car.builder().brand(Car.CarBrand.LEXUS).build();

CarDto carDto = CarMapper.INSTANCE.carToCarDto(car);

assertEquals("Lexus", carDto.getBrand());
}

I mapperen har vi definert at feltet CarDto.hybrid skal populeres med resultatet fra et java-uttrykk — eksempelvis fra en metoden CarUtil.isHybrid.

public class CarUtil {
static String isHybrid(boolean hybrid) {
return hybrid ? "Yes" : "No";
}
}
@Test
public void testThatJavaExpressionIsUsed() {
Car car = Car.builder().hybrid(true).build();

CarDto carDto = CarMapper.INSTANCE.carToCarDto(car);

assertEquals("Yes", carDto.getHybrid());
}

Dato- og tidsformat kan også enkelt konverteres, hvis det for eksempel holder å ekstrahere dato fra en timestamp.

@Test
public void testThatIntendedDateFormatIsUsed() {
Car car = Car.builder()
.handoverDateTime(LocalDateTime.of(2017, 9, 10, 0, 0, 0)).build();

CarDto carDto = CarMapper.INSTANCE.carToCarDto(car);

assertEquals("2017-09-10", carDto.getDeliveredToOwnerDate());
}

For IntelliJ og Eclipse finnes det en MapStruct-plugin som gjør det raskere å bevege seg mellom mapperen og de ulike klassene. Som tidligere beskrevet genereres altså koden for mapperen ved kompilering — noe som gjør det mye mer håndterbart å debugge oppførselen.

@Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2018-03-04T19:22:38+0100",
comments = "version: 1.2.0.Final, compiler: javac, environment: Java 1.8.0_121 (Oracle Corporation)"
)
public class CarMapperImpl implements CarMapper {

@Override
public CarDto carToCarDto(Car car) {
if ( car == null ) {
return null;
}

CarDto carDto = new CarDto();

if ( car.getModel() != null ) {
carDto.setModel( car.getModel() );
}
else {
carDto.setModel( "LC500h" );
}
if ( car.getHandoverDateTime() != null ) {
carDto.setDeliveredToOwnerDate( DateTimeFormatter.ofPattern( "yyyy-MM-dd" ).format( car.getHandoverDateTime() ) );
}
String brandName = carBrandBrandName( car );
if ( brandName != null ) {
carDto.setBrand( brandName );
}
carDto.setSeatCount( car.getNumberOfSeats() );

carDto.setHybrid( com.example.demo.CarUtil.isHybrid(car.isHybrid()) );

return carDto;
}

private String carBrandBrandName(Car car) {
if ( car == null ) {
return null;
}
CarBrand brand = car.getBrand();
if ( brand == null ) {
return null;
}
String brandName = brand.getBrandName();
if ( brandName == null ) {
return null;
}
return brandName;
}
}

Etter å ha jobbet noen måneder i et prosjekt hvor vi bruker MapStruct som rammeverk for mapping har jeg ingen store innvendinger. Det er raskt å konfigurere byggemiljøet og de vanligste utfordringene med mapping av objekter løses elegant. Det resulterer også i en renere kodebase, hvor utviklerne kan fokusere på å løse oppgaver og skrive enhetstester istedenfor å finne opp kruttet på nytt.

--

--