一曹傀、基本介紹
屬性拷貝工具有很多,也許你用過如下的一些:
- Apache commons-beanutils
- Spring BeanUtils
- cglib BeanCopier
- HuTool BeanUtils
- MapStruct
- getter & setter
這些屬性拷貝工具各自有什么特點(diǎn)和區(qū)別而昨?在日常開發(fā)使用中谍婉,我們該如何做出選擇?
1.1 Apache BeanUtils
- 參數(shù)順序和其它的工具正好相反,導(dǎo)致使用不順手帮孔,容易產(chǎn)生問題;
- 阿里巴巴代碼掃描插件會給出明確的告警不撑;
- 基于反射實(shí)現(xiàn)文兢,性能較差;
- 不推薦使用焕檬;
1.2 Spring BeanUtils
- 基于內(nèi)省+反射,借助getter/setter方法實(shí)現(xiàn)屬性拷貝兼呵,性能比apache高腊敲;
- 在簡單的屬性拷貝場景下推薦使用击喂;
1.3 cglib BeanCopier
- 通過動態(tài)代理的方式來實(shí)現(xiàn)屬性拷貝;
- 性能高效懂昂;
- 在簡單的屬性拷貝場景下推薦使用没宾;
1.4 HuTool BeanUtils
- 性能介于apache和Spring之間凌彬;
- 需要額外引入HuTool的依賴沸柔;
1.5 MapStruct
- 基于getter/setter方法實(shí)現(xiàn)屬性拷貝铲敛,在編譯時自動生成實(shí)現(xiàn)類的代碼;
- 性能媲美getter & setter乱凿;
- 強(qiáng)大的功能可以實(shí)現(xiàn)深度拷貝咽弦;
- 缺點(diǎn)是需要聲明bean的轉(zhuǎn)換接口類胁出;
1.6 getter & setter
- 性能最高全蝶,但是需要手動拷貝;
1.7 總結(jié)
經(jīng)過第三方的對比結(jié)果绷落,總的下來始苇,推薦使用順序?yàn)椋?/p>
apache < HuTool < Spring < cglib < Mapstruct
二催式、使用介紹
2.1 準(zhǔn)備工作
<!-- 導(dǎo)入MapStruct的核心注釋 -->
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
</dependencies>
...
<!-- MapStruct在編譯時工作荣月,并且會集成到像Maven和Gradle這樣的構(gòu)建工具上,我們還必須在<build中/>標(biāo)簽中添加一個插件maven-compiler-plugin捐下,并在其配置中添加annotationProcessorPaths萌业,該插件會在構(gòu)建時生成對應(yīng)的代碼咽白。 -->
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
2.2 映射
2.2.1 基本映射
我們現(xiàn)在需要實(shí)現(xiàn)一個場景,Car是一個domain層的對象實(shí)例排抬,在從數(shù)據(jù)庫讀取出來后傳遞給service層需要轉(zhuǎn)換為CarDTO蹲蒲,這兩個實(shí)例的所有屬性全部相同,現(xiàn)在需要使用mapstruct來完成這個目標(biāo)缘薛。
public class Car {
private String brand;
private Double price;
private Boolean onMarket;
...
// setters + getters + toString
}
public class CarDTO {
private String brand;
private Double price;
private Boolean onMarket;
...
// setters + getters + toString
}
我們需要新建一個Mapper接口宴胧,來映射這兩個對象之間的屬性表锻。
@Mapper
public interface CarMapper {
CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
CarDTO carToCarDTO(Car car);
}
然后就可以進(jìn)行測試了:
@Test
public void test1(){
Car wuling = new Car();
wuling.setBrand("wuling");
wuling.setPrice(6666.66);
wuling.setOnMarket(true);
CarDTO wulingDTO = CarMapper.INSTANCE.carToCarDTO(wuling);
// 結(jié)果為:Car{brand='wuling', price=6666.66, onMarket=true}
System.out.println("結(jié)果為:" + wulingDTO);
}
可以看到瞬逊,mapstruct很好地完成了我們的目標(biāo),那么它是如何做到的呢士骤?我們查看CarMapper.INSTANCE.carToCarDTO(wuling)
的實(shí)現(xiàn)類拷肌,可以看到在編譯過程中自動生成了如下內(nèi)容的接口實(shí)現(xiàn)類:
public class CarMapperImpl implements CarMapper {
@Override
public CarDTO carToCarDTO(Car car) {
if ( car == null ) {
return null;
}
CarDTO carDTO = new CarDTO();
carDTO.setBrand( car.getBrand() );
carDTO.setPrice( car.getPrice() );
carDTO.setOnMarket( car.getOnMarket() );
return carDTO;
}
}
所以束铭,mapstruct并沒有使用反射的機(jī)制契沫,而是使用了普通的set和get方法來進(jìn)行屬性拷貝的,因此要求我們的對象也一定要有set和get方法拴清。
2.2.2 不同屬性名映射
在如上示例中会通,我們源對象和目標(biāo)對象的屬性名稱全都一致涕侈,但是在很多的場景下,源對象和目標(biāo)對象的同一個字段很可能名稱是不同的木张,這種情況下舷礼,只需要在映射接口類中指定即可:
public class Car {
...
private Boolean onMarket;
...
// setters + getters + toString
}
public class CarDTO {
...
private Boolean onSale;
...
// setters + getters + toString
}
@Mapper
public interface CarMapper {
CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
@Mapping(source = "car.onMarket", target = "onSale")
CarDTO carToCarDTO(Car car);
}
如此,生成的接口實(shí)現(xiàn)類如下:
@Override
public CarDTO carToCarDTO(Car car) {
if ( car == null ) {
return null;
}
CarDTO carDTO = new CarDTO();
carDTO.setOnSale( car.getOnMarket() );
carDTO.setBrand( car.getBrand() );
carDTO.setPrice( car.getPrice() );
return carDTO;
}
2.2.3 不同個數(shù)屬性映射
我們假設(shè)Car和CarDTO各有一個對方?jīng)]有的屬性蛛株,那么在進(jìn)行對象拷貝時會發(fā)生什么谨履?
public class Car {
...
private Date birthdate;
// setters + getters + toString
}
public class CarDTO {
...
private String owner;
// setters + getters + toString
}
@Test
public void test1(){
Car wuling = new Car();
wuling.setBrand("wuling");
wuling.setPrice(6666.66);
wuling.setOnMarket(true);
wuling.setBirthdate(new Date());
CarDTO wulingDTO = CarMapper.INSTANCE.carToCarDTO(wuling);
System.out.println("結(jié)果為:" + wulingDTO);
}
然后我們執(zhí)行如上轉(zhuǎn)換的案例屉符,發(fā)現(xiàn)并沒有報錯锹引,從Car拷貝屬性到CarDTO時嫌变,CarDTO由于沒有birthdate屬性躬它,則不會賦值冯吓;同時,CarDTO的owner因?yàn)镃ar中沒有凸舵,因此也不會被賦值失尖,生成的接口實(shí)現(xiàn)類如下:
@Override
public CarDTO carToCarDTO(Car car) {
if ( car == null ) {
return null;
}
CarDTO carDTO = new CarDTO();
carDTO.setOnSale( car.getOnMarket() );
carDTO.setBrand( car.getBrand() );
carDTO.setPrice( car.getPrice() );
return carDTO;
}
因此掀潮,mapstruct只會對共有的交集屬性進(jìn)行拷貝操作。
2.2.4 多個源合并映射
我們新增一個Person類仪吧,其中的name屬性對應(yīng)CarDTO中的owner屬性。
public class Person {
private String name;
private String age;
// setters + getters + toString
}
@Mapper
public interface CarMapper {
CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
@Mapping(source = "car.onMarket", target = "onSale")
@Mapping(source = "person.name", target = "owner")
CarDTO carToCarDTO(Car car, Person person);
}
public class TestMapper1 {
@Test
public void test1(){
Car wuling = new Car();
wuling.setBrand("wuling");
wuling.setPrice(6666.66);
wuling.setOnMarket(true);
wuling.setBirthdate(new Date());
Person jack = new Person();
jack.setName("jack");
jack.setAge("22");
CarDTO wulingDTO = CarMapper.INSTANCE.carToCarDTO(wuling, jack);
// 結(jié)果為:CarDTO{brand='wuling', price=6666.66, onSale=true, owner='jack'}
System.out.println("結(jié)果為:" + wulingDTO);
}
自動生成的接口實(shí)現(xiàn)類如下:
@Override
public CarDTO carToCarDTO(Car car, Person person) {
if ( car == null && person == null ) {
return null;
}
CarDTO carDTO = new CarDTO();
if ( car != null ) {
carDTO.setOnSale( car.getOnMarket() );
carDTO.setBrand( car.getBrand() );
carDTO.setPrice( car.getPrice() );
}
if ( person != null ) {
carDTO.setOwner( person.getName() );
}
return carDTO;
}
2.2.5 子對象映射
如果需要轉(zhuǎn)換的Car對象中的某個屬性不是基本數(shù)據(jù)類型,而是一個對象怎么處理呢吭从。
public class Person {
private String name;
private String age;
// setters + getters + toString
}
public class PersonDTO {
private String name;
private String age;
// setters + getters + toString
}
public class Car {
private String brand;
private Double price;
private Boolean onMarket;
private Person owner;
// setters + getters + toString
}
public class CarDTO {
private String brand;
private Double price;
private Boolean onMarket;
private PersonDTO owner;
// setters + getters + toString
}
@Mapper
public interface PersonMapper {
PersonMapper INSTANCE = Mappers.getMapper(PersonMapper.class);
PersonDTO personToPersonDTO(Person person);
}
@Mapper(uses = {PersonMapper.class})
public interface CarMapper {
CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
@Mapping(source = "onMarket", target = "onSale")
CarDTO carToCarDTO(Car car);
}
@Test
public void test1(){
Person jack = new Person();
jack.setName("jack");
jack.setAge("22");
Car wuling = new Car();
wuling.setBrand("wuling");
wuling.setPrice(6666.66);
wuling.setOnMarket(true);
wuling.setOwner(jack);
CarDTO wulingDTO = CarMapper.INSTANCE.carToCarDTO(wuling);
// 結(jié)果為:CarDTO{brand='wuling', price=6666.66, onSale=true, owner=Person{name='jack', age='22'}}
System.out.println("結(jié)果為:" + wulingDTO);
}
這里最重要的是:
- 需要增加PersonMapper接口谱醇,讓mapstruct能夠?qū)erson和PersonDTO進(jìn)行轉(zhuǎn)換步做;
- CarMapper中需要引入PersonMapper全度,如果存在多個對象屬性,此處就要引入多個對象屬性的Mapper接口勉盅;
2.2.6 集合屬性映射
如果需要轉(zhuǎn)換的Car對象中的某個屬性不是基本數(shù)據(jù)類型顶掉,而是一個集合類型該怎么處理痒筒?
public class Car {
private String brand;
private Double price;
private Boolean onMarket;
private List<Person> ownerList;
// setters + getters + toString
}
同2.2.5內(nèi)容。
2.2.7 枚舉映射
枚舉映射的工作方式與字段映射相同移袍。MapStruct會對具有相同名稱的枚舉進(jìn)行映射葡盗。
public enum PayType {
CASH,
ALIPAY,
WEPAY,
DIGITAL_CASH,
CARD_VISA,
CARD_CREDIT;
}
public enum PayTypeNew {
CASH,
ALIPAY,
WEPAY,
DIGITAL_CASH,
CARD_VISA,
CARD_CREDIT;
}
@Mapper
public interface PayTypeMapper {
PayTypeMapper INSTANCE = Mappers.getMapper(PayTypeMapper.class);
PayTypeNew payTypeToPayTypeNew(PayType payType);
}
@Test
public void test2(){
PayType p1 = PayType.ALIPAY;
PayTypeNew p2 = PayTypeMapper.INSTANCE.payTypeToPayTypeNew(p1);
// 結(jié)果為:ALIPAY
System.out.println("結(jié)果為:" + p2);
}
但是在更多的場景下戳粒,源枚舉和目標(biāo)枚舉并不是一一對應(yīng)的虫啥,比如目標(biāo)枚舉如下:
public enum PayTypeNew {
CASH,
NETWORK,
CARD;
}
此時涂籽,我們就需要手動指定源枚舉和目標(biāo)枚舉之間的對應(yīng)關(guān)系:
@Mapper
public interface PayTypeMapper {
PayTypeMapper INSTANCE = Mappers.getMapper(PayTypeMapper.class);
@ValueMappings({
@ValueMapping(source = "ALIPAY", target = "NETWORK"),
@ValueMapping(source = "WEPAY", target = "NETWORK"),
@ValueMapping(source = "DIGITAL_CASH", target = "CASH"),
@ValueMapping(source = "CARD_VISA", target = "CARD"),
@ValueMapping(source = "CARD_CREDIT", target = "CARD")
})
PayTypeNew payTypeToPayTypeNew(PayType payType);
}
如果對應(yīng)CARD的場景比較多,手動一個個地對應(yīng)會比較繁瑣树枫,因此還有一種方式能實(shí)現(xiàn)相同的效果砂轻,而且比較簡潔:
@Mapper
public interface PayTypeMapper {
PayTypeMapper INSTANCE = Mappers.getMapper(PayTypeMapper.class);
@ValueMappings({
@ValueMapping(source = "ALIPAY", target = "NETWORK"),
@ValueMapping(source = "WEPAY", target = "NETWORK"),
@ValueMapping(source = "DIGITAL_CASH", target = "CASH"),
@ValueMapping(source = MappingConstants.ANY_REMAINING, target = "CARD")
})
PayTypeNew payTypeToPayTypeNew(PayType payType);
}
MappingConstants.ANY_REMAINING
表示剩下其它的源枚舉和目標(biāo)枚舉對應(yīng)不上的全部映射為指定的枚舉對象搔涝。
還有一種方式,使用MappingConstants.ANY_UNMAPPED
表示所有未顯示指定目標(biāo)枚舉的都會被映射為CARD:
@Mapper
public interface PayTypeMapper {
PayTypeMapper INSTANCE = Mappers.getMapper(PayTypeMapper.class);
@ValueMappings({
@ValueMapping(source = "ALIPAY", target = "NETWORK"),
@ValueMapping(source = "WEPAY", target = "NETWORK"),
@ValueMapping(source = "DIGITAL_CASH", target = "CASH"),
@ValueMapping(source = MappingConstants.ANY_UNMAPPED, target = "CARD")
})
PayTypeNew payTypeToPayTypeNew(PayType payType);
}
2.2.8 集合映射
如果源對象和目標(biāo)對象都是集合蜕煌,且對象中的屬性都是基本數(shù)據(jù)類型斜纪,則映射方法和之前類似文兑,映射接口改為如下即可:
@Mapper
public interface CarMapper {
CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
List<CarDTO> carToCarDTOList(List<Car> carList);
Set<CarDTO> carToCarDTOSet(Set<Car> carSet);
Map<String, CarDTO> carToCarDTOMap(Map<String, Car> carMap);
}
如果對象中屬性不僅是基本數(shù)據(jù)類型绿贞,還有對象類型或?qū)ο箢愋偷募项愋偷脑挘琺apstruct也是支持映射的,詳情參考官網(wǎng)文檔寨辩,此處不再贅述歼冰。
2.3 轉(zhuǎn)換
2.3.1 類型轉(zhuǎn)換
mapstruct提供了基本數(shù)據(jù)類型和包裝數(shù)據(jù)類型隔嫡、一些常見場景下的自動轉(zhuǎn)換;
- 基本類型及其對應(yīng)的包裝類型之間的轉(zhuǎn)換梢杭;比如int和Integer秸滴、float和Float、long和Long届垫、boolean和Boolean等全释;
- 任意基本類型和任意包裝類型之間的轉(zhuǎn)換浸船;比如int和long、byte和Integer等判族;
- 任意基本類型项戴、包轉(zhuǎn)類型和String之間的轉(zhuǎn)換周叮;比如boolean和String、Integer和String合冀;
- 枚舉和String君躺;
- 大數(shù)類型(BigInteger开缎、BigDecimal)、基本類型奕删、基本類型包裝類型完残、String之間的相互轉(zhuǎn)換;
- 其它一些場景熟掂,參考MapStruct 1.4.2.Final Reference Guide打掘;
2.3.2 格式轉(zhuǎn)換
mapstruct可以對源對象的屬性值進(jìn)行格式化之后拷貝給目標(biāo)對象的屬性;
-
日期格式轉(zhuǎn)換
public class Car { private String brand; private Double price; private LocalDate marketDate; // setters + getters + toString } public class CarDTO { private String brand; private Double price; private String saleDate; // setters + getters + toString }
@Mapper public interface CarMapper { CarMapper INSTANCE = Mappers.getMapper(CarMapper.class); @Mapping(source = "marketDate", target = "saleDate", dateFormat = "dd/MM/yyyy") CarDTO carToCarDTO(Car car); }
@Test public void test1(){ Car wuling = new Car(); wuling.setBrand("wuling"); wuling.setPrice(6666.66); wuling.setMarketDate(LocalDate.now()); // 轉(zhuǎn)換前為:Car{brand='wuling', price=6666.66, marketDate=2022-01-19} System.out.println("轉(zhuǎn)換前為:" + wuling); CarDTO wulingDTO = CarMapper.INSTANCE.carToCarDTO(wuling); // 結(jié)果為:CarDTO{brand='wuling', price=6666.66, saleDate='19/01/2022'} System.out.println("結(jié)果為:" + wulingDTO); }
-
數(shù)字格式轉(zhuǎn)換
@Mapper public interface CarMapper { CarMapper INSTANCE = Mappers.getMapper(CarMapper.class); @Mapping(source = "price", target = "price", numberFormat = "$#.00") CarDTO carToCarDTO(Car car); }
2.4 高級特性
2.4.1 依賴注入
在前面例子中的Mapper映射接口中,我們都需要CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
來創(chuàng)建一個實(shí)例仑乌,如果我們是Spring工程的話琴锭,就可以把這個實(shí)例托管給Spring進(jìn)行管理决帖。
@Mapper(componentModel = "spring")
public interface CarMapper {
List<CarDTO> carToCarDTOList(List<Car> carList);
}
然后使用的時候地回,從Spring中自動注入接口對象即可:
@SpringBootTest
public class TestMapper1 {
@Autowired
private CarMapper carMapper;
@Test
public void test1(){
Car wuling = new Car();
wuling.setBrand("wuling");
wuling.setPrice(6666.66);
wuling.setMarketDate(LocalDate.now());
Car changan = new Car();
changan.setBrand("changan");
changan.setPrice(7777.77);
changan.setMarketDate(LocalDate.now());
List<Car> carList = new ArrayList<>();
carList.add(wuling);
carList.add(changan);
List<CarDTO> carDTOList = carMapper.carToCarDTOList(carList);
System.out.println("結(jié)果為:" + carDTOList);
}
}
2.4.2 設(shè)置默認(rèn)值
-
常量默認(rèn)值
無論源對象的屬性字段值是什么刻像,目標(biāo)對象的該字段都是給定的常量值。
@Mapper(componentModel = "spring") public interface PersonMapper { @Mapping(target = "name", constant = "zhangsan") PersonDTO personToPersonDTO(Person person); }
-
空值默認(rèn)值
如果源對象的屬性字段值為空谷羞,那么就使用指定的默認(rèn)值湃缎。
@Mapper(componentModel = "spring") public interface PersonMapper { @Mapping(source = "name", target = "name", defaultValue = "unknown") PersonDTO personToPersonDTO(Person person); }
2.4.3 使用表達(dá)式
mapstruct甚至允許在對象屬性映射中使用java表達(dá)式:
@Mapper(componentModel = "spring", imports = {UUID.class, LocalDateTime.class})
public interface PersonMapper {
@Mapping(target = "id", expression = "java(UUID.randomUUID().toString())")
@Mapping(source = "birthdate", target = "birthdate", defaultExpression = "java(LocalDateTime.now())")
PersonDTO personToPersonDTO(Person person);
}
或者等價寫法為:
@Mapper(componentModel = "spring")
public interface PersonMapper {
@Mapping(target = "id", expression = "java(java.util.UUID.randomUUID().toString())")
@Mapping(source = "birthdate", target = "birthdate", defaultExpression = "java(java.time.LocalDateTime.now())")
PersonDTO personToPersonDTO(Person person);
}
2.4.4 前置及后置方法
@Mapper(componentModel = "spring")
public abstract class PersonMapper {
@BeforeMapping
public void before(Person person){
System.out.println("前置處理Q愀琛V小求妹!");
if(ObjectUtils.isEmpty(person.getName())){
System.out.println("Person的name不能為空佳窑!");
return;
}
}
@Mapping(target = "id", expression = "java(java.util.UUID.randomUUID().toString())")
@Mapping(source = "birthdate", target = "birthdate", defaultExpression = "java(java.time.LocalDateTime.now())")
public abstract PersonDTO personToPersonDTO(Person person);
@AfterMapping
public void after(@MappingTarget PersonDTO personDTO){
System.out.println("后置處理:" + personDTO.getName() + "!!!");
}
}
三神凑、參考文獻(xiàn)
MapStruct 1.4.2.Final Reference Guide
MapStruct使用指南 - 知乎 (zhihu.com)
常見Bean拷貝框架使用姿勢及性能對比 - 知乎 (zhihu.com)
四、補(bǔ)充填坑
在正文的實(shí)例中爱榕,我們對于Bean對象都是使用手動寫getter坡慌、setter、toString方法的跪者,但是在真實(shí)開發(fā)中渣玲,大家都是采用了Lombok插件弟晚,如果不做特殊配置指巡,就會出現(xiàn)mapstruct運(yùn)行時lombok不生效的問題,只需要在pom配置中增加如下內(nèi)容:
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
</annotationProcessorPaths>
具體原理和詳情可以參考:當(dāng) Lombok 遇見了 MapStruct の「坑」 - 知乎 (zhihu.com)