(viejo confiable).
Introduction
MapStruct is a code generator that greatly simplifies the implementation of mappings between Java bean types, following a convention-over-configuration approach.
It uses annotations to generate mapping code at compile time, ensuring high performance and reducing boilerplate code.
There are several Java DTO mapping libraries, such as MapStruct, Dozer, and ModelMapper. MapStruct stands out by generating mapping implementations during compilation rather than relying on runtime reflection.
The core solution involves creating an interface annotated with @Mapper. During compilation, the MapStruct annotation processor generates the mapping implementation. With Maven, this can be done by running mvn clean install.
This approach reduces repetitive code and makes object mapping easier to maintain.
Basic Usage of MapStruct
To use MapStruct, you need to define a mapper interface and annotate it with @Mapper.
Here is a simple example:
@Mapper
public interface SimpleSourceDestinationMapper {
SimpleSourceDestinationMapper INSTANCE =
Mappers.getMapper(SimpleSourceDestinationMapper.class);
SimpleDestination sourceToDestination(SimpleSource source);
SimpleSource destinationToSource(SimpleDestination destination);
}
The interface defines two mapping methods: one converts a SimpleSource object into a SimpleDestination object, while the other performs the reverse mapping.
The Mappers.getMapper() method provides an instance of the generated implementation. MapStruct generates the implementation automatically during compilation.
Nested Object Mapping
MapStruct can also map objects containing nested objects. The @Mapping annotation allows you to specify the source and target properties explicitly.
Consider an employee entity containing an identifier, a name, and a division. The corresponding DTO may use different property names and contain a nested division DTO.
@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);
DivisionDTO divisionToDivisionDTO(Division entity);
Division divisionDTOToDivision(DivisionDTO dto);
}
In this example, the source and target property names are different. The @Mapping annotations explicitly define how these properties correspond to each other.
The mapper also declares methods for converting Division objects into DivisionDTO objects and vice versa.
MapStruct can reuse mapping methods to handle nested objects, allowing complex object structures to be mapped through a combination of simple mappings.
List POJO Mapping
Mapping collections sometimes requires additional logic, particularly when the destination contains a list of objects that must be constructed from a source object.
In the following example, a mapper uses an expression to call a custom default method that creates a list of inner objects.
@Mapper
public interface ListPojoMapper {
@Mapping(
target = "innerPojoList",
expression = "java(mapInnerPojoList(firstPojo))"
)
SecondPojo mapFirstPojoToSecondPojo(FirstPojo firstPojo);
default List<InnerPojo> mapInnerPojoList(FirstPojo firstPojo) {
System.out.println(
"mapInnerPojoList..... default interface method!"
);
List<InnerPojo> innerPojos =
java.util.Arrays.asList(new InnerPojo());
for (InnerPojo innerPojo : innerPojos) {
innerPojo.setInnerString(
firstPojo.getStringAttribute()
);
}
List<InnerPojo> result =
new ArrayList<InnerPojo>(innerPojos);
return result;
}
}
The mapInnerPojoList() method demonstrates how a custom mapping method can be implemented directly in a mapper interface using the default keyword.
The method creates an InnerPojo object, assigns a string attribute from the source object, and returns a list containing the resulting object.
The expression attribute in @Mapping allows MapStruct to invoke this custom method when generating the mapping implementation.
This technique is useful when a mapping cannot be expressed as a straightforward property-to-property assignment.
Composite Inner POJOs
MapStruct can handle complex mappings, including composite POJOs containing inner POJOs.
Consider an EmployeeDTO class with the following properties:
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class EmployeeDTO {
private int employeeId;
private String employeeName;
private DivisionDTO division;
}
The nested DivisionDTO class contains its own properties:
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class DivisionDTO {
private int id;
private String name;
}
The mapper can define the conversion methods for both the employee and division objects:
@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);
DivisionDTO divisionToDivisionDTO(Division entity);
Division divisionDTOToDivision(DivisionDTO dto);
}
This example illustrates how MapStruct can map properties with different names and work with nested object structures.
By defining the appropriate mapping methods, developers can separate the mapping logic from the business logic and avoid manually copying properties between objects.
Generated Code
One of the main advantages of MapStruct is that it generates ordinary Java code during compilation.
For example, the generated implementation of SimpleSourceDestinationMapper can look like this:
import javax.annotation.processing.Generated;
@Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2025-02-18T15:58:04-0300",
comments = "version: 1.6.3, compiler: javac"
)
public class SimpleSourceDestinationMapperImpl
implements SimpleSourceDestinationMapper {
@Override
public SimpleDestination sourceToDestination(
SimpleSource source) {
if (source == null) {
return null;
}
SimpleDestination.SimpleDestinationBuilder
simpleDestination = SimpleDestination.builder();
simpleDestination.name(source.getName());
simpleDestination.description(source.getDescription());
return simpleDestination.build();
}
@Override
public SimpleSource destinationToSource(
SimpleDestination destination) {
if (destination == null) {
return null;
}
SimpleSource.SimpleSourceBuilder simpleSource =
SimpleSource.builder();
simpleSource.name(destination.getName());
simpleSource.description(destination.getDescription());
return simpleSource.build();
}
}
The generated implementation checks for null values, creates the destination object using its builder, copies the properties, and returns the resulting instance.
Notice that the generated code contains direct method calls such as getName() and getDescription(). This is one of the reasons MapStruct can provide high-performance mappings without relying on runtime reflection for these operations.
The generated classes are produced by the annotation processor during compilation. Developers normally work with the mapper interface rather than manually maintaining the generated implementation.
Drawbacks and Limitations
MapStruct works well when mapping large numbers of objects with similar structures and many attributes. It can significantly reduce repetitive code when converting between entities, DTOs, and other Java bean types.
However, mapping becomes more complex when the source and destination objects have significantly different structures.
Nested classes and composite objects may require additional mapping methods or explicit configuration to determine how properties should be transferred between outer and inner objects.
Mapping can involve Java beans, primitive values, nested classes, and collections. Collections may require particular attention when the destination structure differs from the source structure or when each element needs custom transformation logic.
For these situations, custom methods and explicit @Mapping annotations can help make the intended conversion clear.
Conclusion
MapStruct is a useful tool for Java applications that need to convert objects between different representations. By generating mapping implementations at compile time, it reduces boilerplate code while maintaining readable and efficient Java implementations.
Its annotation-based approach supports straightforward property mapping, nested objects, collections, and custom conversion logic. For complex object structures, developers may need to define explicit mappings and custom methods.
Overall, MapStruct is a practical option for simplifying DTO mapping and keeping conversion logic separate from the rest of the application.
No comments :
Post a Comment