➜ Old React website
Chung Cheuk Hang MichaelJava Web Developer
Linux 基本指令Java 測試(六):Cucumber

進階 Maven

Continued from previous post:
Java 開發筆記(六)

Table of contents

1 Maven dependency mediation

相信開發過帶有複雜 dependencies 既 Maven 項目既 Java developers 都可能曾經遇過呢個問題:
我個 Maven 項目有 2 個或以上既 direct dependencies,呢啲 dependencies 都係 depend on 一個相同 groupId + artifactId 但係唔同 version 既 Maven library,咁到底我個項目最終會 depends on 呢個 transitive dependency 既邊一個版本號碼?
答案:
Maven 3 喺處理一個項目既 nested dependencies 裡面重複 declare 既 dependencies 既時候,會用到 2 個 rules 去揀選版本號碼:
  • Maven 會睇曬呢啲重複既 dependency declarations,揀最接近我地既項目既果個 dependency declaration,然後就用佢既版本號碼。
  • 如果喺距離上有平排既情況,Maven 會揀 dependency tree 裡面較上 declare 既果個版本號碼(POM 檔既 <dependencies> 由上至下睇,列喺較上面既係優先)。
例子:
  • 獨立例子一
    • 我地既項目 MyLib2 個 direct dependencies,分別係 Alice 以及 Bob(相同次序)。
      • MyLib 1.0Alice 2.0Bob 2.0Charlie 2.0
      • MyLib 1.0Bob 1.0Charlie 1.0
    • 結果係 Maven 最終會揀 Charlie 1.0,因為 Charlie 1.0 最接近我地既項目。
  • 獨立例子二
    • 我地既項目 MyLib2 個 direct dependencies,分別係 Alice 以及 Bob(相同次序)。
      • MyLib 1.0Alice 1.0Charlie 1.0
      • MyLib 1.0Bob 2.0Charlie 2.0
    • 結果係 Maven 最終會揀 Charlie 1.0,因為兩個 Charlie 既 dependency declarations 同我地項目有相同既距離,而 Charlie 1.0 係較先 declare。
(上面以「➜」表示 direct dependency。)
當我地明白左「距離」既原理之後,我地就會知道點解:
  • 當我地想將我地既 Maven 項目用到既某啲 transitive dependencies 既版本號碼升級,我地會將呢啲 dependencies 連同特定既版本號碼直接 declare 喺我地項目之下作為一個 direct dependency,簡單咁做到覆蓋版本號碼既效果。
  • 有啲情況下,當我地移動左我地既 Maven 項目既 direct dependencies 既次序之後,我地個程式既 behaviors 會唔同左,咁應該係因為某啲 transitive dependencies 既版本號碼改變左。
註:
  • Maven 係唔會理版本號碼本身既值。佢唔會因為一個版本號碼 lexicographically 大過另一個就揀大啲果個。
參考資料:

2 Maven Shade Plugin

Maven 項目既 pom.xml 既默認 packaging type 係 jar,而喺默認情況下,maven-jar-plugin 喺 build 我地既 JAR 檔既時候:
  • 佢只會將我地既 Maven 項目裡面既 Java bytecode .class 檔抄入去。
  • 我地既 Maven 項目既所有 dependencies 以及 transitive dependencies 既資訊只會以一個 POM XML 檔既形式存在,而佢地既 Java bytecode .class 檔係唔會被抄入去最終既 JAR 檔裡面,原因係要避免重複打包呢啲檔案。
喺少數情況下,我地既 Maven 項目用到既 dependencies 之間可能會出現唔兼容既情況,而唔兼容既原因係佢地一定既 implementations 只係兼容特定版本號碼既 dependencies。
舉個獨立既例子:
  • 我地既項目 MyLib2 個 direct dependencies,分別係 Alice 以及 Bob(依次序)。
    1. MyLib 1.0Alice 1.0Charlie 1.0
    2. MyLib 1.0Bob 2.0Charlie 2.0
  • 根據我地對 Maven 揀選 transitive dependency 版本號碼既理解,我地知道 Maven 最終會揀 Charlie 1.0
  • Charlie 2.0 introduce 左 breaking changes,佢好多野都同 Charlie 1.0 有差異。
    • 差異既例子
      • Classes、enums、annotations 等等既野有增減。
      • Classes 既 method signatures 或者 enums、annotations 等等既野裡面既內容有增減。
    • 因為呢個原因,Alice 1.0Charlie 1.0 兼容,但係同 Charlie 2.0 唔兼容。
    • 因為呢個原因,Bob 2.0Charlie 2.0 兼容,但係同 Charlie 1.0 唔兼容。
