开心一刻
想买摩托车了,但是钱不够,想找老爸借点
我:老爸,我想买一辆摩托车,上下班也方便
老爸:你表哥上个月骑摩托车摔走了,你不知道?还要买摩托车?
我:对不起,我不买了
老板:就是啊,骑你表哥那辆得了呗,买啥新的
先抛问题
关于 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
依赖
使用了 idea 的 Maven Helper 插件,一款不错的 maven dependency 分析工具,推荐使用
此时你们是不是有疑问了:不就依赖 spring-boot-starter-web
,怎么会有各种 log
的依赖?
然后我在 pom.xml
中加一行,仅仅加一行
此时的 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
此时的 log
依赖如下
也许你们觉得没问题,我再给你们引申下;logback1.3.14
依赖的 slf4j
版本是 2.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
为什么是 1.3.14,而不是 1.2.12?这里其实涉及到 自定义属性
的覆盖,有点类似 java 中的 override;1.2.12 是在父依赖(spring-boot-starter-parent)的父依赖(spring-boot-dependencies)中声明的自定义属性
而我们自己声明的自定义属性 <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>
此时的依赖
slf4j 为什么是 1.7.36,而不是 logback 中的 2.0.7?这里其实涉及到 自定义属性
的优先级
自定义属性的优先级同样遵循 maven 传递依赖的第一、第二原则
从爷爷(spring-boot-dependencies)继承来的 slf4j.version
是 1.7.36
相当于是自己的,传递依赖的长度是 0,而 logback 从其父亲继承而来的 slf4j.version
(2.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>
总结
- maven 的传递依赖是个很强大的功能,以后碰到那种引入一个依赖而带入了超多依赖的情况,不要再有疑问
- maven 依赖优先级遵循两个原则
第一原则:最短路径优先
第二原则:最先声明优先
第一原则处理不了的情况才会采用第二原则;自定义属性同样遵循这两个原则