幹掉 BeanUtils!試試這款 Bean 自動對映工具,真心強大!

macrozheng發表於2021-11-03
平時做專案的時候,經常需要做PO、VO、DTO之間的轉換。簡單的物件轉換,使用BeanUtils基本上是夠了,但是複雜的轉換,如果使用它的話又得寫一堆Getter、Setter方法了。今天給大家推薦一款物件自動對映工具MapStruct,功能真心強大!

SpringBoot實戰電商專案mall(50k+star)地址:https://github.com/macrozheng/mall

關於BeanUtils

平時我經常使用Hutool中的BeanUtil類來實現物件轉換,用多了之後就發現有些缺點:

  • 物件屬性對映使用反射來實現,效能比較低;
  • 對於不同名稱或不同型別的屬性無法轉換,還得單獨寫Getter、Setter方法;
  • 對於巢狀的子物件也需要轉換的情況,也得自行處理;
  • 集合物件轉換時,得使用迴圈,一個個拷貝。

對於這些不足,MapStruct都能解決,不愧為一款功能強大的物件對映工具!

MapStruct簡介

MapStruct是一款基於Java註解的物件屬性對映工具,在Github上已經有4.5K+Star。使用的時候我們只要在介面中定義好物件屬性對映規則,它就能自動生成對映實現類,不使用反射,效能優秀,能實現各種複雜對映。

IDEA外掛支援

作為一款非常流行的物件對映工具,MapStruct還提供了專門的IDEA外掛,我們在使用之前可以先安裝好外掛。

專案整合

在SpingBoot中整合MapStruct非常簡單,僅續新增如下兩個依賴即可,這裡使用的是1.4.2.Final版本。
<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>

基本使用

整合完MapStruct之後,我們來體驗下它的功能吧,看看它有何神奇之處!

基本對映

我們先來個快速入門,體驗一下MapStruct的基本功能,並聊聊它的實現原理。
  • 首先我們準備好要使用的會員PO物件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;
}
  • 然後再準備好會員的DTO物件MemberDto,我們需要將Member物件轉換為MemberDto物件;
/**
 * 購物會員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;
}
  • 然後建立一個對映介面MemberMapper,實現同名同型別屬性、不同名稱屬性、不同型別屬性的對映;
/**
 * 會員物件對映
 * 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);
}
  • 接下來在Controller中建立測試介面,直接通過介面中的INSTANCE例項呼叫轉換方法toDto
/**
 * 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);
    }
}

  • 其實MapStruct的實現原理很簡單,就是根據我們在Mapper介面中使用的@Mapper@Mapping等註解,在執行時生成介面的實現類,我們可以開啟專案的target目錄看下;

  • 下面是MapStruct為MemberMapper生成好的物件對映程式碼,可以和手寫Getter、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;
        }
    }
}

集合對映

MapStruct也提供了集合對映的功能,可以直接將一個PO列表轉換為一個DTO列表,再也不用一個個物件轉換了!
  • MemberMapper介面中新增toDtoList方法用於列表轉換;
/**
 * 會員物件對映
 * 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);
}
  • 在Controller中建立測試介面,直接通過Mapper介面中的INSTANCE例項呼叫轉換方法toDtoList
/**
 * 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);
    }
}
  • 在Swagger中呼叫介面測試下,PO列表已經轉換為DTO列表了。

子物件對映

MapStruct對於物件中包含子物件也需要轉換的情況也是有所支援的。
  • 例如我們有一個訂單PO物件Order,巢狀有MemberProduct物件;
/**
 * 訂單
 * 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;
}
  • 我們需要轉換為OrderDto物件,OrderDto中包含MemberDtoProductDto兩個子物件同樣需要轉換;
/**
 * 訂單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;
}
  • 我們只需要建立一個Mapper介面,然後通過使用uses將子物件的轉換Mapper注入進來,然後通過@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);
}
  • 接下來在Controller中建立測試介面,直接通過Mapper中的INSTANCE例項呼叫轉換方法toDto
/**
 * 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);
    }
}
  • 在Swagger中呼叫介面測試下,可以發現子物件屬性已經被轉換了。

合併對映

MapStruct也支援把多個物件屬性對映到一個物件中去。
  • 例如這裡把MemberOrder的部分屬性對映到MemberOrderDto中去;
/**
 * 會員商品資訊組合Dto
 * Created by macro on 2021/10/21.
 */