其實無論 Maven 最終揀左邊個版本號碼既 Charlie 都好,我地個程式都會存在住 runtime errors(例如 NoSuchMethodError),導致我地既程式冇辦法執行。
為左解決呢個問題,我地可以用 maven-shade-plugin 去修改 dependency libraries 既 classes,然後打包成岩用既新 libraries。

2.1 簡單既例子

com.michael:charlie:1.0 既 project setup:
  • pom.xml 裡面冇任何 dependency,亦冇任何 build plugin。
  • 只有一個 class:src/main/java/com/charlie/Calculator.java
    • public class Calculator { public int sum(int a, int b) { return a + b; } }
com.michael:charlie:2.0 既 project setup:
  • pom.xml 裡面冇任何 dependency,亦冇任何 build plugin。
  • 只有一個 class:src/main/java/com/charlie/Calculator.java
    • public class Calculator { public int sum(int a, int b, int c) { return a + b + c; } }
com.michael:alice:1.0 既 project setup:
  • pom.xml 裡面只有一個 com.michael:charlie:1.0 既 dependency,冇任何 build plugin。
  • 只有一個 class:src/main/java/com/alice/AliceCalculator.java
    • public class AliceCalculator { public int sum(int a, int b) { return new Calculator().sum(a, b); } }
com.michael:bob:2.0 既 project setup:
  • pom.xml 裡面只有一個 com.michael:charlie:2.0 既 dependency,冇任何 build plugin。
  • 只有一個 class:src/main/java/com/bob/BobCalculator.java
    • public class BobCalculator { public int sum(int a, int b, int c) { return new Calculator().sum(a, b, c); } }
com.michael:my-lib:1.0 既 project setup:
  • pom.xml 裡面有 com.michael:alice:1.0 以及 com.michael:bob:2.0 既 dependencies(次序有影響,但喺呢個例子冇所謂),同埋一個 spring-boot-maven-plugin 既 build plugin。
  • 只有一個 class:src/main/java/code/Main.java
    • public class Main { public static void main(String[] args) { System.out.println(new AliceCalculator().sum(1, 2) + new BobCalculator().sum(1, 2, 3)); } }
喺呢個例子裡面:
  • 我地有 2 個唔同版本既 com.charlie.Calculator class,佢地既 sum methods 既 signatures 有分別。
  • my-lib 1.0 有需要同時用到 2 個版本既 charlie library 作為 transitive dependencies。
不過呢度就衍生左一啲問題:
  • 當有多過一個 Java class 既 fully qualified class names 係一樣既時候,JVM 喺一個 class loader object 裡面只會 load 其中一個版本既 class。
  • Maven 唔容許喺一個項目裡面相同 groupId 以及 artifactId 既 transitive dependency 存在多個版本。
詳細解釋:
  • my-lib 1.0 depends on alice 1.0 以及 bob 2.0,而佢地各自 depends on 唔同版本既 charlie transitive dependency。
  • 視乎佢地喺 my-lib 1.0pom.xml<dependencies> 裡面既次序,Maven 就會揀佢地其中一個作為 my-lib 1.0 既 transitive dependency。
  • 不過,既然 classpath 只能存在一個版本既 charlie,咁就會導致 my-lib 1.0 缺少左其中一個版本既 Calculator class,最終導致 AliceCalculator 或者 BobCalculator 其中一個 class 出現 NoSuchMethodError runtime error。
