The Master Class of "Learn Spring Security" is live:
1. Overview
In this article, we’ll explore the use of MapStruct which is, simply put, a Java Bean mapper.
This API contains functions that automatically map between two Java Beans. With MapStruct we only need to create the interface and the library will automatically create a concrete implementation during compile time.
2. MapStruct and Transfer Object Pattern
For most applications, you’ll notice a lot of boilerplate code converting POJOs to other POJOs.
For example, a common type of conversion happens between persistence-backed entities and DTOs that go out to the client side.
So that is the problem that MapStruct solves – manually creating bean mappers is time-consuming. The library is able to generate bean mapper classes automatically.
3. Maven
Let’s add the below dependency into our Maven pom.xml:
<dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-jdk8</artifactId> <version>1.0.0.Final</version> </dependency>
The latest stable release can always be found here:
Let’s also add the annotationProcessorPaths section to the configuration part of the maven-compiler-plugin plugin. The mapstruct-processor is used to generate the mapper implementation during the build:
<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>${org.mapstruct.version}</version> </path> </annotationProcessorPaths> </configuration> </plugin>
4. Basic Mapping
4.1. Creating a POJO
Let’s first create a simple Java POJO:
public class SimpleSource { private String name; private String description; // getters and setters } public class SimpleDestination { private String name; private String description; // getters and setters }
4.2. The Mapper Interface
@Mapper public interface SimpleSourceDestinationMapper { SimpleDestination sourceToDestination(SimpleSource source); SimpleSource destinationToSource(SimpleDestination destination); }
Notice we did not create any implementation class for our SimpleSourceDestinationMapper – because MapStruct creates it for us.
4.3. The New Mapper
We can trigger the MapStruct processing by executing an mvn clean install.
This will generate the implementation class under /target/generated-sources/annotations/.
Here is the class that MapStruct auto-creates for us:
public class SimpleSourceDestinationMapperImpl implements SimpleSourceDestinationMapper { @Override public SimpleDestination sourceToDestination(SimpleSource source) { if ( source == null ) { return null; } SimpleDestination simpleDestination = new SimpleDestination(); simpleDestination.setName( source.getName() ); simpleDestination.setDescription( source.getDescription() ); return simpleDestination; } @Override public SimpleSource destinationToSource(SimpleDestination destination){ if ( destination == null ) { return null; } SimpleSource simpleSource = new SimpleSource(); simpleSource.setName( destination.getName() ); simpleSource.setDescription( destination.getDescription() ); return simpleSource; } }
4.4. A Test Case
Finally, with everything generated, let’s write a test case will show that values in SimpleSource match values in SimpleDestination.
public class SimpleSourceDestinationMapperTest { private SimpleSourceDestinationMapper mapper = Mappers.getMapper(SimpleSourceDestinationMapper.class); @Test public void givenSourceToDestination_whenMaps_thenCorrect() { SimpleSource simpleSource = new SimpleSource(); simpleSource.setName("SourceName"); simpleSource.setDescription("SourceDescription"); SimpleDestination destination = mapper.sourceToDestination(simpleSource); assertEquals(simpleSource.getName(), destination.getName()); assertEquals(simpleSource.getDescription(), destination.getDescription()); } @Test public void givenDestinationToSource_whenMaps_thenCorrect() { SimpleDestination destination = new SimpleDestination(); destination.setName("DestinationName"); destination.setDescription("DestinationDescription"); SimpleSource source = mapper.destinationToSource(destination); assertEquals(destination.getName(), source.getName()); assertEquals(destination.getDescription(), source.getDescription()); } }
5. Mapping with Dependency Injection
Next, let’s obtain an instance of a mapper in MapStruct by simply calling Mappers.getMapper(YourClass.class).
Of course that’s a very manual way of getting the instance – a much better alternative would be injecting the mapper directly where we need it (if our project uses any Dependency Injection solution).
Luckily MapStruct has solid support for both Spring and CDI (Contexts and Dependency Injection).
To use Spring IoC in our mapper, we simply need to add the componentModel attribute to @Mapper with the value spring and for CDI would be cdi .
5.1. Modify the Mapper
Add the following code to SimpleSourceDestinationMapper:
@Mapper(componentModel = "spring") public interface SimpleSourceDestinationMapper
6. Mapping Fields with Different Field Names
From our previous example, MapStruct was able to map our beans automatically because they have the same field names. So what if a bean we are about to map has a different field name?
For our example, we will be creating a new bean called Employee and EmployeeDTO.
6.1. New POJOs
public class EmployeeDTO { private int employeeId; private String employeeName; // getters and setters }
public class Employee { private int id; private String name; // getters and setters }
6.2. The Mapper Interface
When mapping different field names, we will need to configure its source field to its target field and to do that we will need to add @Mappings annotation. This annotation accepts an array of @Mapping annotation which we will use to add the target and source attribute.
In MapStruct we can also use dot notation to define a member of a bean:
@Mapper public interface EmployeeMapper { @Mappings({ @Mapping(target="employeeId", source="entity.id"), @Mapping(target="employeeName", source="entity.name") }) EmployeeDTO employeeToEmployeeDTO(Employee entity); @Mappings({ @Mapping(target="id", source="dto.employeeId"), @Mapping(target="name", source="dto.employeeName") }) Employee employeeDTOtoEmployee(EmployeeDTO dto); }
6.3. The Test Case
Again we need to test that both source and destination object values match:
@Test public void givenEmpDTODiffNametoEmp_whenMaps_thenCorrect() { EmployeeDTO dto = new EmployeeDTO(); dto.setEmployeeId(1); dto.setEmployeeName("John"); Employee entity = mapper.employeeDTOtoEmployee(dto); assertEquals(dto.getEmployeeId(), entity.getId()); assertEquals(dto.getEmployeeName(), entity.getName()); }
More test cases can be found in the Github project.
7. Mapping Beans with Child Beans
Next we’ll show how to map a bean with references to other beans.
7.1. Modify the POJO
Let’s add a new bean reference in the Employee object:
public class EmployeeDTO { private int employeeId; private String employeeName; private DivisionDTO division; // getters and setters omitted }
public class Employee { private int id; private String name; private Division division; // getters and setters omitted }
public class Division { private int id; private String name; // default constructor, getters and setters omitted }
7.2. Modify the Mapper
Here we need to add a method to convert the Division to DivisionDTO and vice versa; if MapStruct detects that the object type needs to be converted and the method to convert exists in the same class then it will use it automatically.
Let’s add this to the mapper:
DivisionDTO divisionToDivisionDTO(Division entity); Division divisionDTOtoDivision(DivisionDTO dto);
7.3. Modify the Test Case
Let’s modify and add a few test cases to the existing one:
@Test public void givenEmpDTONestedMappingToEmp_whenMaps_thenCorrect() { EmployeeDTO dto = new EmployeeDTO(); dto.setDivision(new DivisionDTO(1, "Division1")); Employee entity = mapper.employeeDTOtoEmployee(dto); assertEquals(dto.getDivision().getId(), entity.getDivision().getId()); assertEquals(dto.getDivision().getName(), entity.getDivision().getName()); }
8. Mapping With Type Conversion
MapStruct also offers a couple of ready made implicit type conversions and for our example, we will try to convert a String date to an actual Date object.
For more details on implicit type conversion, you may read the MapStruct reference guide.
8.1. Modify the Beans
Add a start date for our employee:
public class Employee { // other fields private Date startDt; // getters and setters }
public class EmployeeDTO { // other fields private String employeeStartDt; // getters and setters }
8.2. Modify the Mapper
Modify the mapper and provide the dateFormat for our start date:
@Mappings({ @Mapping(target="employeeId", source = "entity.id"), @Mapping(target="employeeName", source = "entity.name"), @Mapping(target="employeeStartDt", source = "entity.startDt", dateFormat = "dd-MM-yyyy HH:mm:ss")}) EmployeeDTO employeeToEmployeeDTO(Employee entity); @Mappings({ @Mapping(target="id", source="dto.employeeId"), @Mapping(target="name", source="dto.employeeName"), @Mapping(target="startDt", source="dto.employeeStartDt", dateFormat="dd-MM-yyyy HH:mm:ss")}) Employee employeeDTOtoEmployee(EmployeeDTO dto);
8.3. Modify the Test Case
Let’s add a few more test case to verify the conversion is correct:
private static final String DATE_FORMAT = "dd-MM-yyyy HH:mm:ss"; @Test public void givenEmpStartDtMappingToEmpDTO_whenMaps_thenCorrect() throws ParseException { Employee entity = new Employee(); entity.setStartDt(new Date()); EmployeeDTO dto = mapper.employeeToEmployeeDTO(entity); SimpleDateFormat format = new SimpleDateFormat(DATE_FORMAT); assertEquals(format.parse(dto.getEmployeeStartDt()).toString(), entity.getStartDt().toString()); } @Test public void givenEmpDTOStartDtMappingToEmp_whenMaps_thenCorrect() throws ParseException { EmployeeDTO dto = new EmployeeDTO(); dto.setEmployeeStartDt("01-04-2016 01:00:00"); Employee entity = mapper.employeeDTOtoEmployee(dto); SimpleDateFormat format = new SimpleDateFormat(DATE_FORMAT); assertEquals(format.parse(dto.getEmployeeStartDt()).toString(), entity.getStartDt().toString()); }
9. Conclusion
This article provided an introduction to MapStruct, we have introduced most of the basics of the Mapping library and how to use it in our applications.
The implementation of these examples and tests can be found in the Github project. This is a Maven project, so it should be easy to import and run as it is.