結合例項看 maven 傳遞依賴與優先順序,難頂也得上丫

青石路發表於2024-08-07

開心一刻

想買摩托車了,但是錢不夠,想找老爸借點

我:老爸,我想買一輛摩托車,上下班也方便

老爸:你表哥上個月騎摩托車摔走了,你不知道?還要買摩托車?

我:對不起,我不買了

老闆:就是啊,騎你表哥那輛得了唄,買啥新的

你是認真的嗎

先拋問題

關於 maven 的依賴(dependency),我相信大家多少都知道點

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.qsl</groupId>
    <artifactId>spring-boot-2_7_18</artifactId>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.18</version>
    </parent>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>
</project>

依賴什麼就引入什麼,是不是很合理,也很合邏輯?我們來看下此時的 log 依賴

log依賴

使用了 idea 的 Maven Helper 外掛,一款不錯的 maven dependency 分析工具,推薦使用

此時你們是不是有疑問了:不就依賴 spring-boot-starter-web,怎麼會有各種 log 的依賴?

然後我在 pom.xml 中加一行,僅僅加一行

新加一行

此時的 log 依賴與之前就有了變化

log依賴變化

這是為什麼?

你以為沒關係,實際啟動時會出現如下異常(原因請看:SpringBoot2.7還是任性的,就是不支援Logback1.3,你能奈他何

SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
Exception in thread "main" java.lang.NoClassDefFoundError: org/slf4j/impl/StaticLoggerBinder
	at org.springframework.boot.logging.logback.LogbackLoggingSystem.getLoggerContext(LogbackLoggingSystem.java:304)
	at org.springframework.boot.logging.logback.LogbackLoggingSystem.beforeInitialize(LogbackLoggingSystem.java:118)
	at org.springframework.boot.context.logging.LoggingApplicationListener.onApplicationStartingEvent(LoggingApplicationListener.java:238)
	at org.springframework.boot.context.logging.LoggingApplicationListener.onApplicationEvent(LoggingApplicationListener.java:220)
	at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:178)
	at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:171)
	at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:145)
	at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:133)
	at org.springframework.boot.context.event.EventPublishingRunListener.starting(EventPublishingRunListener.java:79)
	at org.springframework.boot.SpringApplicationRunListeners.lambda$starting$0(SpringApplicationRunListeners.java:56)
	at java.util.ArrayList.forEach(ArrayList.java:1249)
	at org.springframework.boot.SpringApplicationRunListeners.doWithListeners(SpringApplicationRunListeners.java:120)
	at org.springframework.boot.SpringApplicationRunListeners.starting(SpringApplicationRunListeners.java:56)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:299)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1300)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1289)
	at com.qsl.Application.main(Application.java:16)
Caused by: java.lang.ClassNotFoundException: org.slf4j.impl.StaticLoggerBinder
	at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
	at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
	... 17 more

然後你就懵逼了

怎麼會這樣

我們再調整下 pom.xml

pom去掉springboot日誌

此時的 log 依賴如下

logback1.3.14依賴的slf4j怎麼是1.7.36

也許你們覺得沒問題,我再給你們引申下;logback1.3.14 依賴的 slf4j 版本是 2.0.7

logback1.3.14依賴slf4j2.0.7

slf4j1.7.36 是哪來的,為什麼不是 2.0.7 ?

這一連串問題下來,就問你們慌不慌,但你們不要慌,因為我會出手!

傳遞性依賴

在 maven 誕生之前,那時候新增 jar 依賴可以說是一個非常頭疼的事,需要手動去新增所有的 jar,非常容易遺漏,然後根據異常去補遺漏的 jar;很多有經驗的老手都會分類,比如引入 Spring 需要新增哪幾個 jar,引入 POI 又需要新增哪幾個 jar,但還是容易遺漏;而 maven 的傳遞性依賴機制就很好的解決了這個問題

何謂傳遞性依賴,回到我們最初的案例

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.qsl</groupId>
    <artifactId>spring-boot-2_7_18</artifactId>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.18</version>
    </parent>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>
</project>

直觀看上去,只依賴了 spring-boot-starter-web,但 spring-boot-starter-web 也有自身的依賴,maven 也會進行解析,以此類推,maven 會將那些必要的間接依賴以傳遞性依賴的形式引入到當前的專案中