如果 my-lib 1.0<dependencies>alice 1.0 然後 bob 2.0
Exception in thread "main" java.lang.NoSuchMethodError: 'int com.charlie.Calculator.sum(int, int, int)' at com.bob.BobCalculator.sum(BobCalculator.java:8) at code.Main.main(Main.java:9)
如果 my-lib 1.0<dependencies>bob 2.0 然後 alice 1.0
Exception in thread "main" java.lang.NoSuchMethodError: 'int com.charlie.Calculator.sum(int, int)' at com.alice.AliceCalculator.sum(AliceCalculator.java:8) at code.Main.main(Main.java:9)
要解決呢個問題,我地可以 shade alice 1.0 或者 bob 2.0,甚至 shade 曬佢地。
要 shade alice 1.0,我地需要創建一個 com.michael:alice-shaded:1.0 既 library,而佢既 project setup:
  • pom.xml 裡面只有一個 com.michael:alice:1.0 既 dependency,同埋一個 maven-shade-plugin 既 build plugin。
  • 可以唔需要 src folder。
要 shade bob 2.0,我地需要創建一個 com.michael:bob-shaded:2.0 既 library,而佢既 project setup:
  • pom.xml 裡面只有一個 com.michael:bob:2.0 既 dependency,同埋一個 maven-shade-plugin 既 build plugin。
  • 可以唔需要 src folder。
以下係 alice-shaded 1.0pom.xml
1<groupId>com.michael</groupId> 2<artifactId>alice-shaded</artifactId> 3<version>1.0</version> 4<packaging>jar</packaging> 5 6<dependencies> 7 <dependency> 8 <groupId>com.michael</groupId> 9 <artifactId>alice</artifactId> 10 <version>1.0</version> 11 </dependency> 12</dependencies> 13 14<build> 15 <plugins> 16 <plugin> 17 <groupId>org.apache.maven.plugins</groupId> 18 <artifactId>maven-shade-plugin</artifactId> 19 <version>3.5.2</version> 20 <configuration> 21 <promoteTransitiveDependencies>true</promoteTransitiveDependencies> 22 <keepDependenciesWithProvidedScope>false</keepDependenciesWithProvidedScope> 23 <artifactSet> 24 <includes> 25 <include>com.michael:charlie</include> 26 <include>com.michael:alice</include> 27 </includes> 28 </artifactSet> 29 <relocations> 30 <relocation> 31 <pattern>com.charlie</pattern> 32 <shadedPattern>com.alice</shadedPattern> 33 <includes> 34 <include>com.charlie.*</include> 35 </includes> 36 </relocation> 37 </relocations> 38 </configuration> 39 <executions> 40 <execution> 41 <phase>package</phase> 42 <goals> 43 <goal>shade</goal> 44 </goals> 45 </execution> 46 </executions> 47 </plugin> 48 </plugins> 49</build>
至於 bob-shaded 2.0,佢既 pom.xml 就同上面 alice-shaded 1.0pom.xml 基本上一樣,我地只需要將所有 alice 既字眼換成 bob,將版本號碼改成 2.0
建立好呢兩個 Maven 項目之後,
  1. 我地要將 alice-shaded 1.0 以及 aob-shaded 2.0 build 到 Maven repository。
    • 想快速測試既話可以先 mvn install 到本地 Maven repository。
  2. 我地要將 my-lib 1.0 既兩個 dependencies 換成呢兩個 shade 左既版本。
    • 我地會見到 my-lib 1.0 而家唔再有 charlie 既 transitive dependency。
    • 我地而家會有 com.alice.Calculator 以及 com.bob.Calculator 兩個唔同 packages 既 classes,佢地係對應原先唔同版本既 com.charlie.Calculator class。
  3. 當我地執行 my-libMain class 既時候,就會見到 console print 9(即係 (1 + 2) + (1 + 2 + 3) 既結果)。
註:
  • mvn package 或者 mvn install 之後會見到 project root folder 多左個 dependency-reduced-pom.xml 檔,佢係 maven-shade-plugin 根據配置而修改出黎既 POM 檔。
  • maven-shade-plugin 會將已經 shade 左入 JAR artifact 既 dependencies 喺 POM 度變成 provided scope。
    • 根據 Maven,provided scope 既 dependencies 唔係 transitive,所以就算佢地存在喺 alice-shaded 1.0 以及 bob-shaded 2.0 既 POM 檔,我地都唔洗擔心 my-lib 1.0 會有佢地(亦唔洗擔心 spring-boot-maven-plugin 既奇怪邏輯會將 provided scope 既 dependencies 以及佢地帶黎既 transitive dependencies 包含喺 nested JAR artifact 裡面)。
    • 但如果我地都想佢地唔存在喺 POM 檔,可以用 keepDependenciesWithProvidedScope = false 既配置黎移除佢地。
    • 如果 alice-shaded 1.0 以及 bob-shaded 2.0 既 parent POM 係 spring-boot-starter-parent 既話,咁就會 inherit 左佢 <build> section 既 <pluginManagement> 去用 keepDependenciesWithProvidedScope = true 作為 maven-shade-plugin 既默認配置。
  • 如果 charliealice 或者 bob 有 dependencies,咁 alice-shaded 1.0 以及 bob-shaded 2.0 就要用到 promoteTransitiveDependencies = true 既配置,去令 alice 1.0 以及 bob 2.0 既 dependencies 以及所有帶黎既 transitive dependencies 直接寫喺 alice-shaded 1.0 以及 bob-shaded 2.0 既 POM 檔裡面。
  • <artifactSet> 裡面,我地喺 <includes> 度指定左只會 shade com.michael:charlie 以及 com.michael:alice,而默認係會 shade 全部 dependencies。
  • <relocations> 裡面,我地針對 com.charlie.* packages 既所有 classes,將佢地 package 名既 com.charlie relocate 成 com.alice(或者 com.bob),再 shade 佢地入 JAR artifact 裡面。
參考資料:

3 Maven profiles

Spring Boot 可以畀我地用唔同 profiles 既 YAML 或者 Java Properties 檔案作為配置檔,但係如果我地將本地開發既 application-local.yml 或者 application-local.properties 檔放左落 src/main/resources 裡面,Maven CLI 打包出黎既 JAR 檔裡面就會包括埋呢啲本地開發用既檔案。
一個比較有系統既做法係用 Maven profiles:
  • 我地可以喺 Eclipse IDE 裡面揀我地想用既 Maven profile 去 build 個 Maven project。
  • 我地可以用 Maven CLI 指定 Maven profile 去 build 或者 run 我地個 Maven project。
  • 只要有 set 岩默認既 Maven profile,同埋啲檔案完全分開擺放,咁呢個方法就唔會影響到 production build。
下面係 Maven profiles 既其中一個用法:
1<project> 2 3 <!-- 一般 project metadata --> 4 5 <!-- properties --> 6 7 <!-- parent POM --> 8 9 <!-- dependencyManagement --> 10 11 <!-- dependencies --> 12 13 <!-- build --> 14 15 <profiles> 16 <profile> 17 <id>local</id> 18 <build> 19 <resources> 20 <resource> 21 <directory>src/main/resources/local</directory> 22 </resource> 23 </resources> 24 </build> 25 <dependencies> 26 <dependency> 27 <groupId>com.h2database</groupId> 28 <artifactId>h2</artifactId> 29 </dependency> 30 </dependencies> 31 </profile> 32 33 <profile> 34 <id>cloud</id> 35 <activation> 36 <activeByDefault>true</activeByDefault> 37 </activation> 38 <build> 39 <resources> 40 <resource> 41 <directory>src/main/resources/cloud</directory> 42 </resource> 43 </resources> 44 </build> 45 <dependencies> 46 <dependency> 47 <groupId>com.microsoft.sqlserver</groupId> 48 <artifactId>mssql-jdbc</artifactId> 49 </dependency> 50 </dependencies> 51 </profile> 52 </profiles> 53 54</project>
然後可以喺 src/main/resources/local 以及 src/main/resources/cloud 既 folders 裡面放帶有唔同配置既 application.ymllogback.xml 等等。
工具指定 Maven profile 既方式
Eclipse IDERight click 個 project > Maven > Select Maven Profiles...
Maven CLI-P 既 CLI option,例如 mvn clean package -P cloudmvn clean spring-boot:run -P local