头图
When working on projects, it is often necessary to switch between PO, VO, and DTO. Simple object conversion, using BeanUtils is basically enough, but if you use it for complex conversion, you have to write a bunch of Getter and Setter methods. Today I recommend an automatic object mapping tool MapStruct , which is really powerful!

SpringBoot actual combat e-commerce project mall (50k+star) address: https://github.com/macrozheng/mall

About BeanUtils

I usually use the BeanUtil class in Hutool to implement object conversion. After using it too much, I found some shortcomings:

  • Object attribute mapping is realized by reflection, and the performance is relatively low;
  • For different names or different types of properties cannot be converted, you have to write Getter and Setter methods separately;
  • For nested sub-objects that also need to be converted, you have to deal with it yourself;
  • When converting collection objects, you have to use loops and copy them one by one.

For these shortcomings, MapStruct can solve it, and it is worthy of being a powerful object mapping tool!

Introduction to MapStruct

MapStruct is an object attribute mapping tool based on Java annotations. There is already 4.5K+Star on Github. When using it, as long as we define the object attribute mapping rules in the interface, it can automatically generate the mapping implementation class, does not use reflection, has excellent performance, and can achieve various complex mappings.

IDEA plug-in support

As a very popular object mapping tool, MapStruct also provides a special IDEA plug-in, we can install the plug-in before using it.

Project integration

Integrating MapStruct in SpingBoot is very simple, just add the following two dependencies, here is the 1.4.2.Final version.
<dependency>
    <!--MapStruct相关依赖-->
    <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>

Basic use

After integrating MapStruct, let's experience its functions and see what its magic is!

Basic mapping

Let's take a quick start, experience the basic functions of MapStruct, and talk about its implementation principles.
  • First, we prepare the member PO object Member to be used;
/**
 * 购物会员
 * 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;
}
  • Then prepare the member's DTO object MemberDto , we need to convert the Member object to the MemberDto object;
/**
 * 购物会员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;
    //与PO类型不同的属性
    private String birthday;
    //与PO名称不同的属性
    private String phoneNumber;
    private String icon;
    private Integer gender;
}
  • Then create a mapping interface MemberMapper to realize the mapping of attributes of the same name and type, attributes of different names, and attributes of different types;
/**
 * 会员对象映射
 * Created by macro on 2021/10/21.
 */
@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);
}
  • Next, create a test interface in the Controller, and directly call the conversion method toDto INSTANCE instance in the interface;
/**
 * MapStruct对象转换测试Controller
 * Created by macro on 2021/10/21.
 */
@RestController
@Api(tags = "MapStructController", description = "MapStruct对象转换测试")
@RequestMapping("/mapStruct")
public class MapStructController {

    @ApiOperation(value = "基本映射")
    @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);
    }
}
  • After running the project, I tested the interface in Swagger and found that all properties of PO have been successfully converted to DTO. Swagger access address: http://localhost:8088/swagger-ui

  • In fact, the implementation principle of MapStruct is very simple, that is, according to the @Mapper and @Mapping we used in the Mapper interface, to generate the implementation class of the interface at runtime, we can open the 06181ed35af95a directory of the target

  • The following is the MemberMapper , you can say goodbye to the handwritten Getter and Setter!
public class MemberMapperImpl implements MemberMapper {
    public MemberMapperImpl() {
    }

    public MemberDto toDto(Member member) {
        if (member == null) {
            return null;
        } else {
            MemberDto memberDto = new MemberDto();
            memberDto.setPhoneNumber(member.getPhone());
            if (member.getBirthday() != null) {
                memberDto.setBirthday((new SimpleDateFormat("yyyy-MM-dd")).format(member.getBirthday()));
            }

            memberDto.setId(member.getId());
            memberDto.setUsername(member.getUsername());
            memberDto.setPassword(member.getPassword());
            memberDto.setNickname(member.getNickname());
            memberDto.setIcon(member.getIcon());
            memberDto.setGender(member.getGender());
            return memberDto;
        }
    }
}

Collection mapping

MapStruct also provides the function of collection mapping, which can directly convert a PO list into a DTO list, and no longer need to convert objects one by one!
  • Add the toDtoList MemberMapper interface for list conversion;
/**
 * 会员对象映射
 * Created by macro on 2021/10/21.
 */
@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);
}
  • Create a test interface in the Controller, and call the conversion method toDtoList INSTANCE instance in the Mapper interface;
/**
 * MapStruct对象转换测试Controller
 * Created by macro on 2021/10/21.
 */
@RestController
@Api(tags = "MapStructController", description = "MapStruct对象转换测试")
@RequestMapping("/mapStruct")
public class MapStructController {

    @ApiOperation(value = "集合映射")
    @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);
    }
}
  • After calling the interface test in Swagger, the PO list has been converted to a DTO list.

Sub-object mapping

MapStruct also supports the situation where the object contains sub-objects that also need to be converted.
  • For example, we have an order PO object Order , nested with Member and Product objects;
