還在用BeanUtils拷貝對象?MapStruct纔是王者!【附源碼】

前幾天,遠在北京的小夥伴在羣裏拋出了“MapStruct”的概念。對於只聞其名,未見其人的我來說,決定對其研究一番。本文我們就從 MapStruct 的概念出發,通過具體的代碼示例來研究它的使用情況,最後與“市面上”的其它工具來做個對比!

官方介紹

首先我們打開 MapStruct官網地址,映入眼簾的就是下邊的三步曲:

What is it?

MapStruct 是一個代碼生成器,它基於約定優先於配置的方法大大簡化了 JavaBean 類型之間映射的實現。生成的映射代碼使用普通方法調用,因此速度快、類型安全且易於理解。

Why?

多層應用程序通常需要在不同的對象模型(例如實體和 DTO)之間進行映射。編寫這樣的映射代碼是一項乏味且容易出錯的任務。MapStruct 旨在通過儘可能自動化來簡化這項工作。

與其他映射框架不同,MapStruct編譯時生成 bean 映射,這確保了高性能,允許快速的開發人員反饋和徹底的錯誤檢查。

How?

MapStruct 是插入 Java 編譯器的註釋處理器,可以在命令行構建(MavenGradle等)中使用,也可以在首選 IDE 中使用。它使用合理的默認值,但在配置或實現特殊行爲時,用戶可以自定義實現。

官網的解釋總是咬文嚼字,晦澀難懂的,看到這你只需要記住 MapStruct 是用來做實體類映射——實體類拷貝 的就可以了。

源碼地址:https://github.com/mapstruct/mapstruct</br>
官網推薦的 Demo: https://github.com/mapstruct/mapstruct-examples

簡單實現

我們注意到官網中有涉及到簡單樣例的實現,我們用2分鐘來分析一波:

1. 引入依賴

<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-jdk8</artifactId>
    <version>1.3.0.Final</version>
</dependency>
//註解處理器,根據註解自動生成mapper的實現
<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-processor</artifactId>
    <version>1.2.0.Final</version>
</dependency>

我們在編譯時會報 java: No property named "numberOfSeats" exists in source parameter(s). Did you mean "null"? 錯誤,經過查閱資料發現 mapstruct-processorLombok 的版本需要統一一下:mapstruct-processor1.2.0.FinalLombok1.16.14

2. 準備實體類 Car.java 和 數據傳輸類 CarDto.java

@NoArgsConstructor
@AllArgsConstructor
@Data
public class Car {
    private String make;
    private int numberOfSeats;
    private CarType type;
}

@Data
@NoArgsConstructor
@AllArgsConstructor
public class CarDto {
    private String make;
    private int seatCount;
    private String type;

}

3. 創建映射器接口,裏邊定義映射方法

@Mapper
public interface CarMapper {
 
    CarMapper INSTANCE = Mappers.getMapper( CarMapper.class );

    @Mapping(source = "numberOfSeats", target = "seatCount")
    CarDto carToCarDto(Car car); 
   
}

解析分析:

  • @Mapper 將接口標記爲映射接口,並允許 MapStruct 處理器在編譯期間啓動。這裏的 @Mapper 註解不是 mybatis 的註解,而是 org.mapstruct.Mapper 的;
  • 實際映射方法 carToCarDto() 期望源對象 Car 作爲參數,並返回目標對象 CarDto ,方法名可以自由選擇;
  • 對於源對象和目標對象中具有不同名稱的屬性,可以使用 @Mapping 註釋來配置名稱;
  • 對於源對象和目標對象中具有不同類型的屬性,也可以使用 @Mapping 註釋來進行轉換,比如:類型屬性將從枚舉類型轉換爲字符串;
  • 一個接口中可以有多個映射方法,對於所有的這些方法,MapStruct 將生成一個實現;
  • 該接口的實現實例可以從 Mappers 中獲得,接口聲明一個 INSTANCE,爲客戶端提供對映射器實現的訪問。

