告別 BeanUtils,Mapstruct 從入門到精通
如果你現在還在使用 BeanUtils,看了本文,也會像我一樣,從此改用 Mapstruct。
對象之間的屬性拷貝,之前用的是 Spring 的 BeanUtils,有一次,在學習領域驅動設計的時候,看了一位大佬的文章,他在文章中提到使用 Mapstruct 做 DO 和 Entity 的相互轉換,出於好奇,後來就去了解了一下 Mapstruct,發現這個工具確實優秀,所以果斷棄用 BeanUtils。
如果你現在還在使用 BeanUtils,看了本文,也會像我一樣,從此改用 Mapstruct。
先上結論,Mapstruct 的性能遠遠高於 BeanUtils,這應該是大佬使用 Mapstruct 的主要原因,下面是我的測試結果,可以看出隨着屬性個數的增加,BeanUtils 的耗時也在增加,並且 BeanUtils 的耗時跟屬性個數成正比,而 Mapstruct 的耗時卻一直是 1 秒,所以從對比數據可以看出 Mapstruct 是非常優秀的,其性能遠遠超過 BeanUtils。
下文會講到 Mapstruct 性能好的根本原因。
Mapstruct 依賴
使用 Mapstruct 需要依賴的包如下, mapstruct、mapstruct-processor、lombok,可以去倉庫中查看最新版本。
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.5.0.Final</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.5.0.Final</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
</dependency>
簡單的屬性拷貝
下面我們先來看下 Mapstruct 最簡單的使用方式。
當兩個對象的屬性類型和名稱完全相同時,Mapstruct 會自動拷貝;假設我們現在需要把 UserPo 的屬性值拷貝到 UserEntity 中,我們需要做下面幾件事情:
-
定義 UserPo 和 UserEntity
-
定義轉換接口
-
編寫測試 main 方法
首先定義 UserPo 和 UserEntity
UserPo 和 UserEntity 的屬性類型和名稱完全相同。
package mapstruct;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class UserPo {
private Long id;
private Date gmtCreate;
private Date createTime;
private Long buyerId;
private Long age;
private String userNick;
private String userVerified;
}
package mapstruct;
import lombok.Data;
import java.util.Date;
@Data
public class UserEntity {
private Long id;
private Date gmtCreate;
private Date createTime;
private Long buyerId;
private Long age;
private String userNick;
private String userVerified;
}
定義轉換接口
定義 mapstruct 接口,在接口上打上 @Mapper 註解。
接口中有一個常量和一個方法,常量的值是接口的實現類,這個實現類是 Mapstruct 默認幫我們實現的,下文會講到。定義了一個 po2entity 的轉換方法,表示把入參 UserPo 對象,轉換成 UserEntity。
注意 @Mapper 是 Mapstruct 的註解,不要引錯了。
package mapstruct;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
@Mapper
public interface IPersonMapper {
IPersonMapper INSTANCT = Mappers.getMapper(IPersonMapper.class);
UserEntity po2entity(UserPo userPo);
}
測試類
創建一個 UserPo 對象,並使用 Mapstruct 做轉化。
package mapstruct;
import org.springframework.beans.BeanUtils;
import java.util.Date;
public class MapStructTest {
public static void main(String[] args) {
testNormal();
}
public static void testNormal() {
System.out.println("-----------testNormal-----start------");
UserPo userPo = UserPo.builder()
.id(1L)
.gmtCreate(new Date())
.buyerId(666L)
.userNick("測試mapstruct")
.userVerified("ok")
.age(18L)
.build();
System.out.println("1234" + userPo);
UserEntity userEntity = IPersonMapper.INSTANCT.po2entity(userPo);
System.out.println(userEntity);
System.out.println("-----------testNormal-----ent------");
}
}
測試結果
可以看到,所有賦值的屬性都做了處理,且兩邊的值都一樣,結果符合預期。
Mapstruct 性能優於 BeanUtils 的原因
Java 程序執行的過程,是由編譯器先把 java 文件編譯成 class 字節碼文件,然後由 JVM 去解釋執行 class 文件。Mapstruct 正是在 java 文件到 class 這一步幫我們實現了轉換方法,即做了預處理,提前編譯好文件,如果用過 lombok 的同學一定能理解其好處,通過查看 class 文件,可以看出 IPersonMapper 被打上 org.mapstruct.Mapper 註解後,編譯器自動會幫我們生成一個實現類 IPersonMapperImpl,並實現了 po2entity 這個方法,看下面的截圖。
IPersonMapperImpl 代碼
從生成的代碼可以看出,轉化過程非常簡單,只使用了 UserPo 的 get 方法和 UserEntity 的 set 方法,沒有複雜的邏輯處理,清晰明瞭,所以性能很高。
下面再去看 BeanUtils 的默認實現。
Spring 的 BeanUtils 源碼
BeanUtils 部分源碼如下,轉換的原理是使用的反射,反射的效率相對來說是低的,因爲 jvm 優化在這種場景下有可能無效,所以在對性能要求很高或者經常被調用的程序中,儘量不要使用。我們平時在研發過程中,也會遵守這個原則,非必要,不反射。
從下面的 BeanUtils 代碼中可以看出,轉化邏輯非常複雜,有很多的遍歷,去獲取屬性,獲取方法,設置方法可訪問,然後執行,所以執行效率相對 Mapstruct 來說,是非常低的。回頭看 Mapstruct 自動生成的實現類,簡潔、高效。
private static void copyProperties(Object source, Object target, Class<?> editable, String... ignoreProperties)
throws BeansException {
Assert.notNull(source, "Source must not be null");
Assert.notNull(target, "Target must not be null");
Class<?> actualEditable = target.getClass();
if (editable != null) {
if (!editable.isInstance(target)) {
throw new IllegalArgumentException("Target class [" + target.getClass().getName() +
"] not assignable to Editable class [" + editable.getName() + "]");
}
actualEditable = editable;
}
PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);
List<String> ignoreList = (ignoreProperties != null ? Arrays.asList(ignoreProperties) : null);
for (PropertyDescriptor targetPd : targetPds) {
Method writeMethod = targetPd.getWriteMethod();
if (writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) {
PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());
if (sourcePd != null) {
Method readMethod = sourcePd.getReadMethod();
if (readMethod != null &&
ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) {
try {
if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
readMethod.setAccessible(true);
}
Object value = readMethod.invoke(source);
if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
writeMethod.setAccessible(true);
}
writeMethod.invoke(target, value);
}
catch (Throwable ex) {
throw new FatalBeanException(
"Could not copy property '" + targetPd.getName() + "' from source to target", ex);
}
}
}
}
}
屬性類型相同名稱不同
對於屬性名稱不同的屬性進行處理時,需要使用 @Mapping,比如修改 UserEntity 中的 userNick 爲 userNick1,然後進行轉換。
修改 UserEntity 屬性 userNick1
package mapstruct;
import lombok.Data;
import java.util.Date;
@Data
public class UserEntity {
private Long id;
private Date gmtCreate;
private Date createTime;
private Long buyerId;
private Long age;
private String userNick1;
private String userVerified;
}
@Mapping 註解指定 source 和 target 字段名稱對應關係
@Mapping(target = "userNick1", source = "userNick"),此處的意思就是在轉化的過程中,將 UserPo 的 userNick 屬性值賦值給 UserEntity 的 userNick1 屬性。
package mapstruct;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
@Mapper
public interface IPersonMapper {
IPersonMapper INSTANCT = Mappers.getMapper(IPersonMapper.class);
@Mapping(target = "userNick1", source = "userNick")
UserEntity po2entity(UserPo userPo);
}
執行結果
可以看到,正常映射,符合預期。
查看 class 文件
我們再來看實現類,可以看到,Mapstruct 幫我們做了處理,把 po 的 userNick 屬性賦值給了 entity 的 userNick1。
String 轉日期 & String 轉數字 & 忽略某個字端 & 給默認值等
@Mapping(target = "createTime", source = "createTime", dateFormat = "yyyy-MM-dd")
@Mapping(target = "age", source = "age", numberFormat = "#0.00")
@Mapping(target = "id", ignore = true)
@Mapping(target = "userVerified", defaultValue = "defaultValue-2")
查看 class 實現類
-
createTime:可以看到對日期使用了 SimpleDateFormat 進行轉換,這裏建議不要使用這個,因爲每次都創建了一個 SimpleDateFormat,可以參考《阿里巴巴 Java 開發手冊》關於日期轉換的建議。
-
age:字符串轉數字,也是幫忙做了處理
-
id:字段賦值沒有了
-
userVerified:如果爲 null 賦值默認值
自定義轉換
如果現有的能力都不能滿足需要,可以自定義一個轉換器,比如我們需要把一個字符串使用 JSON 工具轉換成對象。
添加屬性
我們在 po 中加入一個字符串的 attributes 屬性,在 entity 中加入 Attributes 類型的屬性
package mapstruct;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Attributes {
private Long id;
private String name;
}
package mapstruct;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class UserPo {
private Long id;
private Date gmtCreate;
private String createTime;
private Long buyerId;
private String age;
private String userNick;
private String userVerified;
private String attributes;
}
package mapstruct;
import lombok.Data;
import java.util.Date;
@Data
public class UserEntity {
private Long id;
private Date gmtCreate;
private Date createTime;
private Long buyerId;
private Long age;
private String userNick1;
private String userVerified;
private Attributes attributes;
}
編寫自定義轉換處理類
轉換器很簡單,就是一個普通的 Java 類,只要在方法上打上 Mapstruct 的註解 @Named。
package mapstruct;
import com.alibaba.fastjson.JSONObject;
import org.apache.commons.lang3.StringUtils;
import org.mapstruct.Named;
public class AttributeConvertUtil {
/**
* json字符串轉對象
*
* @param jsonStr
* @return
*/
@Named("jsonToObject")
public Attributes jsonToObject(String jsonStr) {
if (StringUtils.isEmpty(jsonStr)) {
return null;
}
return JSONObject.parseObject(jsonStr, Attributes.class);
}
}
修改轉換接口
-
在 @Mapper 上引用我們的自定義轉換代碼類 AttributeConvertUtil
-
使用 qualifiedByName 指定我們使用的自定義轉換方法
package mapstruct;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
/**
* @author jiangzhengyin
*/
@Mapper(uses = AttributeConvertUtil.class)
public interface IPersonMapper {
IPersonMapper INSTANCT = Mappers.getMapper(IPersonMapper.class);
@Mapping(target = "attributes", source = "attributes", qualifiedByName = "jsonToObject")
@Mapping(target = "userNick1", source = "userNick")
@Mapping(target = "createTime", source = "createTime", dateFormat = "yyyy-MM-dd")
@Mapping(target = "age", source = "age", numberFormat = "#0.00")
@Mapping(target = "id", ignore = true)
@Mapping(target = "userVerified", defaultValue = "defaultValue-2")
UserEntity po2entity(UserPo userPo);
}
測試類及結果
可以看出我們將把 String 轉成了 JSON 對象
public class MapStructTest {
public static void main(String[] args) {
testNormal();
}
public static void testNormal() {
System.out.println("-----------testNormal-----start------");
String attributes = "{\"id\":2,\"name\":\"測試123\"}";
UserPo userPo = UserPo.builder()
.id(1L)
.gmtCreate(new Date())
.buyerId(666L)
.userNick("測試mapstruct")
.userVerified("ok")
.age("18")
.attributes(attributes)
.build();
System.out.println("1234" + userPo);
UserEntity userEntity = IPersonMapper.INSTANCT.po2entity(userPo);
System.out.println(userEntity);
System.out.println("-----------testNormal-----ent------");
}
}
查看實現類
可以看到,在實現類中 Mapstruct 幫我們 new 了一個 AttributeConvertUtil 的對象,並調用了該對象的 jsonToObject 方法,將字符串轉成 JSON,最終賦值給了 UserEntity 的 attributes 屬性,實現很簡單,也是我們可以猜到的。
性能對比
代碼很簡單,循環的創建 UserPo 對象,使用兩種方式,轉換成 UserEntity 對象,最終輸出兩種方式的執行耗時。可以加減屬性或者修改轉換次數,對比不同場景下的執行耗時。
public static void testTime() {
System.out.println("-----------testTime-----start------");
int times = 50000000;
final long springStartTime = System.currentTimeMillis();
for (int i = 0; i < times; i++) {
UserPo userPo = UserPo.builder()
.id(1L)
.gmtCreate(new Date())
.buyerId(666L)
.userNick("測試123")
.userVerified("ok")
.build();
UserEntity userEntity = new UserEntity();
BeanUtils.copyProperties(userPo, userEntity);
}
final long springEndTime = System.currentTimeMillis();
for (int i = 0; i < times; i++) {
UserPo userPo = UserPo.builder()
.id(1L)
.gmtCreate(new Date())
.buyerId(666L)
.userNick("測試123")
.userVerified("ok")
.build();
UserEntity userEntity = IPersonMapper.INSTANCT.po2entity(userPo);
}
final long mapstructEndTime = System.currentTimeMillis();
System.out.println("BeanUtils use time=" + (springEndTime - springStartTime) / 1000 + "秒" +
"; Mapstruct use time=" + (mapstructEndTime - springEndTime) / 1000 + "秒");
System.out.println("-----------testTime-----end------");
}
總結
通過本次調研,Mapstruct 的高性能是毋庸置疑的,這也是我選擇使用他的根本原因。在使用方式上和 BeanUtils 對比,Mapstruct 需要創建 mapper 接口和自定義轉換工具類,其實上手成本並不高,但是我們換取了高性能,這是非常值得的,所以強烈推薦大家使用 Mapstruct,是時候和 BeanUtils 說再見了。
保持好奇,不斷探索,讓程序更友好!
團隊介紹
TMALL CAMPUS (天貓校園) 是阿里巴巴旗下重要的業務單元,天貓校園整合阿里巴巴大生態,將新理念、新技術、新業態、新模式落地到校園,爲師生提供多方位、多形態的服務,協助高校後勤服務升級;致力於打造購物、學習、生活、實踐爲一體的校園生活新方式,實現校園商業的服務育人。
天貓校園,讓校園學習生活更美好
作者 | 蔣政印(不習)
編輯 | 橙子君
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/8yDzCzLB-9LncZVeAnMJmA