/**
 * 订单
 * 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;
}
  • We need to convert to OrderDto object, OrderDto contains MemberDto and ProductDto two sub-objects also need to be converted;
/**
 * 订单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;
    //子对象映射Dto
    private MemberDto memberDto;
    //子对象数组映射Dto
    private List<ProductDto> productDtoList;
}
  • We only need to create a Mapper interface, and then inject the conversion Mapper of the child object uses , and then set the attribute mapping rules @Mapping
/**
 * 订单对象映射
 * Created by macro on 2021/10/21.
 */
@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);
}
  • Next, create a test interface in the Controller, and directly call the conversion method toDto INSTANCE instance in the Mapper;
/**
 * MapStruct对象转换测试Controller
 * Created by macro on 2021/10/21.
 */
@RestController
@Api(tags = "MapStructController", description = "MapStruct对象转换测试")
@RequestMapping("/mapStruct")
public class MapStructController {
    
    @ApiOperation(value = "子对象映射")
    @GetMapping("/subMapping")
    public CommonResult subMapping() {
        List<Order> orderList = getOrderList();
        OrderDto orderDto = OrderMapper.INSTANCE.toDto(orderList.get(0));
        return CommonResult.success(orderDto);
    }
}
  • After calling the interface test in Swagger, you can find that the sub-object attributes have been converted.

Merge mapping

MapStruct also supports mapping multiple object properties to one object.
  • For example, here we map some attributes of Member and Order MemberOrderDto ;
/**
 * 会员商品信息组合Dto
 * Created by macro on 2021/10/21.
 */
@Data
@EqualsAndHashCode(callSuper = false)
public class MemberOrderDto extends MemberDto{
    private String orderSn;
    private String receiverAddress;
}
  • Then add the toMemberOrderDto method to the Mapper. It should be noted that because the parameters have two attributes, you need source parameter name. The name of the attribute to prevent conflicts (the two parameters have id attributes);
/**
 * 会员对象映射
 * Created by macro on 2021/10/21.
 */
@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);
}
  • Next, create a test interface in the Controller, and directly call the conversion method toMemberOrderDto INSTANCE instance in the Mapper;
/**
 * MapStruct对象转换测试Controller
 * Created by macro on 2021/10/21.
 */
@RestController
@Api(tags = "MapStructController", description = "MapStruct对象转换测试")
@RequestMapping("/mapStruct")
public class MapStructController {
    
    @ApiOperation(value = "组合映射")
    @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 memberOrderDto = MemberMapper.INSTANCE.toMemberOrderDto(member,order);
        return CommonResult.success(memberOrderDto);
    }
}
  • After calling the interface test in Swagger, you can find that the attributes in Member and Order have been mapped to MemberOrderDto.

Advanced use

Through the above basic usage, you can already play MapStruct, let's introduce some advanced usage.

Use dependency injection

Above we all call methods through the INSTANCE instance in the Mapper interface, and we can also use dependency injection in Spring.
  • To use dependency injection, we only need to @Mapper the componentModel parameter of spring , so that when the interface implementation class is generated, MapperStruct will add the @Component annotation to it;
/**
 * 会员对象映射(依赖注入)
 * Created by macro on 2021/10/21.
 */
@Mapper(componentModel = "spring")
public interface MemberSpringMapper {
    @Mapping(source = "phone",target = "phoneNumber")
    @Mapping(source = "birthday",target = "birthday",dateFormat = "yyyy-MM-dd")
    MemberDto toDto(Member member);
}
  • @Autowired annotation injection in the Controller to use;
/**
 * MapStruct对象转换测试Controller
 * Created by macro on 2021/10/21.
 */
@RestController
@Api(tags = "MapStructController", description = "MapStruct对象转换测试")
@RequestMapping("/mapStruct")
public class MapStructController {

    @Autowired
    private MemberSpringMapper memberSpringMapper;

    @ApiOperation(value = "使用依赖注入")
    @GetMapping("/springMapping")
    public CommonResult springMapping() {
        List<Member> memberList = LocalJsonUtil.getListFromJson("json/members.json", Member.class);
        MemberDto memberDto = memberSpringMapper.toDto(memberList.get(0));
        return CommonResult.success(memberDto);
    }
}
  • Under the test of calling the interface in Swagger, it can be found that it can be used normally as before.

Use constants, default values, and expressions

When using MapStruct to map attributes, we can set the attributes as constants or default values, or we can write expressions in Java to automatically generate attributes.
  • For example, the following product class Product object;
/**
 * 商品
 * Created by macro on 2021/10/12.
 */
@Data
@EqualsAndHashCode(callSuper = false)
public class Product {
    private Long id;
    private String productSn;
    private String name;
    private String subTitle;
    private String brandName;
    private BigDecimal price;
    private Integer count;
    private Date createTime;
}
  • Product ProductDto we want to convert the object, id property is set to a constant, count set the default value. 1, productSn arranged to generate a UUID;
/**
 * 商品Dto
 * Created by macro on 2021/10/12.
 */
