Advanced MapStruct Tutorial

MapStruct is a powerful Java annotation processor that simplifies the mapping between Java bean types. In this advanced tutorial, we will explore several advanced features of MapStruct, such as mapping from multiple sources to a target object, using Java expressions in mappings and much more!

Prerequisites

Before diving into the examples, ensure you have checked this introduction article to Mapstruct: How to Map Your DTO Objects with MapStruct

Then, make sure you have the following dependencies in your pom.xml if you are using Maven:

<dependencies>
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct</artifactId>
        <version>1.6.3.Final</version>
    </dependency>
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct-processor</artifactId>
        <version>1.6.3.Final</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

For Gradle, add the following to your build.gradle:

dependencies {
    implementation 'org.mapstruct:mapstruct:1.6.3.Final'
    annotationProcessor 'org.mapstruct:mapstruct-processor:1.6.3.Final'
}

1. Mapping from Multiple Sources

MapStruct allows you to map data from multiple source objects into a single target object. This is particularly useful when you need to aggregate data from different sources.

Example

Suppose we have two source classes, Student and Address, and we want to create a DeliveryAddress object.

public class Student {
    private String name;
    private Long id;

    // Getters and Setters
}

public class Address {
    private String city;
    private String state;
    private int houseNo;

    // Getters and Setters
}

public class DeliveryAddress {
    private String name;
    private String city;
    private String state;
    private int houseNumber;

    // Getters and Setters
}

And here is the Mapper Interface which maps multiple source objects to DeliveryAddress:

@Mapper
public interface DeliveryAddressMapper {
    @Mapping(source = "student.name", target = "name")
    @Mapping(source = "address.city", target = "city")
    @Mapping(source = "address.state", target = "state")
    @Mapping(source = "address.houseNo", target = "houseNumber")
    DeliveryAddress getDeliveryAddress(Student student, Address address);
}
mapstruct tutorial for java

Finally, here is how you can use your DeliveryAddressMapper to build a DeliveryAddress:

Student student = new Student();
student.setName("John Doe");
Address address = new Address();
address.setCity("New York");
address.setState("NY");
address.setHouseNo(123);

DeliveryAddressMapper mapper = Mappers.getMapper(DeliveryAddressMapper.class);
DeliveryAddress deliveryAddress = mapper.getDeliveryAddress(student, address);

2. Using Java Expressions in Mappings

MapStruct allows you to use Java expressions to perform more complex mappings.

Example

Suppose we want to calculate the length of a student’s name while mapping.

Mapper Interface with Expression

@Mapper
public interface StudentMapper {
    @Mapping(target = "nameLength", expression = "java(student.getName().length())")
    StudentDto toStudentDto(Student student);
}

public class StudentDto {
    private int nameLength;

    // Getters and Setters
}

Usage

Student student = new Student();
student.setName("John Doe");

StudentMapper mapper = Mappers.getMapper(StudentMapper.class);
StudentDto studentDto = mapper.toStudentDto(student);

3. Lazy Mapping with @AfterMapping

The @AfterMapping annotation allows you to perform additional processing after the mapping is done, which can be useful for lazy loading or further calculations.

Example

Suppose we want to set a default value after mapping.

Mapper Interface with After Mapping

@Mapper
public interface UserMapper {

    UserDto userToUserDto(User user);

    @AfterMapping
    default void setDefaultValues(@MappingTarget UserDto userDto) {
        if (userDto.getRole() == null) {
            userDto.setRole("USER"); // Set default role if not provided
        }
    }
}

Usage

User user = new User();
user.setName("Jane Doe");

UserMapper mapper = Mappers.getMapper(UserMapper.class);
UserDto userDto = mapper.userToUserDto(user);

4. Additional Advanced Features

4.1 Nested Mappings

MapStruct can handle nested mappings easily by specifying the path for nested properties.

@Mapper
public interface OrderMapper {
    @Mapping(target = "customerName", source = "customer.name")
    OrderDto orderToOrderDto(Order order);
}

4.2 Custom Mapping Methods

You can define custom methods for complex mappings.

@Mapper
public interface ProductMapper {

    @Mappings({
        @Mapping(target = "price", source = "product.price"),
        @Mapping(target = "discountedPrice", expression = "java(calculateDiscount(product))")
    })
    ProductDto productToProductDto(Product product);

    default double calculateDiscount(Product product) {
        return product.getPrice() * 0.9; // Apply a 10% discount
    }
}

This custom mapping demonstrates an advanced use of MapStruct, including field mapping and custom logic for transformations. Here’s an explanation:

What This Code Does:

The ProductMapper interface maps a Product object to a ProductDto object, while performing:

  1. Standard field mapping: Maps the price field directly from Product to ProductDto.
  2. Custom field mapping: Calculates the discountedPrice in ProductDto using a custom method (calculateDiscount).

Conclusion

In this advanced MapStruct tutorial, we explored various features that enhance object mapping in Java applications:

  • Mapping from multiple sources into a single target object.
  • Using Java expressions for complex field mappings.
  • Implementing lazy mapping with the @AfterMapping annotation.
  • Additional features like nested mappings and custom methods.

By leveraging these advanced features, you can create efficient and maintainable mappings in your Java applications using MapStruct.

Was this article helpful? We need your support to keep MasterTheBoss alive!