傳遞性依賴

問題

不就依賴 spring-boot-starter-web,怎麼會有各種 log 的依賴?

是不是清楚了?

依賴優先順序

傳遞性依賴機制大大簡化了依賴宣告,對我們開發者而言非常友好,比如我們需要用到 spring 的 web 功能,只需要簡單的引入

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>

就 ok 了,是不是 so easy ?但同樣會帶來一些問題,比如專案 P 有如下兩條傳遞性依賴

P -> A -> B -> C(1.0)

P -> D -> C(2.0)

那麼哪個 C 會被 maven 引入到 P 專案中呢?此時 maven 會啟用它的第一原則

最短路徑優先

這裡的 路徑 指的是傳遞依賴的長度,一次傳遞依賴的長度是 1,P 到 C(1.0)傳遞依賴的長度是 3,而 P 到 C(2.0)傳遞依賴的長度是 2,所以 C(2.0)會被 maven 引入到 P 專案,而 C(1.0)會被忽略

最短路徑優先 並不能解決所有問題,比如專案 P 有如下兩條傳遞性依賴

P -> B -> C(1.0)

P -> D -> C(2.0)

兩條傳遞依賴的長度都是 2,那 maven 會引入誰了?從 maven 2.0.9 開始,maven 增加了第二原則

第一宣告優先

用來處理 最短路徑優先 處理不了的情況;在專案 P 的 pom 中,先被宣告的會被 maven 採用而引入到專案 P,所以 B 和 D 的宣告順序決定了 maven 是引入 C(1.0)還是引入 C(2.0),如果 B 先於 D 被宣告,那麼 C(1.0)會被 maven 引入到 P,而 C(2.0)會被忽略

我們再來看

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.qsl</groupId>
    <artifactId>spring-boot-2_7_18</artifactId>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.18</version>
    </parent>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <logback.version>1.3.14</logback.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>
</project>

此時的 logback

log依賴變化

為什麼是 1.3.14,而不是 1.2.12?這裡其實涉及到 自定義屬性 的覆蓋,有點類似 java 中的 override;1.2.12 是在父依賴(spring-boot-starter-parent)的父依賴(spring-boot-dependencies)中宣告的自定義屬性

logback1.2.12

而我們自己宣告的自定義屬性 <logback.version>1.3.14</logback.version> 正好覆蓋掉了 1.2.12,所以 maven 採用的是 1.3.14

是不是隻剩最後一個問題了,我們先來回顧下問題,pom.xml 內容如下

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.qsl</groupId>
    <artifactId>spring-boot-2_7_18</artifactId>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.18</version>
    </parent>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <logback.version>1.3.14</logback.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <exclusions>
                <exclusion>
                    <artifactId>spring-boot-starter-logging</artifactId>
                    <groupId>org.springframework.boot</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>${logback.version}</version>
        </dependency>
    </dependencies>
</project>

此時的依賴

logback1.3.14依賴的slf4j怎麼是1.7.36

slf4j 為什麼是 1.7.36,而不是 logback 中的 2.0.7?這裡其實涉及到 自定義屬性 的優先順序

自定義屬性的優先順序同樣遵循 maven 傳遞依賴的第一、第二原則

從爺爺(spring-boot-dependencies)繼承來的 slf4j.version1.7.36

slf4j1.7.36

相當於是自己的,傳遞依賴的長度是 0,而 logback 從其父親繼承而來的 slf4j.version (2.0.7)

slf4j2.0.7

傳遞依賴長度是 1,所以 maven 採用的是 1.7.36 而不是 2.0.7;那如何改了,最簡單的方式如下

<properties>
	<maven.compiler.source>8</maven.compiler.source>
	<maven.compiler.target>8</maven.compiler.target>
	<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
	<logback.version>1.3.14</logback.version>
	<slf4j.version>2.0.7</slf4j.version>
</properties>

總結

  1. maven 的傳遞依賴是個很強大的功能,以後碰到那種引入一個依賴而帶入了超多依賴的情況,不要再有疑問

  2. maven 依賴優先順序遵循兩個原則

    第一原則:最短路徑優先

    第二原則:最先宣告優先

    第一原則處理不了的情況才會採用第二原則;自定義屬性同樣遵循這兩個原則

相關文章