Introduction
Mapstruct is a code generator for the Java programming language that greatly simplifies the process of converting between different Java classes using a builder pattern. When working with complex applications, especially those following clean architecture principles, we often need to transform objects between different layers (e.g., from entities to DTOs and vice versa). MapStruct makes this process efficient and maintainable.
The generated mapping code uses plain method invocations and thus is fast, type-safe and easy to understand.
Setup
Add the following dependencies to your pom.xml
:
<properties> <org.mapstruct.version>1.5.5.Final</org.mapstruct.version></properties>
<dependencies> <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct</artifactId> <version>${org.mapstruct.version}</version> </dependency></dependencies>
<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.1</version> <configuration> <source>17</source> <target>17</target> <annotationProcessorPaths> <path> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>${org.mapstruct.version}</version> </path> </annotationProcessorPaths> </configuration> </plugin> </plugins></build>
Basic Usage
Let’s look at a simple example. Consider we have a User entity and a UserDTO:
// User.javapublic class User { private Long id; private String firstName; private String lastName; private String email; private LocalDate birthDate; private Address address;
// getters and setters}
// Address.javapublic class Address { private String street; private String city; private String country;
// getters and setters}
// UserDTO.javapublic class UserDTO { private Long id; private String fullName; // combination of firstName and lastName private String email; private Integer age; // calculated from birthDate private String location; // combination of city and country
// getters and setters}
To create a mapper for these classes, we define a mapper interface:
@Mapper(componentModel = "spring")public interface UserMapper {
@Mapping(target = "fullName", expression = "java(user.getFirstName() + \" \" + user.getLastName())") @Mapping(target = "age", expression = "java(calculateAge(user.getBirthDate()))") @Mapping(target = "location", expression = "java(user.getAddress().getCity() + \", \" + user.getAddress().getCountry())") UserDTO userToUserDTO(User user);
default Integer calculateAge(LocalDate birthDate) { return Period.between(birthDate, LocalDate.now()).getYears(); }}
Advanced Features
Custom Method Mappings
Sometimes you need to implement custom mapping logic. MapStruct allows you to define default methods in your mapper interface:
@Mapper(componentModel = "spring")public interface ComplexMapper {
@Mapping(target = "status", source = "orderStatus") @Mapping(target = "totalAmount", expression = "java(calculateTotal(order))") OrderDTO orderToOrderDTO(Order order);
default BigDecimal calculateTotal(Order order) { return order.getItems().stream() .map(item -> item.getPrice().multiply(new BigDecimal(item.getQuantity()))) .reduce(BigDecimal.ZERO, BigDecimal::add); }}
Collection Mapping
MapStruct can automatically handle collections:
@Mapper(componentModel = "spring")public interface OrderMapper {
OrderDTO orderToOrderDTO(Order order); List<OrderDTO> ordersToOrderDTOs(List<Order> orders); Set<OrderDTO> ordersToOrderDTOs(Set<Order> orders);}
Multiple Source Objects
You can map from multiple source objects:
@Mapper(componentModel = "spring")public interface UserProfileMapper {
@Mapping(source = "user.id", target = "userId") @Mapping(source = "profile.bio", target = "biography") @Mapping(source = "settings.theme", target = "userTheme") UserProfileDTO userAndProfileToDTO(User user, Profile profile, UserSettings settings);}
Best Practices
-
Use Spring Component Model: Always use
@Mapper(componentModel = "spring")
when working with Spring applications to enable dependency injection. -
Null Value Handling: Configure null value handling at the mapper level:
@Mapper(componentModel = "spring", nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
- Documentation: Document complex mappings using
@Mapping
annotation’sdefaultValue
anddefaultExpression
:
@Mapping(target = "status", source = "orderStatus", defaultValue = "PENDING", defaultExpression = "java(OrderStatus.PENDING)")
Testing
Testing MapStruct mappings is straightforward:
@SpringBootTestclass UserMapperTest {
@Autowired private UserMapper userMapper;
@Test void shouldMapUserToUserDTO() { // Given User user = new User(); user.setFirstName("John"); user.setLastName("Doe"); user.setEmail("john@example.com"); user.setBirthDate(LocalDate.of(1990, 1, 1));
Address address = new Address(); address.setCity("New York"); address.setCountry("USA"); user.setAddress(address);
// When UserDTO userDTO = userMapper.userToUserDTO(user);
// Then assertThat(userDTO.getFullName()).isEqualTo("John Doe"); assertThat(userDTO.getEmail()).isEqualTo("john@example.com"); assertThat(userDTO.getLocation()).isEqualTo("New York, USA"); assertThat(userDTO.getAge()).isEqualTo(33); }}
Complex Object Conversion Example
Let’s look at a more complex example involving an e-commerce order system:
public class Order { private Long id; private Customer customer; private List<OrderItem> items; private PaymentDetails payment; private OrderStatus status; private LocalDateTime createdAt; private ShippingAddress shippingAddress;
// getters and setters}
public class Customer { private Long id; private String firstName; private String lastName; private String email; private CustomerType type; private List<Address> addresses;
// getters and setters}
public class OrderItem { private Product product; private Integer quantity; private BigDecimal priceAtOrder; private List<String> customizations;
// getters and setters}
public class PaymentDetails { private PaymentMethod method; private PaymentStatus status; private BigDecimal amount; private String transactionId;
// getters and setters}
public class OrderSummaryDTO { private String orderNumber; private String customerFullName; private String customerEmail; private List<OrderItemDTO> items; private BigDecimal totalAmount; private String status; private String paymentStatus; private String shippingAddress; private LocalDateTime orderDate;
// getters and setters}
public class OrderItemDTO { private String productName; private String productSku; private Integer quantity; private BigDecimal unitPrice; private BigDecimal totalPrice; private List<String> customizations;
// getters and setters}
@Mapper(componentModel = "spring", imports = {BigDecimal.class})public interface OrderMapper {
@Mapping(target = "orderNumber", expression = "java(generateOrderNumber(order))") @Mapping(target = "customerFullName", expression = "java(order.getCustomer().getFirstName() + \" \" + order.getCustomer().getLastName())") @Mapping(target = "customerEmail", source = "customer.email") @Mapping(target = "totalAmount", expression = "java(calculateTotalAmount(order))") @Mapping(target = "status", expression = "java(order.getStatus().name())") @Mapping(target = "paymentStatus", source = "payment.status") @Mapping(target = "shippingAddress", expression = "java(formatAddress(order.getShippingAddress()))") @Mapping(target = "orderDate", source = "createdAt") OrderSummaryDTO orderToOrderSummaryDTO(Order order);
@Mapping(target = "productName", source = "product.name") @Mapping(target = "productSku", source = "product.sku") @Mapping(target = "unitPrice", source = "priceAtOrder") @Mapping(target = "totalPrice", expression = "java(item.getPriceAtOrder().multiply(new BigDecimal(item.getQuantity())))") OrderItemDTO orderItemToDTO(OrderItem item);
default String generateOrderNumber(Order order) { return String.format("ORD-%d-%tY%<tm%<td", order.getId(), order.getCreatedAt()); }
default BigDecimal calculateTotalAmount(Order order) { return order.getItems().stream() .map(item -> item.getPriceAtOrder() .multiply(new BigDecimal(item.getQuantity()))) .reduce(BigDecimal.ZERO, BigDecimal::add); }
default String formatAddress(ShippingAddress address) { return String.format("%s, %s, %s, %s - %s", address.getStreet(), address.getCity(), address.getState(), address.getCountry(), address.getZipCode()); }
@AfterMapping default void handleSpecialCustomerTypes(Order order, @MappingTarget OrderSummaryDTO dto) { if (order.getCustomer().getType() == CustomerType.VIP) { dto.setCustomerFullName(dto.getCustomerFullName() + " (VIP)"); } }}
@Service@RequiredArgsConstructorpublic class OrderService { private final OrderMapper orderMapper;
public OrderSummaryDTO getOrderSummary(Long orderId) { Order order = orderRepository.findById(orderId) .orElseThrow(() -> new OrderNotFoundException(orderId));
return orderMapper.orderToOrderSummaryDTO(order); }}
This example demonstrates several advanced MapStruct features:
- Complex Object Hierarchy: Mapping nested objects (Order → Customer → Address)
- Collection Mapping: Handling lists of OrderItems
- Custom Methods: Using helper methods for complex transformations
- After Mapping: Post-processing with @AfterMapping
- Expression Mapping: Using Java expressions for computed fields
- Multiple Source Fields: Combining multiple fields into one
- Format Transformation: Converting complex objects to formatted strings
The mapper handles:
- Nested object traversal
- Collection transformation
- Custom business logic
- Formatted string output
- Calculated fields
- Special case handling
This example shows how MapStruct can handle complex real-world scenarios while keeping the code clean and maintainable.
Security Considerations and Known Vulnerabilities
As of February 2024, MapStruct has maintained a strong security track record. However, it’s important to note:
MapStruct Core (1.5.5.Final)
- Currently no known critical vulnerabilities
- The library itself performs compile-time code generation, minimizing runtime security risks
- No direct exposure to user input or network operations
Related Dependencies
When using MapStruct with Spring Boot, be aware of these dependencies:
-
maven-compiler-plugin (3.8.1)
- No known critical vulnerabilities
- Recommended to use version 3.11.0 or later for improved security
-
Spring Framework Integration
- MapStruct’s Spring integration is passive and inherits Spring’s security context
- Follow Spring Security best practices when exposing mapped objects through APIs
Best Security Practices
- Input Validation: Always validate input before mapping
@Mapper(componentModel = "spring")public interface UserMapper { default UserDTO toDto(User user) { if (user == null) { throw new IllegalArgumentException("User cannot be null"); } // perform mapping }}
- Sensitive Data: Be careful when mapping sensitive information
@Mapper(componentModel = "spring")public interface UserMapper { @Mapping(target = "password", ignore = true) @Mapping(target = "securityQuestions", ignore = true) UserDTO userToUserDTO(User user);}
- Regular Updates: Keep dependencies updated to receive security patches
Conclusion
MapStruct is a powerful tool that simplifies object mapping in Java applications. It provides:
- Type-safe mapping
- Compile-time error checking
- High performance
- Clean and maintainable code
- Excellent integration with Spring Framework
By using MapStruct, you can focus on your business logic while letting the library handle the tedious task of object transformation. The generated code is easy to debug and performs better than reflection-based mapping frameworks.
Alternative Libraries
While MapStruct is an excellent choice for object mapping, there are several alternatives worth considering:
ModelMapper
- Pros:
- Runtime mapping using reflection
- No code generation required
- Flexible mapping configurations
- Intuitive API
- Cons:
- Lower performance due to reflection
- No compile-time type safety
- Higher memory usage
ModelMapper modelMapper = new ModelMapper();UserDTO userDTO = modelMapper.map(user, UserDTO.class);
JMapper
- Pros:
- High performance
- XML or annotation configuration
- Supports complex mappings
- Cons:
- Less active community
- Limited documentation
- Steeper learning curve
Dozer
- Pros:
- Rich feature set
- XML or API configuration
- Good for legacy systems
- Cons:
- Slower performance
- Heavy memory footprint
- XML configuration can be verbose
Note: The ratings are relative comparisons based on available documentation, community feedback, and published benchmarks. Your specific use case may yield different results.
When to Choose Each
-
Choose MapStruct when:
- Performance is critical
- Compile-time type safety is required
- Working with Spring Boot applications
- Need clean, maintainable generated code
-
Choose ModelMapper when:
- Quick prototyping is needed
- Runtime mapping configuration is required
- Type safety is less critical
- Simple setup is preferred
-
Choose JMapper when:
- High performance is needed
- XML configuration is preferred
- Working with legacy systems
-
Choose Dozer when:
- Complex legacy system integration
- Extensive mapping configurations needed
- Performance is not critical
Each library has its strengths, but MapStruct’s combination of performance, type safety, and excellent Spring integration makes it the preferred choice for modern Java applications.