平時做專案的時候,經常需要做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);
}
}
- 執行專案後在Swagger中測試介面,發現PO所有屬性已經成功轉換到DTO中去了,Swagger訪問地址:http://localhost:8088/swagger-ui
- 其實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
,巢狀有Member
和Product
物件;
/**
* 訂單
* 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
中包含MemberDto
和ProductDto
兩個子物件同樣需要轉換;
/**
* 訂單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也支援把多個物件屬性對映到一個物件中去。
- 例如這裡把
Member
和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;
}
- 然後在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
註解中的constant
、defaultValue
、expression
設定好對映規則;
/**
* 商品物件對映
* 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!