Introduction to MapStruct
MapStruct is a Java annotation processor that generates type-safe and efficient mappers for object mappings. It is widely used for mapping data between different Java beans, typically in the context of transferring data between layers in an application. MapStruct generates mapping code at compile time, ensuring high performance and type safety.
How MapStruct Works
MapStruct generates implementation code for mapping interfaces at compile time. You define mapping methods in an interface, and MapStruct generates the implementation for you. This approach avoids the runtime overhead of reflection and makes the mapping code type safe and fast.
Here are five key points on how MapStruct works:
-
Annotation Processor:
- MapStruct uses an annotation processor to generate mapping code at compile time. This means that the mapping logic is generated as Java source code, ensuring high performance and type safety.
- The core annotation used is
@Mapper
, which is applied to an interface or abstract class that defines the mapping methods.
-
Automatic Mapping:
- MapStruct automatically maps properties with the same name and type between source and target objects. This reduces the need for manual mapping logic and makes it easy to use.
- For example, if you have two classes
PersonDTO
andPerson
with propertiesfirstName
,lastName
, andage
, MapStruct will automatically map these properties if they have the same names and types.
-
Custom Mappings and Conversions:
- MapStruct allows you to define custom mappings using the
@Mapping
annotation. You can specify the source and target properties explicitly if they have different names. - It also supports custom-type conversions using methods annotated with
@Named
. You can define methods to handle specific type conversions and reference them in your mapping methods.
- MapStruct allows you to define custom mappings using the
-
Nested Mappings:
- MapStruct supports nested mappings, which means you can map nested objects and their properties. This is particularly useful for complex object graphs.
- For example, you can map nested objects like
Address
inside anEmployee
object by specifying the nested property paths in the@Mapping
annotation.
-
Mapping Collections:
- MapStruct can handle mapping collections such as lists and sets. It can map collections of objects from the source to the target, provided the individual elements can be mapped.
- This is useful when dealing with relationships like one-to-many or many-to-many between objects in the source and target classes.
Installation
Adding MapStruct to Your Project
To use MapStruct, add the following dependencies to your pom.xml
if you're using Maven:
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.5.2.Final</version> <!-- or the latest version -->
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.5.2.Final</version>
<scope>provided</scope>
</dependency>
For Gradle:
implementation 'org.mapstruct:mapstruct:1.5.2.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.2.Final'
Basic Usage: Simple Bean Mapping
Let's start with a simple example of mapping between two Java beans, PersonDTO
and Person
.
Defining the Beans
public class PersonDTO {
private String firstName;
private String lastName;
private int age;
// Getters and Setters
}
public class Person {
private String givenName;
private String familyName;
private int age;
// Getters and Setters
}
Creating the Mapper
Create a mapper interface with the @Mapper
annotation.
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
@Mapper
public interface PersonMapper {
PersonMapper INSTANCE = Mappers.getMapper(PersonMapper.class);
@Mapping(source = "firstName", target = "givenName")
@Mapping(source = "lastName", target = "familyName")
Person toPerson(PersonDTO personDTO);
@Mapping(source = "givenName", target = "firstName")
@Mapping(source = "familyName", target = "lastName")
PersonDTO toPersonDTO(Person person);
}
Using the Mapper
public class MapStructExample {
public static void main(String[] args) {
PersonMapper mapper = PersonMapper.INSTANCE;
PersonDTO personDTO = new PersonDTO();
personDTO.setFirstName("Amit");
personDTO.setLastName("Sharma");
personDTO.setAge(30);
Person person = mapper.toPerson(personDTO);
System.out.println("Person: " + person.getGivenName() + " " + person.getFamilyName() + ", Age: " + person.getAge());
PersonDTO mappedPersonDTO = mapper.toPersonDTO(person);
System.out.println("PersonDTO: " + mappedPersonDTO.getFirstName() + " " + mappedPersonDTO.getLastName() + ", Age: " + mappedPersonDTO.getAge());
}
}
Explanation: This example creates a mapper interface, PersonMapper
, to map between PersonDTO
and Person
. The @Mapping
annotations specify the source and target fields for the mapping. The generated mapper is used to convert between the two types.
Output
Person: Amit Sharma, Age: 30
PersonDTO: Amit Sharma, Age: 30
Custom Mappings
You can define custom mappings using methods in the mapper interface.
Defining the Beans
public class EmployeeDTO {
private String empName;
private String empId;
// Getters and Setters
}
public class Employee {
private String name;
private String id;
// Getters and Setters
}
Creating the Mapper
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
@Mapper
public interface EmployeeMapper {
EmployeeMapper INSTANCE = Mappers.getMapper(EmployeeMapper.class);
@Mapping(source = "empName", target = "name")
@Mapping(source = "empId", target = "id")
Employee toEmployee(EmployeeDTO employeeDTO);
@Mapping(source = "name", target = "empName")
@Mapping(source = "id", target = "empId")
EmployeeDTO toEmployeeDTO(Employee employee);
}
Using the Mapper
public class CustomMappingExample {
public static void main(String[] args) {
EmployeeMapper mapper = EmployeeMapper.INSTANCE;
EmployeeDTO employeeDTO = new EmployeeDTO();
employeeDTO.setEmpName("Rohit");
employeeDTO.setEmpId("E001");
Employee employee = mapper.toEmployee(employeeDTO);
System.out.println("Employee: " + employee.getName() + ", ID: " + employee.getId());
EmployeeDTO mappedEmployeeDTO = mapper.toEmployeeDTO(employee);
System.out.println("EmployeeDTO: " + mappedEmployeeDTO.getEmpName() + ", ID: " + mappedEmployeeDTO.getEmpId());
}
}
Explanation: This example demonstrates how to define custom mappings between EmployeeDTO
and Employee
using the @Mapping
annotation.
Output
Employee: Rohit, ID: E001
EmployeeDTO: Rohit, ID: E001
Nested Mappings
MapStruct supports nested mappings, allowing you to map nested objects.
Defining the Beans
public class AddressDTO {
private String street;
private String city;
// Getters and Setters
}
public class EmployeeDTO {
private String empName;
private String empId;
private AddressDTO address;
// Getters and Setters
}
public class Address {
private String streetName;
private String cityName;
// Getters and Setters
}
public class Employee {
private String name;
private String id;
private Address address;
// Getters and Setters
}
Creating the Mapper
@Mapper
public interface EmployeeMapper {
EmployeeMapper INSTANCE = Mappers.getMapper(EmployeeMapper.class);
@Mapping(source = "empName", target = "name")
@Mapping(source = "empId", target = "id")
@Mapping(source = "address.street", target = "address.streetName")
@Mapping(source = "address.city", target = "address.cityName")
Employee toEmployee(EmployeeDTO employeeDTO);
@Mapping(source = "name", target = "empName")
@Mapping(source = "id", target = "empId")
@Mapping(source = "address.streetName", target = "address.street")
@Mapping(source = "address.cityName", target = "address.city")
EmployeeDTO toEmployeeDTO(Employee employee);
}
Using the Mapper
public class NestedMappingExample {
public static void main(String[] args) {
EmployeeMapper mapper = EmployeeMapper.INSTANCE;
AddressDTO addressDTO = new AddressDTO();
addressDTO.setStreet("MG Road");
addressDTO.setCity("Bangalore");
EmployeeDTO employeeDTO = new EmployeeDTO();
employeeDTO.setEmpName("Vikas");
employeeDTO.setEmpId("E123");
employeeDTO.setAddress(addressDTO);
Employee employee = mapper.toEmployee(employeeDTO);
System.out.println("Employee: " + employee.getName() + ", ID: " + employee.getId() +
", Address: " + employee.getAddress().getStreetName() + ", " + employee.getAddress().getCityName());
EmployeeDTO mappedEmployeeDTO = mapper.toEmployeeDTO(employee);
System.out.println("EmployeeDTO: " + mappedEmployeeDTO.getEmpName() + ", ID: " + mappedEmployeeDTO.getEmpId() +
", Address: " + mappedEmployeeDTO.getAddress().getStreet() + ", " + mappedEmployeeDTO.getAddress().getCity());
}
}
Explanation: This example demonstrates how to map nested objects between EmployeeDTO
and Employee
using MapStruct.
Output
Employee: Vikas, ID: E123, Address: MG Road, Bangalore
EmployeeDTO: Vikas, ID: E123, Address: MG Road, Bangalore
Custom Type Conversions
You can define custom-type conversions using methods in the mapper interface.
Defining the Beans
public class ProductDTO {
private String name;
private String price;
// Getters and Setters
}
public class Product {
private String name;
private double price;
// Getters and Setters
}
Creating the Mapper
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Named;
import org.mapstruct.factory.Mappers;
@Mapper
public interface ProductMapper {
ProductMapper INSTANCE = Mappers.getMapper(ProductMapper.class);
@Mapping(source = "price", target = "price", qualifiedByName = "priceToDouble")
Product toProduct(ProductDTO productDTO);
@Mapping(source = "price", target = "price", qualifiedByName = "doubleToPrice")
ProductDTO toProductDTO(Product product);
@Named("priceToDouble")
static double priceToDouble(String price) {
return Double.parseDouble(price.replace("₹", "").trim());
}
@Named("doubleToPrice")
static String doubleToPrice(double price) {
return "₹ " + String.format("%.2f", price);
}
}
Using the Mapper
public class CustomConversionExample {
public static void main(String[] args) {
ProductMapper mapper = ProductMapper.INSTANCE;
ProductDTO productDTO = new ProductDTO();
productDTO.setName("Laptop");
productDTO.setPrice("₹ 50000");
Product product = mapper.toProduct(productDTO);
System.out.println("Product: " + product.getName() + ", Price: " + product.getPrice());
ProductDTO mappedProductDTO = mapper.toProductDTO(product);
System.out.println("ProductDTO: " + mappedProductDTO.getName() + ", Price: " + mappedProductDTO.getPrice());
}
}
Explanation: This example demonstrates how to define custom-type conversions using the @Named
annotation for methods within the mapper interface.
Output
Product: Laptop, Price: 50000.0
ProductDTO: Laptop, Price: ₹ 50000.00
Using MapStruct in Spring Boot
MapStruct can be seamlessly integrated with Spring Boot applications. Here's how you can use MapStruct in a Spring Boot project.
Adding MapStruct and Spring Boot Dependencies
Add the following dependencies to your pom.xml
:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.5.2.Final</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.5.2.Final</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
Defining the Beans
public class UserDTO {
private String firstName;
private String lastName;
private String email;
// Getters and Setters
}
public class User {
private String givenName;
private String familyName;
private String emailAddress;
// Getters and Setters
}
Creating the Mapper
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
import org.springframework.stereotype.Component;
@Mapper(componentModel = "spring")
@Component
public interface UserMapper {
UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);
@Mapping(source = "firstName", target = "givenName")
@Mapping(source = "lastName", target = "familyName")
@Mapping(source = "email", target = "emailAddress")
User toUser(UserDTO userDTO);
@Mapping(source = "givenName", target = "firstName")
@Mapping(source = "familyName", target = "lastName")
@Mapping(source = "emailAddress", target = "email")
UserDTO toUserDTO(User user);
}
Creating the Spring Boot Application
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MapStructSpringBootApplication {
public static void main(String[] args) {
SpringApplication.run(MapStructSpringBootApplication.class, args);
}
}
Creating a REST Controller
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/users")
public class UserController {
@Autowired
private UserMapper userMapper;
@PostMapping
public UserDTO createUser(@RequestBody UserDTO userDTO) {
User user = userMapper.toUser(userDTO);
// Simulate saving the user to the database
return userMapper.toUserDTO(user);
}
}
Using the REST API
You can test the REST API using a tool like Postman. Make a POST request to http://localhost:8080/users
with the following JSON body:
{
"firstName": "Amit",
"lastName": "Sharma",
"email": "amit.sharma@example.com"
}
Output
The response will be:
{
"firstName": "Amit",
"lastName": "Sharma",
"email": "amit.sharma@example.com"
}
Explanation: This example demonstrates how to integrate MapStruct with a Spring Boot application. The UserMapper
interface is annotated with @Mapper(componentModel = "spring")
to enable Spring dependency injection. The UserController
uses the UserMapper
to map between UserDTO
and User
objects in the REST API.
Conclusion
MapStruct is a powerful and efficient library for mapping Java beans. This guide covered the basics of setting up MapStruct, performing simple and nested mappings, custom type conversions, and complex nested examples. Additionally, it showed how to integrate MapStruct with a Spring Boot application.
By leveraging MapStruct, you can simplify and enhance your data transfer logic in Java applications. For more detailed information and advanced features, refer to the official MapStruct documentation.
Comments
Post a Comment
Leave Comment