4. 實現類

我們可以將代碼進行編譯,然後會發現在 target 文件中生成了 CarMapperImpl.class 文件:

從代碼中可以看出 MapStruct 爲我們自動生成了 set/get 代碼,並且對枚舉類進行了特殊處理。

5. 客戶端

@Test
public void shouldMapCarToDto() {

    Car car = new Car( "Morris", 5, CarType.SEDAN );
    CarDto carDto = CarMapper.INSTANCE.carToCarDto( car );
    System.out.println(carDto);
    
}

執行結果:


小結: MapStruct 基於 mapper 接口,在編譯期動態生成 set/get 代碼的 class 文件 ,在運行時直接調用該 class 文件。

MapStruct 配置

@Mapper

我們翻開上邊提到的 Mapper 註釋的源碼,該註釋的解釋是:將接口或抽象類標記爲映射器,並通過 MapStruct 激活該類型實現的生成。我們找到其中的 componentModel 屬性,默認值爲 default,它有四種值供我們選擇:

  • default:映射器不使用組件模型,實例通常通過 Mappers.getMapper(java.lang.Class)獲取;
  • cdi:生成的映射器是 application-scopedCDI bean,可以通過 @Inject 獲取;
  • spring:生成的映射器是 Spring bean,可以通過 @Autowired 獲取;
  • jsr330:生成的映射器被 @javax.inject.Named@Singleton 註釋,可以通過 @inject 獲取;

上邊我們用的就是默認的方法,當然我們也可以用 @Autowired 來引入接口依賴,此處不再舉例,有興趣的小夥伴可以自己試試!

另外我們可以看下 uses 屬性:可以通過定義其他類來完成字段轉換,接下來我們來個小例子演示一下:

1. 定義一個 CarVo.java 類

@Data
@NoArgsConstructor
@AllArgsConstructor
public class CarVo {

    private String make;
    private int seatCount;
    private boolean type;
}

2. 在 mapper 中定義一個 vo 轉爲 dto 的方法 CarDto carVoToCarDto(CarVo carVo);

當不加 uses 屬性時,查看編譯後生成的實現類

public CarDto carVoToCarDto(CarVo carVo) {
    if (carVo == null) {
        return null;
    } else {
        CarDto carDto = new CarDto();
        carDto.setMake(carVo.getMake());
        carDto.setSeatCount(carVo.getSeatCount());
        carDto.setType(String.valueOf(carVo.isType()));
        return carDto;
    }
}
  1. mapper 上增加 uses 屬性,並指定自定義的處理類,代碼如下:
@Mapper(uses = {BooleanStrFormat.class})
public interface CarMapper {
    ......
}

/**
* 自定義的轉換類
*/
@Component
public class BooleanStrFormat {
    public String toStr(boolean type) {
        if(type){
            return "Y";
        }else{
            return "N";
        }
    }

    public boolean toBoolean(String type) {
        if (type.equals("Y")) {
            return true;
        } else {
            return false;
        }
    }
}

/**
* 查看編譯後生成的實現類
*/
public CarDto carVoToCarDto(CarVo carVo) {
    if (carVo == null) {
        return null;
    } else {
        CarDto carDto = new CarDto();
        carDto.setMake(carVo.getMake());
        carDto.setSeatCount(carVo.getSeatCount());
        //調用自定義的類中的方法
        carDto.setType(this.booleanStrFormat.toStr(carVo.isType()));
        return carDto;
    }
}

4.客戶端代碼

@Test
public void shouldMapCarVoToDto() {

    CarVo carVo = new CarVo( "Morris", 5, false );
    CarDto carDto = CarMapper.INSTANCE.carVoToCarDto( carVo );

    System.out.println(carDto);
}

執行結果:


@Mapping

