Generic Top

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE

1. Overview

In this article, we'll learn how to use custom mapper with the MapStruct library.

The MapStruct library is used for mapping between Java bean types. By using a custom mapper with MapStruct, we can customize the default mapping methods.

2. Maven Dependencies

Let’s add the mapstruct-jdk8 library into our Maven pom.xml:

<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-jdk8</artifactId>
    <version>1.3.0.Final</version> 
</dependency>

To see the auto-generated methods inside the project's target folder, we have to add the annotationProcessorPaths to the maven-compiler-plugin plugin:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.5.1</version>
    <configuration>
        <source>1.8</source>
        <target>1.8</target>
        <annotationProcessorPaths>
            <path>
                <groupId>org.mapstruct</groupId>
                <artifactId>mapstruct-processor</artifactId>
                <version>1.3.0.Final</version>
            </path>
        </annotationProcessorPaths>
    </configuration>
</plugin>

3. Custom Mapper

Custom mappers are used to solve specific conversion requirements. To achieve this, we have to define a method to do the conversion. Then, we must notify MapStruct about the method. Finally, MapStruct will call the method to do the conversion from source to target.

For instance, let's imagine that we have an app that calculates the user's body mass index (BMI) report. To calculate BMI, we have to collect the user's body values. To convert imperial units to metric units, we can use the custom mapper methods.

There are two ways of using a custom mapper with MapStruct. We can either call the custom method by typing it inside the @Mapping annotation's qualifiedByName property, or we can create an annotation for it.

Before we start, we have to define a DTO class to hold imperial values:

public class UserBodyImperialValuesDTO {
    private int inch;
    private int pound;
    // constructor, getters, and setters
}

Next, let's define a DTO class to hold metric values:

public class UserBodyValues {
    private double kilogram;
    private double centimeter;
    // constructor, getters, and setters
}

3.1. Custom Mapper with Method

To start using custom mappers, let's create an interface with the @Mapper annotation:

@Mapper 
public interface UserBodyValuesMapper {
    //...
}

Second, let's create our custom method with the return type we want and the argument we need to convert. We have to use the @Named annotation with the value parameter to inform MapStruct about the custom mapper method:

@Mapper
public interface UserBodyValuesMapper {

    @Named("inchToCentimeter")
    public static double inchToCentimeter(int inch) {
        return inch * 2.54;
    }
 
    //...
}

And finally, let's define the mapper interface method with the @Mapping annotation. Within this annotation, we'll tell MapStruct about the source type, target type, and the method it will use:

@Mapper
public interface UserBodyValuesMapper {
    UserBodyValuesMapper INSTANCE = Mappers.getMapper(UserBodyValuesMapper.class);
    
    @Mapping(source = "inch", target = "centimeter", qualifiedByName = "inchToCentimeter")
    public UserBodyValues userBodyValuesMapper(UserBodyImperialValuesDTO dto);
    
    @Named("inchToCentimeter") 
    public static double inchToCentimeter(int inch) { 
        return inch * 2.54; 
    }
}

Let's test our custom mapper:

UserBodyImperialValuesDTO dto = new UserBodyImperialValuesDTO();
dto.setInch(10);

UserBodyValues obj = UserBodyValuesMapper.INSTANCE.userBodyValuesMapper(dto);

assertNotNull(obj);
assertEquals(25.4, obj.getCentimeter(), 0);

3.2. Custom Mapper with an Annotation

To use a custom mapper with an annotation, we have to define an annotation instead of the @Named annotation. Then, we have to inform MapStruct about the newly created annotation by specifying the @Mapping annotation's qualifiedByName parameter.

Let's see how we define the annotation:

@Qualifier
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface PoundToKilogramMapper {
}

Let's add the @PoundToKilogramMapper annotation to our poundToKilogram method:

@PoundToKilogramMapper
public static double poundToKilogram(int pound) {
    return pound * 0.4535;
}

Now, let's define the mapper interface method with the @Mapping annotation. Within the mapping annotation, we'll tell MapStruct about the source type, the target type, and the annotation class that it will use:

@Mapper
public interface UserBodyValuesMapper {
    UserBodyValuesMapper INSTANCE = Mappers.getMapper(UserBodyValuesMapper.class);

    @Mapping(source = "pound", target = "kilogram", qualifiedBy = PoundToKilogramMapper.class)
    public UserBodyValues userBodyValuesMapper(UserBodyImperialValuesDTO dto);

    @PoundToKilogramMapper
    public static double poundToKilogram(int pound) {
        return pound * 0.4535;
    }
}

Finally, let's test our custom mapper:

UserBodyImperialValuesDTO dto = new UserBodyImperialValuesDTO();
dto.setPound(100);

UserBodyValues obj = UserBodyValuesMapper.INSTANCE.userBodyValuesMapper(dto);

assertNotNull(obj);
assertEquals(45.35, obj.getKilogram(), 0);

4. Conclusion

In this article, we learned how to use a custom mapper with the MapStruct library.

The implementations of these examples and tests are available over on GitHub.

Generic bottom

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE

Leave a Reply

avatar
  Subscribe  
Notify of