Lettvint mapping i Java med MapStruct
Ofte må vi oversette data fra en API til en annen (har folk hørt om standardisering??). Det er kjedelig og uelegant å kalle mange gettere og settere, så derfor kan vi heller bruke MapStruct.
Banalt eksempel
La oss si at vi må forholde oss til to eksterne klasser som representerer biler:
public class Vehicle {
private String make;
private int numberOfSeats;
private String type;
// konstruktør, gettere, settere etc.
}
public class Automobile {
private String make;
private int seatCount;
private String type;
// konstruktør, gettere, settere etc.
}
Man kan enkelt mappe fra den ene til den andre slik:
@Mapper // dette gjør at MapStruct sin annotation processor genererer en implementasjon
public interface VehicleMapper {
VehicleMapper INSTANCE = Mappers.getMapper( VehicleMapper.class ); // gjør en instans tilgjengelig
@Mapping(source = "numberOfSeats", target = "seatCount") // dette er det eneste feltet som har forskjellig navn...
Automobile vehicleToAutomobile(Vehicle vehicle); // ...i de to klassene
@InheritInverseConfiguration
Vehicle automobileToVehicle(Automobile automobile);
}
Vi får en fiks ferdig klasse med en metode som konverterer fra Vehicle til Automobile, og en metode som konverterer motsatt vei. Jo likere klassene er, jo mindre arbeid blir det siden felt som heter det samme blir mappet automatisk.
Litt mindre banalt eksempel
Hvis klassene gjør seg vrange med enums…
public class Vehicle {
public enum Type {SEDAN, HATCHBACK, VAN, TRUCK, TRACTOR, ATV}
private String make;
private int numberOfSeats;
private Type type;
// konstruktør, gettere, settere etc.
}
public class Automobile {
public enum Type {CAR, VAN, LORRY, OTHER}
private String make;
private int seatCount;
private Type type;
// konstruktør, gettere, settere etc.
}
…da må vi håndtere det:
@Mapper
public interface VehicleMapper {
VehicleMapper INSTANCE = Mappers.getMapper(VehicleMapper.class);
@Mapping(source = "numberOfSeats", target = "seatCount")
Automobile vehicleToAutomobile(Vehicle vehicle);
@ValueMapping(source = "SEDAN", target = "CAR")
@ValueMapping(source = "HATCHBACK", target = "CAR")
@ValueMapping(source = "TRUCK", target = "LORRY")
@ValueMapping(source = MappingConstants.ANY_REMAINING, target = "OTHER")
// "VAN" er det samme begge steder, så vi trenger ikke gjøre noe selv
Automobile.type mapType(Vehicle.Type type);
}
(Disse klassene kan bare mappes én vei siden den ene har mer spesifikke biltyper enn den andre)
Den genererte implementasjonen vil bruke metoden mapType
til å oversette biltypene. Dette fordi MapStruct ser etter metoder
som har riktig signatur til å gjøre jobben. Av og til må man kode slike metoder for hånd, da kan man lage en abstrakt klasse
i stedet for en interface.
Nøsting
Vi kan håndtere hierarkiske data også:
public class Vehicle {
private Brand brand; // anta at det finnes en klasse "Brand" med bl.a. feltet "name"
private int numberOfSeats;
private String type;
// konstruktør, gettere, settere etc.
}
public class Automobile {
private String make;
private int seatCount;
private String type;
// konstruktør, gettere, settere etc.
}
…med dot-notasjon (som kan brukes begge veier)
@Mapper // dette gjør at MapStruct sin annotation processor genererer en implementasjon
public interface VehicleMapper {
VehicleMapper INSTANCE = Mappers.getMapper( VehicleMapper.class );
@Mapping(source = "numberOfSeats", target = "seatCount")
@Mapping(source = "brand.name", target = "make") // <----
Automobile vehicleToAutomobile(Vehicle Vehicle);
}
Til slutt
Dette er bare en liten del av hva man kan gjøre med MapStruct. Hos DNB er dette et standardverktøy, og jeg har laget mappere for klasser med hundrevis av felt. En av mange halvanstendige tutorials finner du her
God jul!