@Mapping 可以用來配置一個 bean 屬性或枚舉常量的映射,默認是將具有相同名稱的屬性進行映射,當然也可以用 sourceexpression 或者 constant 屬性手動指定,接下來我們來分析下常用的屬性值。

  1. target:屬性的目標名稱,同一目標屬性不能映射多次。如果用於映射枚舉常量,則將給出常量成員的名稱,在這種情況下,源枚舉中的多個值可以映射到目標枚舉的相同值。
  2. source:屬性的源名稱,
  • 如果帶註釋的方法有多個源參數,則屬性名稱必須使用參數名稱限定,例如“addressParam.city"
  • 當找不到匹配的屬性時,MapStruct 將查找匹配的參數名稱;
  • 當用於映射枚舉常量時,將給出常量成員的名稱;
  • 該屬性不能與 constantexpression 一起使用;
  1. dateFormat:通過 SimpleDateFormat 實現 StringDate 日期之間相互轉換。
  2. numberFormat:通過 DecimalFormat 實現 NumberString 的數值格式化。
  3. constant:設置指定目標屬性的常量字符串,當指定的目標屬性的類型爲:primitiveboxed(例如 Long)時,MapStruct 檢查是否可以將該 primitive 作爲有效的文本分配給 primitiveboxed 類型。如果可能,MapStruct 將分配爲文字;如果不可能,MapStruct 將嘗試應用用戶定義的映射方法。
    另外,MapStruct 將常量作爲字符串處理,將通過應用匹配方法、類型轉換方法或內置轉換來轉換該值。此屬性不能與 sourcedefaultValuedefaultExpressionexpression 一起使用。
  4. expression:是一個表達式,根據該表達式設置指定的目標屬性。他的屬性不能與 sourcedefaultValuedefaultExpressionconstant 一起使用。
  5. ignore: 忽略這個字段。

我們用 expression 這個屬性來實現一下上邊用 uses 實現的案例:

1. 在 mapper 中定義方法

@Mapping(target = "type", expression = "java(new com.ittest.controller.BooleanStrFormat().toStr(carVo.isType()))")
CarDto carVoToDtoWithExpression(CarVo carVo);

2. 生成的實現類

@Override
public CarDto carVoToDtoWithExpression(CarVo carVo) {
    if ( carVo == null ) {
        return null;
    }

    CarDto carDto = new CarDto();

    carDto.setMake( carVo.getMake() );
    carDto.setSeatCount( carVo.getSeatCount() );

    carDto.setType( new com.ittest.controller.BooleanStrFormat().toStr(carVo.isType()) );

    return carDto;
}

3. 客戶端

@Test
public void mapCarVoToDtoWithExpression() {

    CarVo carVo = new CarVo( "Morris", 5, false );
    CarDto carDto = CarMapper.INSTANCE.carVoToDtoWithExpression( carVo );

    System.out.println(carDto);
}

運行結果:


至於其他的用法大家可以多多探索。

重要提示:枚舉映射功能已被棄用,並被 ValueMapping 取代。它將在後續版本中刪除。

@Mappings

可以配置多個 @Mapping,例如

@Mappings({
    @Mapping(source = "id", target = "carId"),
    @Mapping(source = "name", target = "carName"),
    @Mapping(source = "color", target = "carColor")
})

@MappingTarget

用於更新已有對象,還是用例子來說明吧:

1. 創建 BMWCar.java 類

@NoArgsConstructor
@AllArgsConstructor
@Data
public class BMWCar {
    private String make;
    private int numberOfSeats;
    private CarType type;

    private String color;
    private String price;

}

2. mapper 中創建更新方法,並查看實現類

// 更新方法
void updateBwmCar(Car car, @MappingTarget BMWCar bwmCar);

// 實現類
public void updateBwmCar(Car car, BMWCar bwmCar) {
    if (car != null) {
        bwmCar.setMake(car.getMake());
        bwmCar.setNumberOfSeats(car.getNumberOfSeats());
        bwmCar.setType(car.getType());
    }
}

