Partner – Microsoft – NPI (cat=Java)
announcement - icon

Microsoft JDConf 2024 conference is getting closer, on March 27th and 28th. Simply put, it's a free virtual event to learn about the newest developments in Java, Cloud, and AI.

Josh Long and Mark Heckler are kicking things off in the keynote, so it's definitely going to be both highly useful and quite practical.

This year’s theme is focused on developer productivity and how these technologies transform how we work, build, integrate, and modernize applications.

For the full conference agenda and speaker lineup, you can explore JDConf.com:

>> RSVP Now

1. Overview

In this tutorial, 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.

Further reading:

Custom Mapper with MapStruct

Learn how to use custom mapper with the MapStruct library

Ignoring Unmapped Properties with MapStruct

MapStruct allows us to copy between Java beans. There are a few ways we can configure it to handle missing fields.

Using Multiple Source Objects with MapStruct

Learn how to use multiple source objects with MapStruct.

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. But the library can 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</artifactId>
    <version>1.6.0.Beta1</version> 
</dependency>

The latest stable release of MapStruct and its processor are both available from the Maven Central Repository.

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.11.0</version>
    <configuration>
        <annotationProcessorPaths>
            <path>
                <groupId>org.mapstruct</groupId>
                <artifactId>mapstruct-processor</artifactId>
                <version>1.6.0.Beta1</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 an 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. Test Case

