<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          是時(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ī)制:

          1. java源碼編譯機(jī)制
          2. 類加載機(jī)制
          3. 類執(zhí)行機(jī)制。

          其中 java 源碼編譯由3個(gè)過程組成:

          1. 分析和輸入到符號(hào)表
          2. 注解處理
          3. 語義分析和生成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)分享

          程序汪接的4萬智慧餐飲項(xiàng)目

          程序汪接的酒店在線開房項(xiàng)目,另外一個(gè)好聽的名字叫智慧酒店


          歡迎添加程序汪個(gè)人微信 itwang008  進(jìn)粉絲群或圍觀朋友圈

          瀏覽 315
          2點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          2點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  免费操逼视频在线观看 | 青娱乐免费视频一二三 | 91精品婷婷国产综合久久韩漫 | 成人一级黄色A片 | 抽插美女大逼网页 |