目錄概述定義實體類CarsizecarInfo造測試數據Spring BeanUtilsApache BeanUtilsCglib BeanCopierMapStruct性能測試深拷貝or淺拷貝 概述 眾所周知,java世界是由類構成的,各種各樣的類,提供各種各樣的作用,共同創造了一個個的java應 ...
目錄
概述
眾所周知,java世界是由類
構成的,各種各樣的類,提供各種各樣的作用,共同創造了一個個的java應用。對象是類的實例,在SpringBoot框架中,對象經常需要拷貝,例如資料庫實體拷貝成業務實體,導入實體轉換為業務實體,各種數據傳輸對象之間的拷貝等等。日常開發工作中用到的地方和頻率是相當的高。本文就圍繞對象拷貝
來聊聊常用的姿勢
(方式)和工具
。
定義實體類
為了演示對象拷貝將創建幾個實體類和幾個生成測試數據的方法。
Car
car描述了車輛這個業務對象, 其中包含一些常見的基本數據類型的屬性和一個 size類型 的屬性,即包含基本數據類型和引用類型,包含子實體。
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
@Data
@Accessors(chain = true)
public class Car implements Serializable {
private Integer id;
private String name;
private String brand;
private String address;
private Size size;
private Double cc;
/**
* 扭矩
*/
private Double torque;
/**
* 廠商
*/
private String manufacturer;
/**
* 上市時間
*/
private LocalDate marketDate;
/**
* 售價
*/
private BigDecimal price;
}
size
size描述了車輛大小,具體來說就是長寬高。
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
@Data
@Accessors(chain = true)
public class Size implements Serializable {
private Double length;
private Double width;
private Double height;
}
carInfo
car對象需要拷貝為carInfo對象,他們兩個大部分屬性都一樣,carInfo比car多了 color ,type
package com.ramble.demo.dto;
import com.ramble.demo.model.Size;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
@Data
@Accessors(chain = true)
public class CarInfo implements Serializable {
private Integer id;
private String name;
private String brand;
private String address;
private Size size;
private Double cc;
/**
* 扭矩
*/
private Double torque;
/**
* 廠商
*/
private String manufacturer;
/**
* 上市時間
*/
private LocalDate marketDate;
/**
* 售價
*/
private BigDecimal price;
private String color;
private Integer type;
}
造測試數據
造測試數據用到了一個工具 javaFaker,通過實例化一個Faker 對象可以輕易的生成測試數據:人名、地址、網址、手機號。。。。。。
gav坐標為:
<dependency>
<groupId>com.github.javafaker</groupId>
<artifactId>javafaker</artifactId>
<version>1.0.2</version>
</dependency>
造數據方法很簡單,一個批量一個單個,可以通過count來控製造的數據量。
/**
* 批量造數據
*
* @return
*/
private List<Car> findCar() {
Faker faker = new Faker(new Locale("zh-CN"));
List<Car> list = new ArrayList<>();
int count = 100000;
for (int i = 0; i < count; i++) {
list.add(getCar(faker));
}
return list;
}
/**
* 造數據 - 單個
*
* @param faker
* @return
*/
private Car getCar(Faker faker) {
Car car = new Car();
car.setId(faker.number().numberBetween(1, 999999999));
car.setName(faker.name().name());
car.setBrand(faker.cat().breed());
car.setAddress(faker.address().fullAddress());
Size size = new Size();
size.setLength(faker.number().randomDouble(7, 4000, 7000));
size.setWidth(faker.number().randomDouble(7, 4000, 7000));
size.setHeight(faker.number().randomDouble(7, 4000, 7000));
car.setSize(size);
car.setCc(faker.number().randomDouble(7, 1000, 12000));
car.setTorque(faker.number().randomDouble(1, 100, 70000));
car.setManufacturer(faker.name().name());
Date date = faker.date().birthday();
Instant instant = date.toInstant();
ZoneId zone = ZoneId.systemDefault();
LocalDate localDate = instant.atZone(zone).toLocalDate();
car.setMarketDate(localDate);
car.setPrice(BigDecimal.valueOf(faker.number().randomDigit()));
return car;
}
Spring BeanUtils
這麼常用的功能官方肯定已經集成了,對對對,就是BeanUtils.copyProperties了。順便說一句,遇到找工具類的時候不要盲目baidu,不妨先看看springframework.util這個包下麵的東西。
下麵看看Spring的BeanUtils.copyProperties用法舉例
Faker faker = new Faker(new Locale("zh-CN"));
Car car = getCar(faker);
CarInfo carInfo = new CarInfo();
BeanUtils.copyProperties(car, carInfo);
- 使用反射實現兩個對象的拷貝
- 不會對類型進行轉換,如source對象有一個integer類型的id屬性,target對象有一個string類型的id屬性,拷貝之後,target對象為null;同理integer 無法拷貝到long
- 官方出品,無需引入其他依賴,推薦指數8顆星
Apache BeanUtils
apache-common系列的東西,java開發多多少少會用到一些,它本身也積累了很多經驗提供一些有用並常用的工具和組件。
針對BeanUtils 使用前需要引入pom
<!-- https://mvnrepository.com/artifact/commons-beanutils/commons-beanutils -->
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.9.4</version>
</dependency>
下麵看看Apache的BeanUtils.copyProperties 用法舉例
Faker faker = new Faker(new Locale("zh-CN"));
Car car = getCar(faker);
CarInfo carInfo = new CarInfo();
try {
org.apache.commons.beanutils.BeanUtils.copyProperties(carInfo, car);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
}
- 強制要處理異常
- source對象和target對象相對Spring來說是顛倒的
- 通過反射實現
- 不會對類型進行轉換,如source對象有一個integer類型的id屬性,target對象有一個string類型的id屬性,拷貝之後,target對象為null;同理integer 無法拷貝到long
- 性能稍微遜色與Spring,推薦指數7顆星
Cglib BeanCopier
BeanCopier使用起來稍微有點複雜,需要先實例化一個BeanCopier對象,然後再調用其copy方法完成對象拷貝
使用前需要引入如下pom
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.2.0</version>
</dependency>
下麵看看 Cglib 的 BeanCopier 用法舉例
Faker faker = new Faker(new Locale("zh-CN"));
Car car = getCar(faker);
CarInfo item = new CarInfo();
inal BeanCopier copier = BeanCopier.create(Car.class, CarInfo.class, false);
copier.copy(car, item, null);
- BeanCopier雖然使用複雜,且要引入依賴,但是性能出眾
- 對於數據量小的情況不沒有必要使用這個,推薦指數5顆星
- 對於數據量大,比如5W個對象拷貝為另外一個對象,這種場景雖然少但是一旦發生了Spring BeanUtils處理起來就非常耗時,這種情況下推薦指數9顆星
MapStruct
MapStruct 是一個代碼生成器,它基於約定生成的映射代碼使用的是普通的方法調用,因此快速、類型安全且易於理解。MapStruct 本質沒有太多科技與狠活,就是幫助開發人員編寫getA,setB這樣的樣板代碼,並提供擴展點,比如將carType映射為type。
使用前需要引入如下pom
<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>
下麵看看MapStruct用法舉例
使用前需要先定義一個介面,編寫轉換邏輯,而後通過調用介面方法獲取轉換後的結果。
CarInfoMapper
import com.ramble.demo.dto.CarInfo;
import com.ramble.demo.model.Car;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
@Mapper
public interface CarInfoMapper {
CarInfoMapper INSTANCT = Mappers.getMapper(CarInfoMapper.class);
CarInfo carToCarInfo(Car car);
}
- Mapper:將此介面標註為用來轉換bean的介面,併在編譯過程中生成相應的實現類
- carToCarInfo,聲明一個轉換方法,並將source作為方法入參,target作為方法出參
- INSTANCT,按照約定,介面聲明一個INSTANCT成員,提供給訪問者消費
- 一個介面可以有多個轉換方法
- 可通過在方法上添加@Mapping註解,將不同屬性名子的屬性進行轉換
- 可以將枚舉類型轉換為字元串
- 性能出眾,術業有專攻,在處理複雜轉換和大數據量的時候很有必要
- 因為要引入依賴並且需要單獨編寫介面,所以對於簡單的轉換還是推薦Spring,這種情況下推薦指數5顆星
- 如果在大數據量的情況,並且轉換相對複雜,推薦指數10顆星
調用 CarInfoMapper 進行對象拷貝
Faker faker = new Faker(new Locale("zh-CN"));
Car car = getCar(faker);
CarInfo carInfo = CarInfoMapper.INSTANCT.carToCarInfo(car);
性能測試
原則上, 性能測試是多維度的,速度、CPU消耗、記憶體消耗等等,本文僅考慮速度,只為說明問題,不為得到測試數據
測試代碼邏輯,使用for結合faker生成一個大的List
測試代碼如下:
/**
* 測試4種方式的處理速度
*/
@GetMapping("/test1")
public void test1() {
Faker faker = new Faker(new Locale("zh-CN"));
List<Car> carList = findCar();
StopWatch sw = new StopWatch("對象拷貝速度測試");
sw.start("方式1:Spring BeanUtils.copyProperties");
List<CarInfo> list1 = new ArrayList<>();
carList.forEach(x -> {
CarInfo item = new CarInfo();
BeanUtils.copyProperties(x, item);
item.setColor(faker.color().name());
item.setType(faker.number().randomDigit());
list1.add(item);
});
sw.stop();
sw.start("方式2:apache BeanUtils.copyProperties");
List<CarInfo> list2 = new ArrayList<>();
carList.forEach(x -> {
CarInfo item = new CarInfo();
try {
org.apache.commons.beanutils.BeanUtils.copyProperties(item, x);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
}
item.setColor(faker.color().name());
item.setType(faker.number().randomDigit());
list2.add(item);
});
sw.stop();
sw.start("方式3:BeanCopier");
List<CarInfo> list3 = new ArrayList<>();
carList.forEach(x -> {
CarInfo item = new CarInfo();
final BeanCopier copier = BeanCopier.create(Car.class, CarInfo.class, false);
copier.copy(x, item, null);
item.setColor(faker.color().name());
item.setType(faker.number().randomDigit());
list3.add(item);
});
sw.stop();
sw.start("方式4:MapStruct");
List<CarInfo> list4 = new ArrayList<>();
carList.forEach(x -> {
CarInfo item = CarInfoMapper.INSTANCT.carToCarInfo(x);
item.setColor(faker.color().name());
item.setType(faker.number().randomDigit());
list4.add(item);
});
sw.stop();
String s = sw.prettyPrint();
System.out.println(s);
double totalTimeSeconds = sw.getTotalTimeSeconds();
System.out.println("總耗時:" + totalTimeSeconds);
}
測試結果如下
10W 數據量
StopWatch '對象拷貝速度測試': running time = 5382233200 ns
---------------------------------------------
ns % Task name
---------------------------------------------
2336099000 043% 方式1:Spring BeanUtils.copyProperties
2322316400 043% 方式2:apache BeanUtils.copyProperties
442981700 008% 方式3:BeanCopier
280836100 005% 方式4:MapStruct
總耗時:5.3822332
100W 數據量
StopWatch '對象拷貝速度測試': running time = 58689078300 ns
---------------------------------------------
ns % Task name
---------------------------------------------
25199532100 043% 方式1:Spring BeanUtils.copyProperties
27179965900 046% 方式2:apache BeanUtils.copyProperties
3629756500 006% 方式3:BeanCopier
2679823800 005% 方式4:MapStruct
總耗時:58.6890783
深拷貝or淺拷貝
上述四種方式均為淺拷貝。可通過如下測試代碼驗證
/**
* 測試4種方式是淺拷貝還是深拷貝
*/
@GetMapping("/test2")
public void test2() {
Faker faker = new Faker(new Locale("zh-CN"));
Car car = getCar(faker);
log.info("car={}", JSON.toJSONString(car));
CarInfo carInfo = new CarInfo();
//方式1
// BeanUtils.copyProperties(car, carInfo);
// Size sizeNew = car.getSize();
// sizeNew.setLength(0d);
// sizeNew.setWidth(0d);
// sizeNew.setHeight(0d);
//// Size s = new Size();
//// s.setHeight(10d);
//// car.setSize(s);
// car.setName("新名字");
// log.info("carInfo={}", JSON.toJSONString(carInfo));
//方式2
// try {
// org.apache.commons.beanutils.BeanUtils.copyProperties(carInfo, car);
// } catch (IllegalAccessException e) {
// throw new RuntimeException(e);
// } catch (InvocationTargetException e) {
// throw new RuntimeException(e);
// }
// Size sizeNew = car.getSize();
// sizeNew.setLength(0d);
// sizeNew.setWidth(0d);
// sizeNew.setHeight(0d);
//
// car.setName("新名字");
// log.info("carInfo={}", JSON.toJSONString(carInfo));
//方式3
// final BeanCopier copier = BeanCopier.create(Car.class, CarInfo.class, false);
// copier.copy(car, carInfo, null);
// Size sizeNew = car.getSize();
// sizeNew.setLength(0d);
// sizeNew.setWidth(0d);
// sizeNew.setHeight(0d);
//
// car.setName("新名字");
// log.info("carInfo={}", JSON.toJSONString(carInfo));
//方式4
carInfo = CarInfoMapper.INSTANCT.carToCarInfo(car);
Size sizeNew = car.getSize();
sizeNew.setLength(0d);
sizeNew.setWidth(0d);
sizeNew.setHeight(0d);
car.setName("新名字");
log.info("carInfo={}", JSON.toJSONString(carInfo));
}
驗證邏輯為,在完成copy後修改car.size 的值,發現carInfo.size隨之改變,說明是淺拷貝。
郵箱:[email protected] 技術交流QQ群:1158377441 歡迎關註我的微信公眾號【TechnologyRamble】,後續博文將在公眾號首發: