Table of contents
相信開發過帶有複雜 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>
由上至下睇,列喺較上面既係優先)。
例子:
- 獨立例子一
- 我地既項目
MyLib
有 2
個 direct dependencies,分別係 Alice
以及 Bob
(相同次序)。
MyLib 1.0
➜ Alice 2.0
➜ Bob 2.0
➜ Charlie 2.0
MyLib 1.0
➜ Bob 1.0
➜ Charlie 1.0
- 結果係 Maven 最終會揀
Charlie 1.0
,因為 Charlie 1.0
最接近我地既項目。
- 獨立例子二
- 我地既項目
MyLib
有 2
個 direct dependencies,分別係 Alice
以及 Bob
(相同次序)。
MyLib 1.0
➜ Alice 1.0
➜ Charlie 1.0
MyLib 1.0
➜ Bob 2.0
➜ Charlie 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。
舉個獨立既例子:
- 我地既項目
MyLib
有 2
個 direct dependencies,分別係 Alice
以及 Bob
(依次序)。
MyLib 1.0
➜ Alice 1.0
➜ Charlie 1.0
MyLib 1.0
➜ Bob 2.0
➜ Charlie 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.0
同 Charlie 1.0
兼容,但係同 Charlie 2.0
唔兼容。
- 因為呢個原因,
Bob 2.0
同 Charlie 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
com.michael:charlie:2.0
既 project setup:
pom.xml
裡面冇任何 dependency,亦冇任何 build plugin。
- 只有一個 class:
src/main/java/com/charlie/Calculator.java
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
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.0
既 pom.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.0
既 pom.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.0
既 pom.xml
基本上一樣,我地只需要將所有 alice
既字眼換成 bob
,將版本號碼改成 2.0
。
建立好呢兩個 Maven 項目之後,
- 我地要將
alice-shaded 1.0
以及 aob-shaded 2.0
build 到 Maven repository。
- 想快速測試既話可以先
mvn install
到本地 Maven repository。
- 我地要將
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。
- 當我地執行
my-lib
既 Main
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
既默認配置。
- 如果
charlie
、alice
或者 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.yml
、logback.xml
等等。
工具 | 指定 Maven profile 既方式 |
---|
Eclipse IDE | Right click 個 project > Maven > Select Maven Profiles... |
Maven CLI | 用 -P 既 CLI option,例如 mvn clean package -P cloud 、mvn clean spring-boot:run -P local |