3. 客戶端代碼

@Test
public void updateBwmCar() {
    Car car = new Car( "Morris", 5, CarType.SEDAN );
    BMWCar bwmCar = new BMWCar("BWM", 5, CarType.SPORTS, "RED", "50w");
    System.out.println("更新前 car:"+car.toString());
    System.out.println("更新前 BWMCar:"+bwmCar.toString());

    CarMapper.INSTANCE.updateBwmCar(car, bwmCar);

    System.out.println("更新後 car:"+car.toString());
    System.out.println("更新後 BWMCar:"+bwmCar.toString());
}

執行結果:


擴展:多個對象映射一個對象

1. 準備實體類 Benz4SMall.javaMall4S.java

@NoArgsConstructor
@AllArgsConstructor
@Data
public class Mall4S {

    private String address;

    private String mobile;

}

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Benz4SMall {

    private String address;
    private String mobile;
    private String make;
    private int numberOfSeats;
}

2. mapper 創建轉換方法並查看生成的實現類

Benz4SMall mallCarToBenzMall(Car car, Mall4S mall4S);

/**
* 實現類
*/
public Benz4SMall mallCarToBenzMall(Car car, Mall4S mall4S) {
    if (car == null && mall4S == null) {
        return null;
    } else {
        Benz4SMall benz4SMall = new Benz4SMall();
        if (car != null) {
            benz4SMall.setMake(car.getMake());
            benz4SMall.setNumberOfSeats(car.getNumberOfSeats());
        }

        if (mall4S != null) {
            benz4SMall.setAddress(mall4S.getAddress());
            benz4SMall.setMobile(mall4S.getMobile());
        }

        return benz4SMall;
    }
}

3. 客戶端

@Test
public void mallCarToBenzMall() {
    Car car = new Car( "Morris", 5, CarType.SEDAN );
    Mall4S mall4S = new Mall4S("北京市", "135XXXX4503");
    Benz4SMall benz4SMall = CarMapper.INSTANCE.mallCarToBenzMall(car, mall4S);
    System.out.println(benz4SMall.toString());
}

執行結果


深拷貝與淺拷貝

深拷貝和淺拷貝最根本的區別在於是否真正獲取一個對象的複製實體,而不是引用。

假設 B 複製了 A ,修改 A 的時候,看 B 是否發生變化:如果 B 跟着也變了,說明是淺拷貝,拿人手短!(修改堆內存中的同一個值);如果 B 沒有改變,說明是深拷貝,自食其力!(修改堆內存中的不同的值)

MapStruct 中是創建新的對象,也就是深拷貝

MapStruct 與其他 Copy 的對比

我們在平時的項目中經常會使用到拷貝的功能,今天我們就將他們做一下對比,直接拋出 ZhaoYingChao88 大佬的實驗結果:

輸出結果:手動Copy >Mapstuct>= cglibCopy > springBeanUtils > apachePropertyUtils > apacheBeanUtils 可以理解爲: 手工複製 > cglib > 反射 > Dozer

根據測試結果,我們可以得出在速度方面,MapStruct 是最好的,執行速度是 Apache BeanUtils 的10倍、Spring BeanUtils 的 4-5倍、和 BeanCopier 的速度差不多。

總結:在大數據量級的情況下,MapStructBeanCopier 都有着較高的性能優勢,其中 MapStruct 尤爲優秀。如果你僅是在日常處理少量的對象時,選取哪個其實變得並不重要,但數據量大時建議還是使用 MapStructBeanCopier 的方式,提高接口性能。

參考鏈接:https://blog.csdn.net/ZYC88888/article/details/109681423?spm=1001.2014.3001.5501

回覆“mapstruct”,即可獲取源碼呦!

以上就是今天的全部內容了,如果你有不同的意見或者更好的idea,歡迎聯繫阿Q,添加阿Q qingqing-4132 可以加入技術交流羣參與討論呦!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章