@Data
@EqualsAndHashCode(callSuper = false)
public class MemberOrderDto extends MemberDto{
    private String orderSn;
    private String receiverAddress;
}
  • 然後在Mapper中新增toMemberOrderDto方法,這裡需要注意的是由於引數中具有兩個屬性,需要通過引數名稱.屬性的名稱來指定source來防止衝突(這兩個引數中都有id屬性);
/**
 * 會員物件對映
 * 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);
}
  • 接下來在Controller中建立測試介面,直接通過Mapper中的INSTANCE例項呼叫轉換方法toMemberOrderDto
/**
 * 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);
    }
}
  • 在Swagger中呼叫介面測試下,可以發現Member和Order中的屬性已經被對映到MemberOrderDto中去了。

進階使用

通過上面的基本使用,大家已經可以玩轉MapStruct了,下面我們再來介紹一些進階的用法。

使用依賴注入

上面我們都是通過Mapper介面中的INSTANCE例項來呼叫方法的,在Spring中我們也是可以使用依賴注入的。
  • 想要使用依賴注入,我們只要將@Mapper註解的componentModel引數設定為spring即可,這樣在生成介面實現類時,MapperStruct會為其新增@Component註解;
/**
 * 會員物件對映(依賴注入)
 * 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);
}
  • 接下來在Controller中使用@Autowired註解注入即可使用;
/**
 * 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);
    }
}
  • 在Swagger中呼叫介面測試下,可以發現與之前一樣可以正常使用。

使用常量、預設值和表示式

使用MapStruct對映屬性時,我們可以設定屬性為常量或者預設值,也可以通過Java中的方法編寫表示式來自動生成屬性。
  • 例如下面這個商品類Product物件;
/**
 * 商品
 * 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物件,id屬性設定為常量,count設定預設值為1,productSn設定為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;
}
  • 建立ProductMapper介面,通過@Mapping註解中的constantdefaultValueexpression設定好對映規則;
/**
 * 商品物件對映
 * 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);
}
  • 接下來在Controller中建立測試介面,直接通過介面中的INSTANCE例項呼叫轉換方法toDto
/**
 * 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);
    }
}
  • 在Swagger中呼叫介面測試下,物件已經成功轉換。

在對映前後進行自定義處理

MapStruct也支援在對映前後做一些自定義操作,類似AOP中的切面。
  • 由於此時我們需要建立自定義處理方法,建立一個抽象類ProductRoundMapper,通過@BeforeMapping註解自定義對映前操作,通過@AfterMapping註解自定義對映後操作;
/**
 * 商品物件對映(自定義處理)
 * 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());
    }
}
  • 接下來在Controller中建立測試介面,直接通過Mapper中的INSTANCE例項呼叫轉換方法toDto
/**
 * 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);
    }
}
  • 在Swagger中呼叫介面測試下,可以發現已經應用了自定義操作。

處理對映異常

程式碼執行難免會出現異常,MapStruct也支援處理對映異常。
  • 我們需要先建立一個自定義異常類;
/**
 * 商品驗證異常類
 * Created by macro on 2021/10/22.
 */
public class ProductValidatorException extends Exception{
    public ProductValidatorException(String message) {
        super(message);
    }
}
  • 然後建立一個驗證類,當price設定小於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;
    }
}
  • 之後我們通過@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;
}
  • 然後在Controller中新增測試介面,設定price-1,此時在進行對映時會丟擲異常;
/**
 * 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);
    }
}
  • 在Swagger中呼叫介面測試下,發現執行日誌中已經列印了自定義異常資訊。

總結

通過上面對MapStruct的使用體驗,我們可以發現MapStruct遠比BeanUtils要強大。當我們想實現比較複雜的物件對映時,通過它可以省去寫Getter、Setter方法的過程。 當然上面只是介紹了MapStruct的一些常用功能,它的功能遠不止於此,感興趣的朋友可以檢視下官方文件。

參考資料

官方文件:https://mapstruct.org/documen...

專案原始碼地址

https://github.com/macrozheng...

本文 GitHub https://github.com/macrozheng/mall-learning 已經收錄,歡迎大家Star!

相關文章