Mapping multiple app layers with Mapstruct

Smart managing of multiple layers

When you work with multiple application layers, a common problem faced is transferring data objects from one layer to another. This transformation from data object A to data object B is called mapping. There are several Mapping-Frameworks which should support the developers to implement this faster and easier. For Java, we have two popular mapping frameworks: Mapstruct and Dozer.

The older one is Dozer. It´s based on XML mapping definitions which will be resolved during runtime. Because the mapping resolution is done during runtime, this framework is recommended for dynamic mapping purposes. But it lacks a bit of performance compared to Mapstruct — the second mapping framework.

When you only need static mappings, I strongly recommend Mapstruct. The mapping resolution is done during compile time and generates mapping-code which has a hand-written-style. It has the highest performance threshold for mapping objects from object A to object B.

Multi-Layer Architecture

Mapstruct has a good documentation which shows a lot of possibilities you can do with the framework: http://mapstruct.org/documentation/stable/reference/html/. However, during project work, I have to implement an API which should return data based on an existing model.

Because of layering architecture, I decided to implement an API-model layer and map the existing model into the API-model. Mapstruct gives you many tools to achieve that. The toughest part is, that the existing and the target models have a complex hierarchy. So I wanted to define mappings from types in the upper hierarchy only once and the children types inherit the mapping configuration from their parents.

To accomplish this, Mapstruct has an annotation called “InheritConfiguration”, which you can find here: http://mapstruct.org/documentation/stable/reference/html/#mapping-configuration-inheritance.

The interesting point here is, that the scope for inheritance lookup could be parent class mappers or MapperConfigs: http://mapstruct.org/documentation/stable/reference/html/#shared-configurations. In our example, we had many abstract types, where we don’t need a real mapper. Instead, we defined a MapperConfig for each abstract type and inherited along the source model.

Source model
MapperConfig classes

Here you see, that the MapperConfig interfaces extend from each other, along with the inheritance, the mapping methods inherit their configuration too:

@MapperConfig
public interface VehicleMapperConfig extends TransportationMeanMapperConfig {
  @InheritConfiguration(name = "mapTransportationMean")
@Mappings({
@Mapping(source = "mainBreak.plate", target="breakPlate"),
@Mapping(source = "mainBreak.body", target= "breakBody")
})
void mapVehicle(Vehicle vehicle, @MappingTarget VehicleDTO dto);
}

For concrete types, normal Mappers will be defined, which uses their MapperConfig, here, for example, the Airplane Mapper.

@Mapper(config = AirplaneMapperConfig.class)
public abstract class AirplaneMapper {
  @InheritConfiguration(name = "mapFlyingVehicle")
public abstract AirplaneDTO mapAirplane(Airplane airplane);

After code generation you’ll see that Mapstruct only generates code for the concrete Mappers inheriting the mapping-configuration from the MapperConfig:

Inherited Mapping configuration

When you take a closer look, for example at the AirplaneMapper, you’ll see that the mappings from the mapping-configuration-hierarchy are used (highlighted some inherited mappings):

public class AirplaneMapperImpl extends AirplaneMapper {
private final LengthMapper lengthMapper = Mappers.getMapper(
LengthMapper.class );
    @Override
public AirplaneDTO mapAirplane(Airplane airplane) {
        if ( airplane == null ) {
return null;
}
        AirplaneDTO airplaneDTO = new AirplaneDTO();
        String body = airplaneMainBreakBody( airplane );
if ( body != null ) {
airplaneDTO.setBreakBody( body );
}
String plate = airplaneMainBreakPlate( airplane );
if ( plate != null ) {
airplaneDTO.setBreakPlate( plate );
}
airplaneDTO.setId( airplane.getId() );
List<String> list = airplane.getTransportationObjects();
if ( list != null ) {
airplaneDTO.setTransportationObjects(
new ArrayList<String>( list ) );
}
else {
airplaneDTO.setTransportationObjects( null );
}
if ( airplane.getWeight() != null ) {
airplaneDTO.setWeight
( String.valueOf( airplane.getWeight() ) );
}

airplaneDTO.setWidth( lengthMapper.mapLength(
airplane.getWidth() ) );
airplaneDTO.setHeight( lengthMapper.mapLength(
airplane.getHeight() ) );
airplaneDTO.setLength( lengthMapper.mapLength(
airplane.getLength() ) );
airplaneDTO.setMaxAltitude( lengthMapper.mapLength(
airplane.getMaxAltitude() ) );
airplaneDTO.setWings( airplane.getWings() );
        return airplaneDTO;
}

With this mapping architecture, it is easy to define new mappings on higher types and all children types automatically implement this mapping, too. This makes maintenance and adjustments much easier. I know, that this example only shows a subset of the functionality of Mapstruct (no decorators, no ‘aftermapping’ or ‘beforemapping’, no expressions and many more). But it shows a common problem with type hierarchies and how to map them.

This example code can be found on the bitgrip account of Github (https://github.com/bitgrip/mapstruct-inheritance-example) to have a closer look.

Any feedback or hints on how to improve the usage of Mapstruct for such cases will be highly appreciated.

Tips for using Mapstruct

Finally, I want to share some general experience and helpful hints for using Mapstruct:

  • Try to stay in the Mapstruct-paradigma. You might think, it’s much easier to write this mapping in a method and reference it by an expression, but when starting with that, you could get problems especially when inheriting or reusing configuration.
  • Avoid Java expressions in Mapstruct and only use them when there is definitely no other way. (most of the time, there is another way)
  • Define an AtomicMapper for mapping of common types which are not implemented by Mapstruct. In my example, the LengthMapper for mapping a Length.
  • Check the generated code often.
  • You can pass the source object to another mapping by reusing the source parameter name.

I hope this is truly helpful for someone using Mapstruct.