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.