Backend Development 19 min read

Mastering MapStruct: Boost Java Object Mapping Beyond BeanUtils

MapStruct is a powerful Java annotation‑based mapper that outperforms BeanUtils by generating compile‑time mapping implementations without reflection, supporting simple, nested, collection, and custom mappings, dependency injection, constants, expressions, and error handling, making object‑DTO conversions efficient and maintainable in Spring Boot projects.

macrozheng
macrozheng
macrozheng
Mastering MapStruct: Boost Java Object Mapping Beyond BeanUtils

About BeanUtils

When converting PO, VO, DTO objects, BeanUtils works for simple cases but has several drawbacks:

Uses reflection, resulting in low performance.

Cannot map properties with different names or types without writing getters/setters.

Nested objects require manual handling.

Collection conversion needs explicit loops.

MapStruct addresses all these issues.

MapStruct Overview

MapStruct is a Java annotation‑based object‑mapping tool with over 6.9K stars on GitHub. By defining mapping rules in an interface, it generates implementation classes at compile time, avoiding reflection and delivering excellent performance for complex mappings.

IDEA Plugin Support

MapStruct provides a dedicated IDEA plugin; install it before using the tool.

Project Integration

Add the following Maven dependencies (version 1.4.2.Final) to integrate MapStruct into a Spring Boot project:

<code>&lt;dependency&gt;
    &lt;!-- MapStruct dependencies --&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;org.mapstruct&lt;/groupId&gt;
        &lt;artifactId&gt;mapstruct&lt;/artifactId&gt;
        &lt;version&gt;${mapstruct.version}&lt;/version&gt;
    &lt;/dependency&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;org.mapstruct&lt;/groupId&gt;
        &lt;artifactId&gt;mapstruct-processor&lt;/artifactId&gt;
        &lt;version&gt;${mapstruct.version}&lt;/version&gt;
        &lt;scope&gt;compile&lt;/scope&gt;
    &lt;/dependency&gt;
&lt;/dependencies&gt;</code>

Basic Usage

Define a member PO class and a corresponding DTO:

<code>/**
 * Shopping member
 */
@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>
<code>/**
 * Member DTO
 */
@Data
@EqualsAndHashCode(callSuper = false)
public class MemberDto {
    private Long id;
    private String username;
    private String password;
    private String nickname;
    // Different type property
    private String birthday;
    // Different name property
    private String phoneNumber;
    private String icon;
    private Integer gender;
}</code>

Create a mapper interface:

<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>

Collection Mapping

MapStruct can map a list of PO objects to a list of DTOs directly:

<code>@Mapper
public interface MemberMapper {
    // ... previous methods
    List<MemberDto> toDtoList(List<Member> list);
}
</code>
<code>@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>

Nested Object Mapping

MapStruct also supports mapping of nested objects, such as an Order containing Member and Product objects:

<code>public class Order {
    private Long id;
    private String orderSn;
    private Date createTime;
    private String receiverAddress;
    private Member member;
    private List<Product> productList;
}
</code>
<code>public class OrderDto {
    private Long id;
    private String orderSn;
    private Date createTime;
    private String receiverAddress;
    private MemberDto memberDto;
    private List<ProductDto> productDtoList;
}
</code>
<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>

Composite Mapping

MapStruct can merge properties from multiple source objects into a single target object:

<code>@Mapper
public interface MemberMapper {
    // ... previous methods
    @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>

Advanced Usage

Dependency Injection

Configure the mapper with

componentModel = "spring"

so that Spring can inject it as a bean:

<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>@Autowired
private MemberSpringMapper memberSpringMapper;
</code>

Constants, Default Values, and Expressions

MapStruct allows setting constant values, default values, and Java expressions during mapping:

<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>

Custom Pre‑ and Post‑Mapping Logic

Define an abstract mapper with

@BeforeMapping

and

@AfterMapping

methods to apply custom logic:

<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>

Exception Handling

Create a custom validator and let the mapper throw a checked exception when validation fails:

<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>
<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>

Conclusion

MapStruct provides a robust, compile‑time solution for object mapping in Java, eliminating the boilerplate code required by BeanUtils and offering extensive features such as nested, collection, custom, and error‑handling mappings, making it an essential tool for backend development.

javamavenSpring BootMapStructObject Mapping
macrozheng
Written by

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.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.