Finally, with everything generated, let’s write a test case showing that values in SimpleSource match values in SimpleDestination:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class SimpleSourceDestinationMapperIntegrationTest {

    @Autowired
    SimpleSourceDestinationMapper simpleSourceDestinationMapper;

    @Test
    public void givenSourceToDestination_whenMaps_thenCorrect() {
        SimpleSource simpleSource = new SimpleSource();
        simpleSource.setName("SourceName");
        simpleSource.setDescription("SourceDescription");

        SimpleDestination destination = simpleSourceDestinationMapper.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 = simpleSourceDestinationMapper.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 merely calling Mappers.getMapper(YourClass.class).

Of course, that’s a very manual way of getting the instance. However, a much better alternative is to inject 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 need to add the componentModel attribute to @Mapper with the value spring, and for CDI, it would be cdi.

5.1. Modify the Mapper

Add the following code to SimpleSourceDestinationMapper:

@Mapper(componentModel = "spring")
public interface SimpleSourceDestinationMapper

5.2. Inject Spring Components into the Mapper

Sometimes, we’ll need to utilize other Spring components inside our mapping logic. In this case, we have to use an abstract class instead of an interface:

@Mapper(componentModel = "spring")
public abstract class SimpleDestinationMapperUsingInjectedService

Then, we can easily inject the desired component using a well-known @Autowired annotation and use it in our code:

@Mapper(componentModel = "spring")
public abstract class SimpleDestinationMapperUsingInjectedService {

    @Autowired
    protected SimpleService simpleService;

    @Mapping(target = "name", expression = "java(simpleService.enrichName(source.getName()))")
    public abstract SimpleDestination sourceToDestination(SimpleSource source);
}

We must remember not to make the injected bean private! This is because MapStruct has to access the object in the generated implementation class.

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?

In this 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 @Mapping annotation for each field.

In MapStruct, we can also use dot notation to define a member of a bean:

@Mapper
public interface EmployeeMapper {

    @Mapping(target = "employeeId", source = "entity.id")
    @Mapping(target = "employeeName", source = "entity.name")
    EmployeeDTO employeeToEmployeeDTO(Employee entity);

    @Mapping(target = "id", source = "dto.employeeId")
    @Mapping(target = "name", source = "dto.employeeName")
    Employee employeeDTOtoEmployee(EmployeeDTO dto);
}

6.3. Test Case

Again, we need to test that both source and destination object values match:

@Test
public void givenEmployeeDTOwithDiffNametoEmployee_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 to 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, 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, check out the MapStruct reference guide.

8.1. Modify the Beans

We 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

We modify the mapper and provide the dateFormat for our start date:

@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);

@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 cases 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. Mapping With an Abstract Class

Sometimes, we may want to customize our mapper in a way that exceeds @Mapping capabilities.

For example, in addition to type conversion, we may want to transform the values in some way, as in our example below.

In such cases, we can create an abstract class and implement methods we want to have customized, and leave abstract those that should be generated by MapStruct.

9.1. Basic Model

In this example, we’ll use the following class:

public class Transaction {
    private Long id;
    private String uuid = UUID.randomUUID().toString();
    private BigDecimal total;

    //standard getters
}

and a matching DTO:

public class TransactionDTO {

    private String uuid;
    private Long totalInCents;

    // standard getters and setters
}

The tricky part here is converting the BigDecimal total amount of dollars into a Long totalInCents.

9.2. Defining a Mapper

We can achieve this by creating our Mapper as an abstract class:

@Mapper
abstract class TransactionMapper {

    public TransactionDTO toTransactionDTO(Transaction transaction) {
        TransactionDTO transactionDTO = new TransactionDTO();
        transactionDTO.setUuid(transaction.getUuid());
        transactionDTO.setTotalInCents(transaction.getTotal()
          .multiply(new BigDecimal("100")).longValue());
        return transactionDTO;
    }

    public abstract List<TransactionDTO> toTransactionDTO(
      Collection<Transaction> transactions);
}

Here, we’ve implemented our fully customized mapping method for a single object conversion.

On the other hand, we left the method, which is meant to map Collection to a List abstract, so MapStruct will implement it for us.

9.3. Generated Result

Since we have already implemented the method to map a single Transaction into a TransactionDTO, we expect MapStruct to use it in the second method.

The following will be generated:

@Generated
class TransactionMapperImpl extends TransactionMapper {

    @Override
    public List<TransactionDTO> toTransactionDTO(Collection<Transaction> transactions) {
        if ( transactions == null ) {
            return null;
        }

        List<TransactionDTO> list = new ArrayList<>();
        for ( Transaction transaction : transactions ) {
            list.add( toTransactionDTO( transaction ) );
        }

        return list;
    }
}

As we can see in line 12, MapStruct uses our implementation in the method that is generated.

10. Before-Mapping and After-Mapping Annotations

Here’s another way to customize @Mapping capabilities by using @BeforeMapping and @AfterMapping annotations. The annotations are used to mark methods that are invoked right before and after the mapping logic.

They are quite useful in scenarios where we might want this behavior to be applied to all mapped super-types.

Let’s take a look at an example that maps the sub-types of Car ElectricCar and BioDieselCar to CarDTO.

While mapping, we would like to map the notion of types to the FuelType enum field in the DTO. Then after the mapping is done, we’d like to change the name of the DTO to uppercase.

10.1. Basic Model

We’ll use the following classes:

public class Car {
    private int id;
    private String name;
}

Sub-types of Car:

public class BioDieselCar extends Car {
}
public class ElectricCar extends Car {
}

The CarDTO with an enum field type FuelType:

public class CarDTO {
    private int id;
    private String name;
    private FuelType fuelType;
}
public enum FuelType {
    ELECTRIC, BIO_DIESEL
}

10.2. Defining the Mapper

Now let’s proceed and write our abstract mapper class that maps Car to CarDTO:

@Mapper
public abstract class CarsMapper {
    @BeforeMapping
    protected void enrichDTOWithFuelType(Car car, @MappingTarget CarDTO carDto) {
        if (car instanceof ElectricCar) {
            carDto.setFuelType(FuelType.ELECTRIC);
        }
        if (car instanceof BioDieselCar) { 
            carDto.setFuelType(FuelType.BIO_DIESEL);
        }
    }

    @AfterMapping
    protected void convertNameToUpperCase(@MappingTarget CarDTO carDto) {
        carDto.setName(carDto.getName().toUpperCase());
    }

    public abstract CarDTO toCarDto(Car car);
}

@MappingTarget is a parameter annotation that populates the target mapping DTO right before the mapping logic is executed in case of @BeforeMapping and right after in case of @AfterMapping annotated method.

10.3. Result

The CarsMapper defined above generates the implementation:

@Generated
public class CarsMapperImpl extends CarsMapper {

    @Override
    public CarDTO toCarDto(Car car) {
        if (car == null) {
            return null;
        }

        CarDTO carDTO = new CarDTO();

        enrichDTOWithFuelType(car, carDTO);

        carDTO.setId(car.getId());
        carDTO.setName(car.getName());

        convertNameToUpperCase(carDTO);

        return carDTO;
    }
}

Notice how the annotated methods invocations surround the mapping logic in the implementation.

11. Support for Lombok

In the recent version of MapStruct, Lombok support was announced. So, we can easily map a source entity and a destination using Lombok.

To enable Lombok support, we need to add the dependency in the annotation processor path. Since Lombok version 1.18.16, we also have to add the dependency on lombok-mapstruct-binding. Now we have the mapstruct-processor as well as Lombok in the Maven compiler plugin:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.11.0</version>
    <configuration>
        <annotationProcessorPaths>
            <path>
                <groupId>org.mapstruct</groupId>
                <artifactId>mapstruct-processor</artifactId>
                <version>1.5.5.Final</version>
            </path>
            <path>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
	        <version>1.18.30</version>
            </path>
            <path>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok-mapstruct-binding</artifactId>
	        <version>0.2.0</version>
            </path>
        </annotationProcessorPaths>
    </configuration>
</plugin>

Let’s define the source entity using Lombok annotations:

@Getter
@Setter
public class Car {
    private int id;
    private String name;
}

And the destination data transfer object:

@Getter
@Setter
public class CarDTO {
    private int id;
    private String name;
}

The mapper interface for this remains similar to our previous example:

@Mapper
public interface CarMapper {
    CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
    CarDTO carToCarDTO(Car car);
}

12. Support for defaultExpression

Starting with version 1.3.0, we can use the defaultExpression attribute of the @Mapping annotation to specify an expression that determines the value of the destination field if the source field is null. This is in addition to the existing defaultValue attribute functionality.

The source entity:

public class Person {
    private int id;
    private String name;
}

The destination data transfer object:

public class PersonDTO {
    private int id;
    private String name;
}

If the id field of the source entity is null, we want to generate a random id and assign it to the destination keeping other property values as-is:

@Mapper
public interface PersonMapper {
    PersonMapper INSTANCE = Mappers.getMapper(PersonMapper.class);
    
    @Mapping(target = "id", source = "person.id", 
      defaultExpression = "java(java.util.UUID.randomUUID().toString())")
    PersonDTO personToPersonDTO(Person person);
}

Let’s add a test case to verify the expression execution:

@Test
public void givenPersonEntitytoPersonWithExpression_whenMaps_thenCorrect() 
    Person entity  = new Person();
    entity.setName("Micheal");
    PersonDTO personDto = PersonMapper.INSTANCE.personToPersonDTO(entity);
    assertNull(entity.getId());
    assertNotNull(personDto.getId());
    assertEquals(personDto.getName(), entity.getName());
}

13. Conclusion

This article provided an introduction to MapStruct. We’ve 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.

Course – LS (cat=Java)

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE
res – REST with Spring (eBook) (everywhere)
Comments are closed on this article!