平时做项目的时候,经常需要做PO、VO、DTO之间的转换。简单的对象转换,除了使用BeanUtils,
MapStruct
可以更好的实现,同时也可以进行复杂的转换,功能很强大!
MapStruct简介
MapStruct是一款基于Java注解的对象属性映射工具。使用的时候我们只要在接口中定义好对象属性映射规则,它就能自动生成映射实现类,不使用反射,性能优秀,能实现各种复杂映射。
项目集成
<dependency>
<!--MapStruct相关依赖-->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.5.3.Final</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.5.3.Final</version>
<scope>compile</scope>
</dependency>
</dependencies>
基本使用及实现
- model对象
Member
:
/**
* 购物会员
*/
@Data
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;
}
- dto对象
MemberDto
:
/**
* 购物会员Dto
*/
@Data
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;
}
- 实现dto对象转换为model对象,实现
同名同类型属性、不同名称属性、不同类型属性的映射
。
1、在MapStruct中,如果源对象和目标对象存在同名但类型不同的字段,并且没有使用@Mapping标识进行显式的映射规则指定,MapStruct会尝试自动进行类型转换。
2、MapStruct会根据Java Bean规范中的命名约定,尝试寻找合适的转换方法,例如使用属性的getter和setter方法进行类型转换。如果找到了适当的转换方法,MapStruct会自动执行类型转换。
3、推荐在存在同名但类型不同的字段时使用@Mapping标识来明确指定映射规则,可以明确告诉MapStruct如何进行字段值的转换,避免潜在的类型转换错误或意外行为。
/**
* 会员对象映射
*/
@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);
}
- 实现原理
MapStruct的实现原理比较简单,就是根据我们在Mapper接口中使用的@Mapper
和@Mapping
等注解,在运行时生成接口的实现类,具体位置在项目的target
目录下;
生成的具体代码如下:
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;
}
}
}
集合映射
MapStruct也提供了集合映射的功能,可以直接将一个MODEL列表转换为一个DTO列表!
- 在
MemberMapper
接口中添加toDtoList
方法用于列表转换;
/**
* 会员对象映射
*/
@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);
}
子对象映射
MapStruct也支持对象中包含子对象也需要转换的情况。
- 场景
有一个订单PO对象Order
,嵌套有Member
和Product
对象,需要转换为OrderDto
对象,OrderDto
中包含MemberDto
和ProductDto
两个子对象同样需要转换;
/**
* 订单
*/
@Data
public class Order {
private Long id;
private String orderSn;
private Date createTime;
private String receiverAddress;
private Member member;
private List<Product> productList;
}
/**
* 订单Dto
*/
@Data
public class OrderDto {
private Long id;
private String orderSn;
private Date createTime;
private String receiverAddress;
//子对象映射Dto
private MemberDto memberDto;
//子对象数组映射Dto
private List<ProductDto> productDtoList;
}
- 需要创建一个
Mapper
接口,然后通过使用uses
将子对象的转换Mapper
注入进来,然后通过@Mapping设
置好属性映射规则即可实现;
/**
* 订单对象映射
*/
@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);
}
合并映射
MapStruct也支持把多个对象属性映射到一个对象中去。
- 场景
把Member
和Order
的部分属性映射到MemberOrderDto
中去;
/**
* 会员商品信息组合Dto
*/
@Data
public class MemberOrderDto extends MemberDto{
private String orderSn;
private String receiverAddress;
}
2.在Mapper
中添加toMemberOrderDto
方法,这里需要注意的是由于参数中具有两个属性,需要通过参数名称.属性的名称
来指定source来防止冲突(这两个参数中都有id属性);
/**
* 会员对象映射
*/
@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);
}
使用依赖注入
常规用法都是MemberMapper.INSTANCE.具体方法
直接调用,同时也可以通过spring依赖注入使用;
- 使用依赖注入,我们只要将
@Mapper
注解的componentModel参数设置为spring
即可,这样在生成接口实现类时,MapperStruct会为其添加@Component注解;
/**
* 会员对象映射(依赖注入)
*/
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public interface MemberSpringMapper {
@Mapping(source = "phone",target = "phoneNumber")
@Mapping(source = "birthday",target = "birthday",dateFormat = "yyyy-MM-dd")
MemberDto toDto(Member member);
}
- 在使用的类中通过
@Autowired
注解注入即可。
使用常量、默认值和表达式
使用MapStruct映射属性时,我们可以设置属性为常量或者默认值,也可以通过Java中的方法编写表达式来自动生成属性。
- 商品类Product对象
/**
* 商品
*/
@Data
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;
}
- ProductDto对象
/**
* 商品Dto
*/
@Data
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;
}
- 把Product转换为ProductDto对象,id属性设置为常量,count设置默认值为1,productSn设置为UUID生成。创建ProductMapper接口,通过@Mapping注解中的constant、defaultValue、expression设置好映射规则;
/**
* 商品对象映射
*/
@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);
}
在映射前后进行自定义处理
MapStruct也支持在映射前后做一些自定义操作,类似AOP中的切面。
需要创建自定义处理方法,创建一个抽象类ProductRoundMapper
,通过@BeforeMapping
注解自定义映射前操作,通过@AfterMapping
注解自定义映射后操作;
/**
* 商品对象映射(自定义处理)
*/
@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());
}
}
处理映射异常
MapStruct也支持处理映射异常。
- 自定义异常类:
/**
* 商品验证异常类
*/
public class ProductValidatorException extends Exception{
public ProductValidatorException(String message) {
super(message);
}
}
- 创建一个验证类,当price设置小于0时抛出我们自定义的异常;
/**
* 商品验证异常处理器
*/
public class ProductValidator {
public BigDecimal validatePrice(BigDecimal price) throws ProductValidatorException {
if(price.compareTo(BigDecimal.ZERO)<0){
throw new ProductValidatorException("价格不能小于0!");
}
return price;
}
}
- 通过@Mapper注解的uses属性运用验证类;
/**
* 商品对象映射(处理映射异常)
* 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;
}
关于BeanUtils
BeanUtils是平时开发中用到较多的对象复制工具类,但是也存在些多缺点。
- 对象属性映射使用反射来实现,性能比较低;
- 对于不同名称或不同类型的属性无法转换,还得单独写Getter、Setter方法,比如
Spring 的 BeanUtils.copyProperties
方法中,并没有内置的类型转换功能,导致String
类型值无法转换为BigDecimal
,而Apache Commons BeanUtils 的 copyProperties
方法在复制属性时会根据源对象字段和目标对象字段的类型进行相应的转换; - 对于嵌套的子对象也需要转换的情况,也得自行处理;
- 集合对象转换时,得使用循环,一个个拷贝。
参考
官方文档:https://mapstruct.org/documentation/stable/reference/html
Bean映射工具介绍对比:
https://blog.csdn.net/lemon_TT/article/details/122779141