@Data
@EqualsAndHashCode(callSuper = false)
public class ProductDto {
    //使用常量
    private Long id;
    //使用表达式生成属性
    private String productSn;
    private String name;
    private String subTitle;
    private String brandName;
    private BigDecimal price;
    //使用默认值
    private Integer count;
    private Date createTime;
}
  • Create the ProductMapper interface, and set the mapping rules @Mapping , defaultValue , and expression in the annotations of constant
/**
 * 商品对象映射
 * Created by macro on 2021/10/21.
 */
@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);
}
  • Next, create a test interface in the Controller, and directly call the conversion method toDto INSTANCE instance in the interface;
/**
 * MapStruct对象转换测试Controller
 * Created by macro on 2021/10/21.
 */
@RestController
@Api(tags = "MapStructController", description = "MapStruct对象转换测试")
@RequestMapping("/mapStruct")
public class MapStructController {
    @ApiOperation(value = "使用常量、默认值和表达式")
    @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 productDto = ProductMapper.INSTANCE.toDto(product);
        return CommonResult.success(productDto);
    }
}
  • Under the test of calling the interface in Swagger, the object has been successfully converted.

Customize before and after mapping

MapStruct also supports some custom operations before and after mapping, similar to the aspect in AOP.
  • At this time, since we need to create custom processing method to create an abstract class ProductRoundMapper by @BeforeMapping annotation before custom mapping operation by @AfterMapping the annotation custom mapping operation;
/**
 * 商品对象映射(自定义处理)
 * Created by macro on 2021/10/21.
 */
@Mapper(imports = {UUID.class})
public abstract class ProductRoundMapper {
    public static 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){
        //映射前当price<0时设置为0
        if(product.getPrice().compareTo(BigDecimal.ZERO)<0){
            product.setPrice(BigDecimal.ZERO);
        }
    }

    @AfterMapping
    public void afterMapping(@MappingTarget ProductDto productDto){
        //映射后设置当前时间为createTime
        productDto.setCreateTime(new Date());
    }
}
  • Next, create a test interface in the Controller, and directly call the conversion method toDto INSTANCE instance in the Mapper;
/**
 * MapStruct对象转换测试Controller
 * Created by macro on 2021/10/21.
 */
@RestController
@Api(tags = "MapStructController", description = "MapStruct对象转换测试")
@RequestMapping("/mapStruct")
public class MapStructController {
    
    @ApiOperation(value = "在映射前后进行自定义处理")
    @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 productDto = ProductRoundMapper.INSTANCE.toDto(product);
        return CommonResult.success(productDto);
    }
}
  • After calling the interface test in Swagger, you can find that the custom operation has been applied.

Handling the mapping exception

Exceptions will inevitably occur when the code runs, and MapStruct also supports handling mapping exceptions.
  • We need to create a custom exception class first;
/**
 * 商品验证异常类
 * Created by macro on 2021/10/22.
 */
public class ProductValidatorException extends Exception{
    public ProductValidatorException(String message) {
        super(message);
    }
}
  • Then create a verification class, and throw our custom exception price set less than 0;
/**
 * 商品验证异常处理器
 * Created by macro on 2021/10/22.
 */
public class ProductValidator {
    public BigDecimal validatePrice(BigDecimal price) throws ProductValidatorException {
        if(price.compareTo(BigDecimal.ZERO)<0){
            throw new ProductValidatorException("价格不能小于0!");
        }
        return price;
    }
}
  • After we passed @Mapper annotations uses use to verify class properties;
/**
 * 商品对象映射(处理映射异常)
 * Created by macro on 2021/10/21.
 */
@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;
}
  • Then add a test interface to the Controller, set price to -1 , at this time an exception will be thrown during the mapping;
/**
 * MapStruct对象转换测试Controller
 * Created by macro on 2021/10/21.
 */
@RestController
@Api(tags = "MapStructController", description = "MapStruct对象转换测试")
@RequestMapping("/mapStruct")
public class MapStructController {
    @ApiOperation(value = "处理映射异常")
    @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 productDto = null;
        try {
            productDto = ProductExceptionMapper.INSTANCE.toDto(product);
        } catch (ProductValidatorException e) {
            e.printStackTrace();
        }
        return CommonResult.success(productDto);
    }
}
  • After calling the interface test in Swagger, it is found that the custom exception information has been printed in the running log.

Summarize

Through the above experience of using MapStruct, we can find that MapStruct is far more powerful than BeanUtils. When we want to implement more complex object mapping, it can save the process of writing Getter and Setter methods. Of course, the above only introduces some common functions of MapStruct. Its functions are much more than that. If you are interested, you can check the official documents.

Reference

Official document: https://mapstruct.org/documentation/stable/reference/html

Project source code address

https://github.com/macrozheng/mall-learning/tree/master/mall-tiny-mapstruct

This article GitHub https://github.com/macrozheng/mall-learning has been included, welcome to Star!

macrozheng
1.1k 声望1.3k 粉丝