How To Get Rid of Annoying Java Types Mappings in Spring Boot Application

In a previous post, a persistence layer was added to the Spring Boot application. In this article, a problem with Java types mappings will be solved. The issue is related to the boilerplate code. Typical java application requires a lot of transformations from one representation to another. As a result, a lot of mapping code is required to be written. Another issue is with readability: in the case of classes with many fields, it is easy to miss some of them. Mappers that were created in How to Create Spring Boot Application will be rewritten in a declarative way using MapStruct.

spring boot mapstruct

Existing Mappers

At the moment two mappers are created in an application.

API layer ItemApiMapper:

@Component
public class ItemApiMapper {

    public CreateItemRequest map(CreateItemDto createItemDto) {
        return CreateItemRequest.builder()
                .title(createItemDto.getTitle())
                .build();
    }

    public UpdateItemRequest map(UpdateItemDto updateItemDto) {
        return UpdateItemRequest.builder()
                .title(updateItemDto.getTitle())
                .build();
    }

}

Service layer ProductItemMapper:

@Component
public class ProductItemMapper {

    public Item map(ProductItem productItem) {
        return Item.builder()
                .id(productItem.getId())
                .title(productItem.getTitle())
                .build();
    }

    public Page<Item> map(Page<ProductItem> page) {
        return page.map(this::map);
    }

    public ProductItem map(CreateItemRequest createItemRequest) {
        return ProductItem.builder()
                .title(createItemRequest.getTitle())
                .build();
    }

    public void map(ProductItem item, UpdateItemRequest updateItemRequest) {
        item.setTitle(updateItemRequest.getTitle());
    }

}

They are pretty small because of a small number of fields in mapped java classes. However, even now they are hard to read. Java Types Mappings in a declarative way is a solution for a specified problem.

java types mappings

MapStruct

MapStruct and Orika are examples of libraries that can be used for declarative java types mappings. They are very similar from the perspective of provided functions. However, these tools work in a totally different way. In the application, MapStruct will be used because of the fact that we can see the actual implementation of a class that will be used for mapping.

MapStruct Dependencies

In order to use MapStruct next lines of code have to be added to build.gradle:

implementation 'org.mapstruct:mapstruct:1.4.2.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.4.2.Final'

By doing this, we are adding MapStruct dependency and the annotation processor to generate Java classes based on a declaration.

MapStruct Mappers

The next step is to rewrite our mappers. They will have the next form:

@Mapper(componentModel = "spring")
public interface ItemApiMapper {
    
    CreateItemRequest map(CreateItemDto createItemDto);

    UpdateItemRequest map(UpdateItemDto updateItemDto);

}

@Mapper(componentModel = "spring")
public interface ProductItemMapper {

    default Page<Item> map(Page<ProductItem> page) {
        return page.map(this::map);
    }

    Item map(ProductItem productItem);

    @Mapping(target = "id", ignore = true)
    ProductItem map(CreateItemRequest createItemRequest);

    @Mapping(target = "id", ignore = true)
    void map(@MappingTarget ProductItem item, UpdateItemRequest updateItemRequest);

}

@Mapper(componentModel = “spring”) tells that the class for the interface has to be generated. With specified component model the generated class will act as a Spring singleton bean and can be injected in other beans.

@Mapping(target = “id”, ignore = true) explicitly specifies that id field has to be ignored in mapping process. In our case, it is used to remove Gradle warnings. However, it can be used to ignore fields.

Page is mapped in a default method based on the existing map method.

That is all that we have to do in our application.

Generated MapStruct classes

MapStruct generates classes that can be viewed and debugged. In our case they have the next form (build/generated/sources/annotationProcessor):

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2021-05-19T13:37:35+0300",
    comments = "version: 1.4.2.Final, compiler: IncrementalProcessingEnvironment from gradle-language-java-6.8.3.jar, environment: Java 11.0.9 (Oracle Corporation)"
)
@Component
public class ItemApiMapperImpl implements ItemApiMapper {

    @Override
    public CreateItemRequest map(CreateItemDto createItemDto) {
        if ( createItemDto == null ) {
            return null;
        }

        CreateItemRequest createItemRequest = new CreateItemRequest();

        createItemRequest.setTitle( createItemDto.getTitle() );

        return createItemRequest;
    }

    @Override
    public UpdateItemRequest map(UpdateItemDto updateItemDto) {
        if ( updateItemDto == null ) {
            return null;
        }

        UpdateItemRequest updateItemRequest = new UpdateItemRequest();

        updateItemRequest.setTitle( updateItemDto.getTitle() );

        return updateItemRequest;
    }
}





@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2021-05-19T13:37:35+0300",
    comments = "version: 1.4.2.Final, compiler: IncrementalProcessingEnvironment from gradle-language-java-6.8.3.jar, environment: Java 11.0.9 (Oracle Corporation)"
)
@Component
public class ProductItemMapperImpl implements ProductItemMapper {

    @Override
    public Item map(ProductItem productItem) {
        if ( productItem == null ) {
            return null;
        }

        Item item = new Item();

        item.setId( productItem.getId() );
        item.setTitle( productItem.getTitle() );

        return item;
    }

    @Override
    public ProductItem map(CreateItemRequest createItemRequest) {
        if ( createItemRequest == null ) {
            return null;
        }

        ProductItem productItem = new ProductItem();

        productItem.setTitle( createItemRequest.getTitle() );

        return productItem;
    }

    @Override
    public void map(ProductItem item, UpdateItemRequest updateItemRequest) {
        if ( updateItemRequest == null ) {
            return;
        }

        item.setTitle( updateItemRequest.getTitle() );
    }
}

MapStruct Features

In this application, only basic MapStruct features were used. However, it can be used for:

  • Map fields with different names
  • Convert fields from one type to another
  • Add hooks before and after the mapping
  • Use constants and expression language during the mapping.
  • Mapping based on provided context
  • Reusability of mappers for child entities

Details can be found at MapStruct Documentation.

Summary

To sum up, in this post mappers were rewritten with MapStruct in a declarative java types mappings way.

The whole code can be found at Github v3.0.0-mapstruct branch.

The whole course:

Oleksii

Java development

You may also like...

Leave a Reply

Your email address will not be published. Required fields are marked *