是時(shí)候丟掉 BeanUtils 了!
共 11300字,需瀏覽 23分鐘
·
2024-07-23 09:30
來源:cnblogs.com/jtea/p/17592696.html
前言
為了更好的進(jìn)行開發(fā)和維護(hù),我們都會(huì)對(duì)程序進(jìn)行分層設(shè)計(jì),例如常見的三層,四層,每層各司其職,相互配合。也隨著分層,出現(xiàn)了 VO,BO,PO,DTO,每層都會(huì)處理自己的數(shù)據(jù)對(duì)象,然后向上傳遞,這就避免不了經(jīng)常要將一個(gè)對(duì)象的屬性拷貝給另一個(gè)對(duì)象。
例如我有一個(gè) User 對(duì)象和一個(gè) UserVO 對(duì)象,要將 User 對(duì)象的10個(gè)屬性賦值個(gè) UserVO 的同名屬性:
-
一種方式是手寫,一個(gè)屬性一個(gè)屬性賦值,相信大家最開始學(xué)習(xí)時(shí)都是這么干的,這種方式就是太低效了。 -
在 idea 中可以安裝插件幫我們快速生成 set 屬性代碼,雖然還是逐個(gè)屬性賦值,但比一個(gè)個(gè)敲,效率提高了很多。
上面兩種方式雖然最原始,做起來很麻煩,容易出錯(cuò),但程序運(yùn)行效率是最高的,現(xiàn)在仍有不少公司要求這么做,一是這樣運(yùn)行效率高,二是不需要引入其它的組件,避免出現(xiàn)其它問題。
但對(duì)于我們來說,這種操作要是多了,開發(fā)效率和代碼可維護(hù)性都會(huì)受到影響,這種賦值屬性代碼很長(zhǎng),看起來很不舒服,所以有了下面幾種方式。
bean copier
apache 的 BeanUtils,內(nèi)部使用了反射,效率很低,在《阿里java開發(fā)規(guī)范中》明令禁止使用,這里就不過多討論。
spring的BeanUtils,對(duì) apache BeanUtils 做了優(yōu)化,運(yùn)行效率較高,可以使用。
BeanUtils.copyProperties(source, target);
BeanUtils.copyProperties(source, target, "id", "createTime"); //不拷貝指定的字段
cglib 的 BeanCopier,使用動(dòng)態(tài)技術(shù)代替反射,在運(yùn)行時(shí)生成一個(gè)子類,只有在第一次動(dòng)態(tài)生成類時(shí)慢,后面基本就本接近原始的set,所以呀運(yùn)行效率比上面兩種要高很多。
BeanCopier beanCopier = BeanCopier.create(SourceData.class, TargetData.class, false);
beanCopier.copy(source, target, null);
我們使用的是Spring BeanUtils,至少出現(xiàn)過兩次問題:
-
一次是拷貝一方的對(duì)象類型變了,由int變成long, source.id int拷貝到target.id long結(jié)果是空,因?yàn)轭愋筒黄ヅ?,BeanUtils 不會(huì)拷貝。由于是使用反射,所以當(dāng)時(shí)修改類型時(shí),只修改了編譯報(bào)錯(cuò)的地方,忘記這種方式,導(dǎo)致結(jié)果都是空,這也很難怪開發(fā),這種方式太隱蔽了。同樣如果屬性重命名,也會(huì)得到一個(gè)空,并且只能在運(yùn)行時(shí)發(fā)現(xiàn)。 -
另一次拷貝的時(shí)候會(huì)把所有屬性都拷過去,漏掉忽略主鍵 id,結(jié)果在插入的時(shí)候報(bào)了唯一索引沖突。我們的場(chǎng)景比較特殊, id,createTime,updateTime這三個(gè)字段是表必須有的,通常也是不能被拷貝的,如果每個(gè)地方都手寫忽略,代碼比較麻煩也容易忘記。
上面3種方式都非常簡(jiǎn)單,意味著功能非常有限,如果你有一些復(fù)雜場(chǎng)景的拷貝,它們就無法支持,例如深拷貝,拷貝一個(gè) List。
另外一個(gè)最重要的點(diǎn)是:它們都是運(yùn)行時(shí)的,這意味著你無法在編譯時(shí)得到任何幫助,無法提前發(fā)現(xiàn)問題。
從標(biāo)題可以看出我們本篇要講的是另一個(gè) copier:MapStruct,接下來就看下它是如何解決我們問題的。
MapStruct
MapStruct 是一個(gè)基于 Java 注解處理器,用于生成類型安全且高性能的映射器??偨Y(jié)一下它有以下優(yōu)點(diǎn):
-
高性能。 使用普通方法賦值,而非反射,MapStruct 會(huì)在編譯期間生成類,使用原生的 set 方法進(jìn)行賦值,所以效率和手寫 set 基本是一樣的。 -
類型安全。 MapStruct 是編譯時(shí)的,所以一旦有類型、名稱等不匹配問題,就可以提前編譯報(bào)錯(cuò)。 -
功能豐富。 MapStruct 的功能非常豐富,例如支持深拷貝,指定各種拷貝行為。 -
使用簡(jiǎn)單。 你所需要做的就是定義接口和拷貝的行為,MapStruct 會(huì)在編譯期生成實(shí)現(xiàn)類。
示例
和學(xué)習(xí)其它組件一樣,我們先用起來,準(zhǔn)備兩個(gè)類,SourceData,TargetData 屬性完全一樣,其中 TestData 是另一個(gè)類。
public class SourceData {
private String id;
private String name;
private TestData data;
private Long createTime;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public TestData getData() {
return data;
}
public void setData(TestData data) {
this.data = data;
}
public Long getCreateTime() {
return createTime;
}
public void setCreateTime(Long createTime) {
this.createTime = createTime;
}
}
導(dǎo)入包 pom
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.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>
定義接口
這里的 Mapper 是 MapStruct 的,可不是 Mybatis 的。
@Mapper
public interface BeanMapper {
BeanMapper INSTANCE = Mappers.getMapper(BeanMapper.class);
TargetData map(SourceData source);
}
使用
SourceData source = new SourceData();
source.setId("123");
source.setName("abc");
source.setCreateTime(System.currentTimeMillis());
TestData testData = new TestData();
testData.setId("123");
TargetData target = BeanMapper.INSTANCE.map(source);
System.out.println(target.getId() + ":" + target.getName() + ":" + target.getCreateTime());
//true
System.out.println(source.getData() == target.getData());
可以看到使用非常簡(jiǎn)單,默認(rèn)情況下 MapStruct 是淺拷貝,所以看到最后一個(gè)輸出是 true。編譯后我們可以在 target 目錄下找到幫我們生成的一個(gè)接口實(shí)現(xiàn)類 BeanMapperImpl,如下:
深拷貝
可以看到它也是幫生成 set 代碼,且默認(rèn)是淺拷貝,所以上面最后一個(gè)輸出是 true。如果想變成深拷貝,在 map 方法上標(biāo)記一下 DeepClone 即可:
@Mapping(target = "data", mappingControl = DeepClone.class)
TargetData map(SourceData source);
重新編譯一下,看到生成的代碼變成如下,這次是深拷貝了。
集合拷貝
支持,新增一個(gè)接口方法即可。
List<TestData> map(List<TestData> source);
類型不一致
如果我將 TargetData 的 createTime 改成 int 類型,再編譯一下,生成代碼如下:
可以看到它會(huì)默認(rèn)幫我們轉(zhuǎn)換,但這是個(gè)隱藏的問題,如果我希望它能在編譯時(shí)就提示,那么可以在 Mapper 注解上指定一些類型轉(zhuǎn)換的策略是報(bào)錯(cuò),如下:
@Mapper(typeConversionPolicy = ReportingPolicy.ERROR)
重新編譯會(huì)提示錯(cuò)誤:
java: Can't map property "Long createTime". It has a possibly lossy conversion from Long to Integer.
禁止隱式轉(zhuǎn)換
如果我將類型改成 String 呢,編譯又正常了,生成代碼如下:
對(duì)于 String 和其它基礎(chǔ)類型的包裝類,它會(huì)隱式幫我們轉(zhuǎn)換,這也是個(gè)隱藏問題,如果我希望它能在編譯時(shí)就提示,可以定義一個(gè)注解,并在 Mapper 中指定它,如下:
@Retention(RetentionPolicy.CLASS)
@MappingControl(MappingControl.Use.DIRECT)
@MappingControl(MappingControl.Use.MAPPING_METHOD)
@MappingControl(MappingControl.Use.COMPLEX_MAPPING)
public @interface ConversationMapping {
}
@Mapper(typeConversionPolicy = ReportingPolicy.ERROR, mappingControl = ConversationMapping.class)
重新編譯會(huì)提示報(bào)錯(cuò):
java: Can't map property "Long createTime" to "String createTime". Consider to declare/implement a mapping method: "String map(Long value)".
這個(gè)可以參見 issus 上的討論:issus1428 issus3186
忽略指定字段
忽略字段可以使用 Mapping 注解的 ignore 屬性,如下:
@Mapping(target = "id", ignore = true)
如果我想忽略某些字段,并且復(fù)用起來,就像我們的場(chǎng)景應(yīng)用,可以定義一個(gè)IgnoreFixedField注解,然后打在方法上
@Mapping(target = "id", ignore = true)
@Mapping(target = "createTime", ignore = true)
@Mapping(target = "updateTime", ignore = true)
@Target(METHOD)
@Retention(RUNTIME)
@Documented
@interface IgnoreFixedField {
}
@IgnoreFixedField
@Mapping(target = "data", mappingControl = DeepClone.class)
TargetData map(SourceData source);
這樣只要打上這個(gè)注解,這3個(gè)字段就不會(huì)拷貝了。
與 lombok 集成
如果你的項(xiàng)目使用了 lombok,上面的代碼可能沒法正常工作。需要在 maven 對(duì) lombok 也做下配置,在上面的 annotationProcessorPaths 加入如下配置即可。
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
</path>
上面只是結(jié)合本人的實(shí)際場(chǎng)景的一些例子,MapStruct 還有更多的功能,參見官方文檔。
總結(jié)
會(huì)用之后我們可以學(xué)習(xí)一下它的原理了,這也是我們平時(shí)學(xué)習(xí)一個(gè)新的東西的習(xí)慣,別一下子就扎到原理,源碼里頭,這樣會(huì)嚴(yán)重打擊學(xué)習(xí)熱情,要先跑起來先,看到成果后你會(huì)更有激情學(xué)習(xí)下去。
其實(shí) MapStruct 的原理和 lombok 是一樣的,都是在編譯期間生成代碼,而不會(huì)影響運(yùn)行時(shí)。例如我們最常見的 @Data 注解,查看源文件你會(huì)發(fā)現(xiàn) getter/setter 生成了,源文件的類不會(huì)有 @Data 注解。
java 代碼編譯和執(zhí)行的整個(gè)過程包含三個(gè)主要機(jī)制:
-
java源碼編譯機(jī)制 -
類加載機(jī)制 -
類執(zhí)行機(jī)制。
其中 java 源碼編譯由3個(gè)過程組成:
-
分析和輸入到符號(hào)表 -
注解處理 -
語義分析和生成class文件。
如下:
其中 annotation processing 就是注解處理,jdk7 之前采用 APT技術(shù),之后的版本使用了 JSR 269 API。
JSR 是什么?java Specification Requests,Java 規(guī)范提案,是指向 JCP(Java Community Process)提出新增一個(gè)標(biāo)準(zhǔn)化技術(shù)規(guī)范的正式請(qǐng)求。jsr 269 是什么?在這里[1]
注解我們非常熟悉,其實(shí)java里的注解有兩種,一種是運(yùn)行時(shí)注解,如常用 @Resource, @Autowired,另一種是編譯時(shí)注解,如 lombok 的 @Data。
編譯時(shí)注解主要作用是在編譯期間生成代碼,這樣就可以避免在運(yùn)行時(shí)使用反射。編譯時(shí)注解處理核心接口是 Processor,它有一個(gè)抽象實(shí)現(xiàn)類 AbstractProcessor 封裝了許多功能,如果要實(shí)現(xiàn)繼承它即可。
知道原理后,我們完全可以模仿 lombok 寫一個(gè)簡(jiǎn)單的生成器。
關(guān)于性能,知道原理后其實(shí)你也知道根本不用擔(dān)心mapstruct的性能問題了,可以參考這個(gè):benchmark[2]
如果要說它的缺點(diǎn),就是得為了這個(gè)簡(jiǎn)單的拷貝功能導(dǎo)這個(gè)包,如果你的程序只有很少的拷貝,那手動(dòng)寫一下也未嘗不可,如果有大量拷貝需求,那就推薦使用了。
程序汪接私活項(xiàng)目目錄,2023年總結(jié)
Java項(xiàng)目分享 最新整理全集,找項(xiàng)目不累啦 07版
程序汪10萬接的無線共享充電寶項(xiàng)目,開發(fā)周期3個(gè)月
程序汪1萬接的企業(yè)官網(wǎng)項(xiàng)目,開發(fā)周期15天
程序汪8萬接的共享口罩項(xiàng)目,開發(fā)周期1個(gè)月
程序汪8萬塊的飲水機(jī)物聯(lián)網(wǎng)私活項(xiàng)目經(jīng)驗(yàn)分享
程序汪接的酒店在線開房項(xiàng)目,另外一個(gè)好聽的名字叫智慧酒店
歡迎添加程序汪個(gè)人微信 itwang008 進(jìn)粉絲群或圍觀朋友圈
