Master Object Mapping in Java with MapStruct: From Basics to Advanced Techniques
This article introduces MapStruct, a powerful Java annotation‑based object‑mapping library, compares it with BeanUtils, shows how to integrate it into a Spring Boot project, and provides step‑by‑step examples for basic, collection, nested, composite, and advanced mappings including dependency injection, constants, custom processing, and exception handling.
When developing projects, we often need to convert between PO, VO, and DTO objects. Simple conversions can be handled with BeanUtils, but complex mappings require many getter and setter methods. This article recommends MapStruct, a powerful automatic object‑mapping tool.
About BeanUtils
I often use Hutool's BeanUtil for object conversion, but it has several drawbacks:
Property mapping relies on reflection, resulting in low performance.
Attributes with different names or types cannot be converted automatically, requiring manual getter/setter methods.
Nested objects also need manual handling.
Collection conversion requires explicit loops to copy each element.
MapStruct addresses all these shortcomings.
MapStruct Overview
MapStruct is a Java annotation‑based object‑property mapping tool with over 4.5K stars on GitHub. By defining mapping rules in an interface, MapStruct generates implementation classes at compile time, avoiding reflection and delivering excellent performance for complex mappings.
IDEA Plugin Support
As a popular mapping tool, MapStruct provides a dedicated IDEA plugin that can be installed before use.
Project Integration
Integrating MapStruct into a Spring Boot project is straightforward—just add the following two dependencies (using version 1.4.2.Final):
<code><dependency>
<!-- MapStruct dependencies -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
<scope>compile</scope>
</dependency>
</dependencies></code>Basic Usage
After integrating MapStruct, let's explore its capabilities.
Basic Mapping
We start with a quick introduction to MapStruct's core features and implementation principle.
First, define the member PO class
Member:
<code>/**
* Shopping member
* Created by macro on 2021/10/12.
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class Member {
private Long id;
private String username;
private String password;
private String nickname;
private Date birthday;
private String phone;
private String icon;
private Integer gender;
}</code>Then define the member DTO class
MemberDto(note the different field names and types):
<code>/**
* Shopping member DTO
* Created by macro on 2021/10/12.
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class MemberDto {
private Long id;
private String username;
private String password;
private String nickname;
// Different type
private String birthday;
// Different name
private String phoneNumber;
private String icon;
private Integer gender;
}</code>Create a mapper interface
MemberMapperto map same‑name properties, different‑name properties, and different‑type properties:
<code>@Mapper
public interface MemberMapper {
MemberMapper INSTANCE = Mappers.getMapper(MemberMapper.class);
@Mapping(source = "phone", target = "phoneNumber")
@Mapping(source = "birthday", target = "birthday", dateFormat = "yyyy-MM-dd")
MemberDto toDto(Member member);
}</code>Use the mapper in a controller to test the conversion:
<code>@RestController
@Api(tags = "MapStructController", description = "MapStruct object conversion test")
@RequestMapping("/mapStruct")
public class MapStructController {
@ApiOperation(value = "Basic Mapping")
@GetMapping("/baseMapping")
public CommonResult baseTest() {
List<Member> memberList = LocalJsonUtil.getListFromJson("json/members.json", Member.class);
MemberDto memberDto = MemberMapper.INSTANCE.toDto(memberList.get(0));
return CommonResult.success(memberDto);
}
}
</code>Running the project and testing the endpoint in Swagger shows that all PO properties have been successfully converted to the DTO.
MapStruct generates the implementation class based on the
@Mapperand
@Mappingannotations. You can inspect the generated code in the
targetdirectory.
The generated mapper implementation eliminates the need for manual getter/setter code.
Collection Mapping
MapStruct also supports collection mapping, allowing a list of PO objects to be converted to a list of DTOs automatically.
Add a
toDtoListmethod to
MemberMapper:
<code>@Mapper
public interface MemberMapper {
MemberMapper INSTANCE = Mappers.getMapper(MemberMapper.class);
@Mapping(source = "phone", target = "phoneNumber")
@Mapping(source = "birthday", target = "birthday", dateFormat = "yyyy-MM-dd")
List<MemberDto> toDtoList(List<Member> list);
}
</code>Test the collection mapping in the controller:
<code>@RestController
@Api(tags = "MapStructController", description = "MapStruct object conversion test")
@RequestMapping("/mapStruct")
public class MapStructController {
@ApiOperation(value = "Collection Mapping")
@GetMapping("/collectionMapping")
public CommonResult collectionMapping() {
List<Member> memberList = LocalJsonUtil.getListFromJson("json/members.json", Member.class);
List<MemberDto> memberDtoList = MemberMapper.INSTANCE.toDtoList(memberList);
return CommonResult.success(memberDtoList);
}
}
</code>Swagger confirms that the PO list has been transformed into a DTO list.
Nested Object Mapping
MapStruct can also map nested objects.
Define an
OrderPO that contains a
Memberand a list of
Productobjects:
<code>/**
* Order
* Created by macro on 2021/10/12.
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class Order {
private Long id;
private String orderSn;
private Date createTime;
private String receiverAddress;
private Member member;
private List<Product> productList;
}
</code>Create the corresponding
OrderDtowith nested DTOs:
<code>/**
* Order DTO
* Created by macro on 2021/10/12.
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class OrderDto {
private Long id;
private String orderSn;
private Date createTime;
private String receiverAddress;
// Nested DTOs
private MemberDto memberDto;
private List<ProductDto> productDtoList;
}
</code>Define
OrderMapperand reuse existing mappers for nested objects:
<code>@Mapper(uses = {MemberMapper.class, ProductMapper.class})
public interface OrderMapper {
OrderMapper INSTANCE = Mappers.getMapper(OrderMapper.class);
@Mapping(source = "member", target = "memberDto")
@Mapping(source = "productList", target = "productDtoList")
OrderDto toDto(Order order);
}
</code>Test nested mapping in the controller:
<code>@RestController
@Api(tags = "MapStructController", description = "MapStruct object conversion test")
@RequestMapping("/mapStruct")
public class MapStructController {
@ApiOperation(value = "Nested Mapping")
@GetMapping("/subMapping")
public CommonResult subMapping() {
List<Order> orderList = getOrderList();
OrderDto orderDto = OrderMapper.INSTANCE.toDto(orderList.get(0));
return CommonResult.success(orderDto);
}
}
</code>Swagger shows that nested objects have been correctly mapped.
Composite Mapping
MapStruct can merge properties from multiple source objects into a single target.
Define a composite DTO
MemberOrderDtothat extends
MemberDtoand adds order fields:
<code>/**
* Member‑order composite DTO
* Created by macro on 2021/10/21.
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class MemberOrderDto extends MemberDto {
private String orderSn;
private String receiverAddress;
}
</code>Add a method to
MemberMapperthat maps both
Memberand
Orderto
MemberOrderDtousing qualified source names:
<code>@Mapper
public interface MemberMapper {
MemberMapper INSTANCE = Mappers.getMapper(MemberMapper.class);
@Mapping(source = "member.phone", target = "phoneNumber")
@Mapping(source = "member.birthday", target = "birthday", dateFormat = "yyyy-MM-dd")
@Mapping(source = "member.id", target = "id")
@Mapping(source = "order.orderSn", target = "orderSn")
@Mapping(source = "order.receiverAddress", target = "receiverAddress")
MemberOrderDto toMemberOrderDto(Member member, Order order);
}
</code>Test the composite mapping:
<code>@RestController
@Api(tags = "MapStructController", description = "MapStruct object conversion test")
@RequestMapping("/mapStruct")
public class MapStructController {
@ApiOperation(value = "Composite Mapping")
@GetMapping("/compositeMapping")
public CommonResult compositeMapping() {
List<Order> orderList = LocalJsonUtil.getListFromJson("json/orders.json", Order.class);
List<Member> memberList = LocalJsonUtil.getListFromJson("json/members.json", Member.class);
Member member = memberList.get(0);
Order order = orderList.get(0);
MemberOrderDto dto = MemberMapper.INSTANCE.toMemberOrderDto(member, order);
return CommonResult.success(dto);
}
}
</code>Swagger confirms that fields from both
Memberand
Orderare present in the composite DTO.
Advanced Usage
Having mastered the basics, we now explore some advanced MapStruct features.
Dependency Injection
Instead of using the static INSTANCE , we can let Spring inject the mapper by setting componentModel = "spring" on the @Mapper annotation.
<code>@Mapper(componentModel = "spring")
public interface MemberSpringMapper {
@Mapping(source = "phone", target = "phoneNumber")
@Mapping(source = "birthday", target = "birthday", dateFormat = "yyyy-MM-dd")
MemberDto toDto(Member member);
}
</code> <code>@RestController
@Api(tags = "MapStructController", description = "MapStruct object conversion test")
@RequestMapping("/mapStruct")
public class MapStructController {
@Autowired
private MemberSpringMapper memberSpringMapper;
@ApiOperation(value = "DI Mapping")
@GetMapping("/springMapping")
public CommonResult springMapping() {
List<Member> memberList = LocalJsonUtil.getListFromJson("json/members.json", Member.class);
MemberDto dto = memberSpringMapper.toDto(memberList.get(0));
return CommonResult.success(dto);
}
}
</code>Constants, Default Values, and Expressions
MapStruct allows setting constant values, default values, or Java expressions for target properties.
Define
Productand
ProductDtowhere
idis a constant,
counthas a default of 1, and
productSnis generated via UUID.
<code>@Mapper(imports = {UUID.class})
public interface ProductMapper {
ProductMapper INSTANCE = Mappers.getMapper(ProductMapper.class);
@Mapping(target = "id", constant = "-1L")
@Mapping(source = "count", target = "count", defaultValue = "1")
@Mapping(target = "productSn", expression = "java(UUID.randomUUID().toString())")
ProductDto toDto(Product product);
}
</code> <code>@RestController
@Api(tags = "MapStructController", description = "MapStruct object conversion test")
@RequestMapping("/mapStruct")
public class MapStructController {
@ApiOperation(value = "Constants & Defaults")
@GetMapping("/defaultMapping")
public CommonResult defaultMapping() {
List<Product> productList = LocalJsonUtil.getListFromJson("json/products.json", Product.class);
Product product = productList.get(0);
product.setId(100L);
product.setCount(null);
ProductDto dto = ProductMapper.INSTANCE.toDto(product);
return CommonResult.success(dto);
}
}
</code>Custom Processing Before and After Mapping
MapStruct supports @BeforeMapping and @AfterMapping methods similar to AOP.
<code>@Mapper(imports = {UUID.class})
public abstract class ProductRoundMapper {
public static final ProductRoundMapper INSTANCE = Mappers.getMapper(ProductRoundMapper.class);
@Mapping(target = "id", constant = "-1L")
@Mapping(source = "count", target = "count", defaultValue = "1")
@Mapping(target = "productSn", expression = "java(UUID.randomUUID().toString())")
public abstract ProductDto toDto(Product product);
@BeforeMapping
public void beforeMapping(Product product) {
// If price < 0, set to 0
if (product.getPrice().compareTo(BigDecimal.ZERO) < 0) {
product.setPrice(BigDecimal.ZERO);
}
}
@AfterMapping
public void afterMapping(@MappingTarget ProductDto productDto) {
// Set current time as createTime
productDto.setCreateTime(new Date());
}
}
</code> <code>@RestController
@Api(tags = "MapStructController", description = "MapStruct object conversion test")
@RequestMapping("/mapStruct")
public class MapStructController {
@ApiOperation(value = "Custom Pre/Post Processing")
@GetMapping("/customRoundMapping")
public CommonResult customRoundMapping() {
List<Product> productList = LocalJsonUtil.getListFromJson("json/products.json", Product.class);
Product product = productList.get(0);
product.setPrice(new BigDecimal(-1));
ProductDto dto = ProductRoundMapper.INSTANCE.toDto(product);
return CommonResult.success(dto);
}
}
</code>Handling Mapping Exceptions
MapStruct can propagate exceptions thrown during mapping.
Create a custom exception
ProductValidatorExceptionand a validator that throws it when price is negative.
<code>public class ProductValidatorException extends Exception {
public ProductValidatorException(String message) {
super(message);
}
}
</code> <code>public class ProductValidator {
public BigDecimal validatePrice(BigDecimal price) throws ProductValidatorException {
if (price.compareTo(BigDecimal.ZERO) < 0) {
throw new ProductValidatorException("Price cannot be less than 0!");
}
return price;
}
}
</code>Use the validator in a mapper and declare the exception:
<code>@Mapper(uses = {ProductValidator.class}, imports = {UUID.class})
public interface ProductExceptionMapper {
ProductExceptionMapper INSTANCE = Mappers.getMapper(ProductExceptionMapper.class);
@Mapping(target = "id", constant = "-1L")
@Mapping(source = "count", target = "count", defaultValue = "1")
@Mapping(target = "productSn", expression = "java(UUID.randomUUID().toString())")
ProductDto toDto(Product product) throws ProductValidatorException;
}
</code> <code>@RestController
@Api(tags = "MapStructController", description = "MapStruct object conversion test")
@RequestMapping("/mapStruct")
public class MapStructController {
@ApiOperation(value = "Exception Handling")
@GetMapping("/exceptionMapping")
public CommonResult exceptionMapping() {
List<Product> productList = LocalJsonUtil.getListFromJson("json/products.json", Product.class);
Product product = productList.get(0);
product.setPrice(new BigDecimal(-1));
ProductDto dto = null;
try {
dto = ProductExceptionMapper.INSTANCE.toDto(product);
} catch (ProductValidatorException e) {
e.printStackTrace();
}
return CommonResult.success(dto);
}
}
</code>The log shows the custom validation exception when the price is negative.
Conclusion
MapStruct is far more powerful than BeanUtils. For complex object mappings, it eliminates the need to write repetitive getter and setter code. The features demonstrated here are only a subset of its capabilities; interested readers should explore the official documentation for more advanced usage.
Reference
Official documentation: https://mapstruct.org/documentation/stable/reference/html
Source Code
GitHub repository: https://github.com/macrozheng/mall-learning/tree/master/mall-tiny-mapstruct
macrozheng
Dedicated to Java tech sharing and dissecting top open-source projects. Topics include Spring Boot, Spring Cloud, Docker, Kubernetes and more. Author’s GitHub project “mall” has 50K+ stars.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.