MapStruct與lombok載入順序問題與annotationProcessorPaths的關係?

經典雞翅 發表於 2022-12-04

MapStruct是什麼?

MapStruct is a code generator that greatly simplifies the implementation of mappings between Java bean types based on a convention over configuration approach.——https://mapstruct.org/
從官方定義來看,MapStruct類似於我們熟悉的BeanUtils, 是一個Bean的轉換框架。
In contrast to other mapping frameworks MapStruct generates bean mappings at compile-time which ensures a high performance, allows for fast developer feedback and thorough error checking.——https://mapstruct.org/
他與BeanUtils最大的不同之處在於,其並不是在程式執行過程中透過反射進行欄位複製的,而是在編譯期生成用於欄位複製的程式碼(類似於Lombok生成get()和set()方法),這種特性使得該框架在執行時相比於BeanUtils有很大的效能提升。

lombok

這個大家都很熟悉,生成getset方法,那麼mapstuct要依賴這種方法。

MapStuct與lombok的引入的正確關係

由於MapStruct和Lombok都會在編譯期生成程式碼,如果配置不當,則會產生衝突,因此在工程中同時使用這兩個包時,應該按照以下方案匯入:

當POM中不包含Lombok時

<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
    <version>1.5.2.Final</version>
</dependency>
<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-processor</artifactId>
    <version>1.5.2.Final</version>
</dependency>

當POM中包含Lombok且不包含

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.24</version>
</dependency>

<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
    <version>1.5.2.Final</version>
</dependency>

<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-processor</artifactId>
    <version>1.5.2.Final</version>
</dependency>

注意:引入時,mapstruct-processor必須lombok後面。

當POM中包含Lombok且包含

<properties>
    <org.mapstruct.version>1.5.2.Final</org.mapstruct.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct</artifactId>
        <version>${org.mapstruct.version}</version>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.24</version>
    </dependency>
</dependencies>
<build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.5.1</version>
                <configuration>
                    <source>8</source>
                    <target>8</target>
                    <annotationProcessorPaths>
                        <path>
                            <groupId>org.mapstruct</groupId>
                            <artifactId>mapstruct-processor</artifactId>
                            <version>${org.mapstruct.version}</version>
                        </path>
                        <path>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                            <version>1.18.12</version>
                        </path>
                    </annotationProcessorPaths>
                </configuration>
            </plugin>
        </plugins>
    </build>

問題產生

如果lombok在mapstuct後面,則會產生問題

<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>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.12</version>
</dependency>

mapstruct編譯原理

為了探究上述問題產生的原因,我們首先要理解MapStruct的基本原理。

MapStruct與其他Bean對映庫最大的不同就是,其在編譯期間生成轉換程式碼,而不是在執行時透過反射生成程式碼。

為了更直觀的理解這一點,可以從target中找到MapStruct自動生成的對應的ConveterImpl類
MapStruct與lombok載入順序問題與annotationProcessorPaths的關係?
即MapStruct為我們編寫的Convert抽象類自動生成了一個實現。

而Lombok也是在編譯時自動生成程式碼,那麼問題大機率就出現在這裡了。

MapStruct是如何與Lombok共存的?

查閱MapStruct官方文件可以發現這樣一段內容:

MapStruct與lombok載入順序問題與annotationProcessorPaths的關係?

其中提到,MapStruct的annotation processor必須在Lombok的annotation processor生成完程式碼之後,才可以正常執行。

所以,這應該就是在匯入dependencies時,必須先匯入Lombok包,再匯入MapStruct-processor包才可以正常執行的原因了。不過還有個問題沒有解決:

Maven到底在哪裡規定了annotation processor的載入順序?難道每次建立工程時,必須記住這些包匯入順序麼?

MapStruct官方推薦的匯入流程

在進一步檢視MapStruct官網時發現,其並沒有將MapStruct-processor放在dependencies中,而是放在了annotationProcessorPaths層級下:

https://mapstruct.org/documentation/installation/

...

<org.mapstruct.version>1.5.2.Final</org.mapstruct.version>

...


org.mapstruct
mapstruct
${org.mapstruct.version}


...



org.apache.maven.plugins
maven-compiler-plugin
3.8.1

1.8
1.8

                <path>
                    <groupId>org.mapstruct</groupId>
                    <artifactId>mapstruct-processor</artifactId>
                    <version>${org.mapstruct.version}</version>
                </path>
                <!-- other annotation processors -->
            </annotationProcessorPaths>
        </configuration>
    </plugin>
</plugins>
這又是為什麼呢?

查閱Maven官方文件,對於有這樣一段描述:

If specified, the compiler will detect annotation processors only in those classpath elements. If omitted, the default classpath is used to detect annotation processors. The detection itself depends on the configuration of annotationProcessors.

即如果有層級,則使用這個層級宣告註解處理器的順序執行,如果沒有,則按照預設classpath的順序來使用註解處理器。

地址:https://maven.apache.org/plugins/maven-compiler-plugin/compile-mojo.html#annotationProcessorPaths

地址:https://maven.apache.org/plugins/maven-compiler-plugin/compile-mojo.html#annotationProcessorPaths

我們接下來以下命令來獲取當前Maven專案中的classpath:

mvn dependency:build-classpath -Dmdep.outputFile=classPath.txt
從匯出內容可以看出,classPath中的Jar包順序就是與dependencies中匯入的順序是相同的。

自此,關於MapStruct匯入順序的所有問題均已經被解決,總結如下:

在POM中沒有annotationProcessorPaths時,Maven使用的classPath作為註解處理器執行的順序,而classPath的順序正是dependencies中匯入的順序。
當MapStruct依賴在Lombok依賴前面時,在執行註解處理器期間, 由於Lombok還未生成get、set程式碼,因此在MapStruct看來,這些類並沒有公開的成員變數,也就無從生成用於轉換的方法。
在使用annotationProcessorPaths後,其強制規定了註解處理器的順序,dependencies中的順序就被忽略了,Maven一定會先執行Lombok再執行MapStruct,程式碼即可正常執行。