介紹
前提
在Java 項目開發中,存在需要連個不同類的對象的轉化的情況,
例如VO與DO 的中同一邏輯對象的值轉換。
@AllArgsConstructor
@Data
public class UserVo {
private Long id;
private String username;
private String password;
private String phoneNum;
private String email;
private Role role;
}
public class UserDo {
private Long id;
private String username;
private String password;
private String phoneNum;
private String email;
private Role role;
}
兩個類中的屬性大致相同,但是應爲所屬的邏輯分層,導致被分成不同的POJO對象,對於業務分層交互中會用到兩個對象的值的交換。
通常的解決方案:
-
自行書寫轉換類,進行值轉換
- 優點:轉換方便,靈活度高
- 缺點:代碼冗餘過高
-
使用Java內省機制,Apache/Spring 的
BeanUtil.copyProperties
- 優點:代碼簡潔
- 缺點:轉換的靈活度不高,對於屬性名稱不同的無法轉換,內省基於Java的反射性能較低
-
MapStruct
- MapStruct 爲兩者的優勢結合,在相對降低代碼繁瑣的同時,保留性能
- 基於註解形式,簡化代碼冗餘
- 才用Java 提供的編譯時Process ,在class 生成期 生成代理類,避免Java 運行時的反射帶來的性能損耗
Demo
Order.java
package com.liaojl.test.mapstruct;
public class Order {
/**
* 訂單id
*/
private Long id;
/**
* 訂單編號
*/
private String orderSn;
/**
* 收貨人姓名/號碼
*/
private String receiverKeyword;
/**
* 訂單狀態:0->待付款;1->待發貨;2->已發貨;3->已完成;4->已關閉;5->無效訂單
*/
private Integer status;
/**
* 訂單類型:0->正常訂單;1->秒殺訂單
*/
private Integer orderType;
/**
* 訂單來源:0->PC訂單;1->app訂單
*/
private Integer sourceType;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getOrderSn() {
return orderSn;
}
public void setOrderSn(String orderSn) {
this.orderSn = orderSn;
}
public String getReceiverKeyword() {
return receiverKeyword;
}
public void setReceiverKeyword(String receiverKeyword) {
this.receiverKeyword = receiverKeyword;
}
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
public Integer getOrderType() {
return orderType;
}
public void setOrderType(Integer orderType) {
this.orderType = orderType;
}
public Integer getSourceType() {
return sourceType;
}
public void setSourceType(Integer sourceType) {
this.sourceType = sourceType;
}
}
OrderQueryParam.java
package com.liaojl.test.mapstruct;
public class OrderQueryParam {
/**
* 訂單編號
*/
private String orderSn;
/**
* 收貨人姓名/號碼
*/
private String receiverKeyword;
/**
* 訂單狀態:0->待付款;1->待發貨;2->已發貨;3->已完成;4->已關閉;5->無效訂單
*/
private Integer status;
/**
* 訂單類型:0->正常訂單;1->秒殺訂單
*/
private Integer orderType;
/**
* 訂單來源:0->PC訂單;1->app訂單
*/
private Integer sourceType;
public String getOrderSn() {
return orderSn;
}
public void setOrderSn(String orderSn) {
this.orderSn = orderSn;
}
public String getReceiverKeyword() {
return receiverKeyword;
}
public void setReceiverKeyword(String receiverKeyword) {
this.receiverKeyword = receiverKeyword;
}
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
public Integer getOrderType() {
return orderType;
}
public void setOrderType(Integer orderType) {
this.orderType = orderType;
}
public Integer getSourceType() {
return sourceType;
}
public void setSourceType(Integer sourceType) {
this.sourceType = sourceType;
}
}
配置轉換類
package com.liaojl.test.mapstruct.mapper;
import com.liaojl.test.mapstruct.Order;
import com.liaojl.test.mapstruct.OrderQueryParam;
import org.mapstruct.Mapper;
@Mapper
public interface OrderMapper {
OrderQueryParam entity2queryParam(Order order);
}
測試
/**
* @author: liaojl
* @date: 2020/5/30 18:09
* @since: 1.0
*/
public class Test {
@org.junit.Test
public void entity2queryParam() {
Order order = new Order();
order.setId(12345L);
order.setOrderSn("orderSn");
order.setOrderType(0);
order.setReceiverKeyword("keyword");
order.setSourceType(1);
order.setStatus(2);
OrderMapper mapper = Mappers.getMapper(OrderMapper.class);
OrderQueryParam orderQueryParam = mapper.entity2queryParam(order);
assertEquals(orderQueryParam.getOrderSn(), order.getOrderSn());
assertEquals(orderQueryParam.getOrderType(), order.getOrderType());
assertEquals(orderQueryParam.getReceiverKeyword(), order.getReceiverKeyword());
assertEquals(orderQueryParam.getSourceType(), order.getSourceType());
assertEquals(orderQueryParam.getStatus(), order.getStatus());
}
}
Java 編譯期 自動生成代理Java代碼
package com.liaojl.test.mapstruct.mapper;
import com.liaojl.test.mapstruct.Order;
import com.liaojl.test.mapstruct.OrderQueryParam;
import javax.annotation.Generated;
@Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2020-05-30T23:20:57+0800",
comments = "version: 1.3.1.Final, compiler: javac, environment: Java 11.0.6 (Amazon.com Inc.)"
)
public class OrderMapperImpl implements OrderMapper {
@Override
public OrderQueryParam entity2queryParam(Order order) {
if ( order == null ) {
return null;
}
OrderQueryParam orderQueryParam = new OrderQueryParam();
orderQueryParam.setOrderSn( order.getOrderSn() );
orderQueryParam.setReceiverKeyword( order.getReceiverKeyword() );
orderQueryParam.setStatus( order.getStatus() );
orderQueryParam.setOrderType( order.getOrderType() );
orderQueryParam.setSourceType( order.getSourceType() );
return orderQueryParam;
}
}
技術點
基於Java Processororg.mapstruct.ap.MappingProcessor
org.mapstruct.Mapper
@org.mapstruct.Mapping註解用來聲明成員屬性的映射。該註解有兩個重要的屬性:
- source 代表轉換的源。
- target 代表轉換的目標。
當兩者屬性相同時可以忽略,MapStruct 轉換時終調用的是 target的setter 和 source 的 getter 方法,而非反射。這也是其性能比較好的原因之一。
@Mapper
public interface OrderMapper {
OrderQueryParam entity2queryParam(Order order);
}
與org.mapstruct.Mapping
針對 target與source 的屬性名稱 存在差異性時,可以進行自定義轉換
- target的a屬性轉爲source中B的a屬性
- target的b屬性轉爲source中b1屬性
@Mapping(target = "a", source = "B.a")
@Mapping(target = "b", source = "b1")
Target sourceToTarget(Source source);
支持默認值
@Mapping(target = "stringProperty", source = "stringProp", defaultValue = "undefined")
格式化操作
格式化也是我們經常使用的操作,比如數字格式化,日期格式化。
這是處理數字格式化的操作,遵循java.text.DecimalFormat的規範:
@Mapping(source = "price", numberFormat = "$#.00")
下面展示了將一個日期集合映射到日期字符串集合的格式化操作上:
@IterableMapping(dateFormat = "dd.MM.yyyy")
List<String> stringListToDateList(List<Date> dates);
使用 java 表達式
下面演示如何使用LocalDateTime 作爲當前的時間值注入 addTime 屬性中。
首先在@org.mapstruct.Mapper 的 imports 屬性中導入 LocalDateTime,該屬性是數組意味着你可以根據需要導入更多的處理類:
@Mapper(imports = {LocalDateTime.class})
接下來只需要在對應的方法上添加註解@org.mapstruct.Mapping ,其屬性expression 接收一個 java() 包括的表達式:
- 無入參版本:
@Mapping(target = "addTime", expression = "java(LocalDateTime.now())")
- 攜帶入參的版本, 我們將 Car 的出廠日期字符串manufactureDateStr 注入到 CarDTO 的 LocalDateTime 類型屬性addTime 中去:
@Mapping(target = "addTime", expression = "java(LocalDateTime.parse(car.manufactureDateStr))")
CarDTO carToCarDTO(Car car);
org.mapstruct.Mappings
配置多個bean屬性的映射。
提示:使用Java 8或更高版本時,可以省略@Mappings 包裝器註釋,並直接在一個方法上指定多個@Mapping註釋。
這兩個示例是相等的。
// before Java 8
@Mapper
public interface MyMapper {
@Mappings({
@Mapping(source = "first", target = "firstProperty"),
@Mapping(source = "second", target = "secondProperty")
})
HumanDto toHumanDto(Human human);
}
// Java 8 and later
@Mapper
public interface MyMapper {
@Mapping(source = "first", target = "firstProperty"),
@Mapping(source = "second", target = "secondProperty")
HumanDto toHumanDto(Human human);
}
Spring 集成
MapStruct 轉換 Mapper 注入Spring IoC 容器
如果使用要把Mapper 注入Spring IoC 容器我們只需要這麼聲明,不用再構建一個單例,就可以像其他 spring bean一樣對註解的Bean 進行引用了:
@Mapper(componentModel = "spring")
public interface OrderMapper {
OrderQueryParam entity2queryParam(Order order);
}