Maven 的配置和依赖是单根继承的

Maven 的模块继承是无法进行多继承的,只能使用单根继承。

Maven 中 dependencies 与 dependencyManagement 的区别

dependencies 即使在子项目中不写该依赖项,那么子项目仍然会从父项目中继承该依赖项(全部继承)

dependencyManagement 里只是声明依赖和它们的版本,并不实现引入,因此子项目需要显示的声明需要用的依赖。如果不在子项目中声明依赖,是不会从父项目中继承下来的;只有在子项目中写了该依赖项,并且没有指定具体版本,才会从父项目中继承该项,并且 version 和 scope 都读取自父 pom; 另外如果子项目中指定了版本号,那么会使用子项目中指定的jar版本。

有效 pom

1
2
3
mvn help:effective-pom > effective-pom.xml
# 获取某个指定 artifact 的依赖树
mvn dependency:tree -Dverbose -Dincludes=com.fasterxml.jackson.core:jackson-core

下载依赖中的源码和文档

在 maven 里,resolve 有特别的含义:仲裁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 只下载源码
mvn dependency:sources

# 静默下载源码
mvn dependency:sources -Dsilent=true

# 只下载文档
mvn dependency:resolve -Dclassifier=javadoc
# 一起下载
mvn dependency:sources dependency:resolve -Dclassifier=javadoc

# 主动获取源码,这样做可以帮助其他构件工具使用 mavenLocal() 仓库
mvn dependency:get -Dartifact=GROUP_ID:ARTIFACT_ID:VERSION:jar:sources
mvn dependency:get -Dartifact=org.springframework:spring-core:5.1.2.RELEASE:jar:sources

在 .m2 中这样配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<settings>
<!-- ... other settings omitted ... -->
<profiles>
<profile>
<id>downloadSources</id>
<properties>
<downloadSources>true</downloadSources>
<downloadJavadocs>true</downloadJavadocs>
</properties>
</profile>
</profiles>

<activeProfiles>
<activeProfile>downloadSources</activeProfile>
</activeProfiles>
</settings>

在项目中这样主动配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.1.2</version>
<executions>
<execution>
<goals>
<goal>sources</goal>
<goal>resolve</goal>
</goals>
<configuration>
<classifier>javadoc</classifier>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>

附赠一个脚本:

1
2
vi complete-build.sh
chmod a+x complete-build.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#!/bin/bash

function traverseAllProjects () {
for f in *; do
if [ -d "$f" ]; then
echo "coming into a folder $f"
cd $f
echo "current path:"
git pull
pwd
POM_FILE=pom.xml
if test -f "$POM_FILE"; then
echo "$POM_FILE exists."
# clean build
time mvn clean install -Dmaven.test.skip=true
# resolve dependencies' javadocs and sources
time mvn dependency:sources dependency:resolve -Dclassifier=javadoc\n
# complete build
time mvn clean install -Dmaven.test.skip=true
fi
GRADLE_FILE=build.gradle
if test -f "$GRADLE_FILE"; then
echo "$GRADLE_FILE exists."
# clean build
time ./gradlew clean build -x test
# 目前还没有简单的在命令行里下载源代码的方法
fi
echo "coming out a folder $f"
cd ..
echo "current path:"
pwd
fi
done
}

time traverseAllProjects

在 idea中这样配置Preference > Build, Execution, Deployment > Build Tools > Maven > importing

bom 和 pom

BOM

BOM定义

BOM(Bill of Material),物料清单。将某一个领域相应的jar统一称之为XXX-BOM予以管理(例如inf-bom、spring-framework-bom),并不是Maven的规定,只是一种约定俗成(convention over configuration)。Gradle 也采用了这样的思路

为什么要使用bom

(1)避免开发同学需要关心各个API的版本关系:BOM这种方式在 dependencyManagement 指定了各个依赖的版本,并不会使得这些依赖并被真正引入(仅做版本管理使用)。在dependency中还要按需引入、但省去了需要制定version的麻烦。

(2)便于历史版本API的收归。

(3)可以说逐渐使用 bom 来统一管理某个团队(领域)的 API 版本已成为一种趋势。

scope 的含义

scope元素的作用:控制 dependency 元素的使用范围。通俗的讲,就是控制 Jar 包在哪些范围被加载和使用。
scope具体含义如下:

compile(默认)

含义:compile 是默认值,如果没有指定 scope 值,该元素的默认值为 compile。被依赖项目需要参与到当前项目的编译,测试,打包,运行等阶段。打包的时候通常会包含被依赖项目。

provided

含义:被依赖项目理论上可以参与编译、测试、运行等阶段,相当于compile,但是再打包阶段做了exclude的动作。
适用场景:例如, 如果我们在开发一个web 应用,在编译时我们需要依赖 servlet-api.jar,但是在运行时我们不需要该 jar 包,因为这个 jar 包已由应用服务器提供,此时我们需要使用 provided 进行范围修饰。

runtime

含义:表示被依赖项目无需参与项目的编译,但是会参与到项目的测试和运行。与compile相比,被依赖项目无需参与项目的编译。
适用场景:例如,在编译的时候我们不需要 JDBC API 的 jar 包,而在运行的时候我们才需要 JDBC 驱动包。

test

含义: 表示被依赖项目仅仅参与测试相关的工作,包括测试代码的编译,执行。
适用场景:例如,Junit 测试。

system

含义:system 元素与 provided 元素类似,但是被依赖项不会从 maven 仓库中查找,而是从本地系统中获取,systemPath 元素用于制定本地系统中 jar 文件的路径。例如:

1
2
3
4
5
6
7
<dependency>
<groupId>org.open</groupId>
<artifactId>open-core</artifactId>
<version>1.5</version>
<scope>system</scope>
<systemPath>${basedir}/WebContent/WEB-INF/lib/open-core.jar</systemPath>
</dependency>

import

它只使用在中,表示从其它的pom中导入dependency的配置,例如 (B项目导入A项目中的包配置):

想必大家在做SpringBoot应用的时候,都会有如下代码:

1
2
3
4
5
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.3.3.RELEASE</version>
</parent>

继承一个父模块,然后再引入相应的依赖。
假如说,我不想继承,或者我想继承多个,怎么做?

我们知道Maven的继承和Java的继承一样,是无法实现多重继承的,如果10个、20个甚至更多模块继承自同一个模块,那么按照我们之前的做法,这个父模块的dependencyManagement会包含大量的依赖。如果你想把这些依赖分类以更清晰的管理,那就不可能了,import scope依赖能解决这个问题。你可以把 dependencyManagement 放到单独的专门用来管理依赖的pom中,然后在需要使用依赖的模块中通过import scope依赖,就可以引入 dependencyManagement。例如可以写这样一个用于依赖管理的pom:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.test.sample</groupId>
<artifactId>base-parent1</artifactId>
<packaging>pom</packaging>
<version>1.0.0-SNAPSHOT</version>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactid>junit</artifactId>
<version>4.8.2</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactid>log4j</artifactId>
<version>1.2.16</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>

然后我就可以通过非继承的方式来引入这段依赖管理配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.test.sample</groupId>
<artifactid>base-parent1</artifactId>
<version>1.0.0-SNAPSHOT</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependency>
<groupId>junit</groupId>
<artifactid>junit</artifactId>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactid>log4j</artifactId>
</dependency>

注意:import scope只能用在 dependencyManagement 里面。这么多的 scope 里面,import 也因此是最危险的,因为 import 会把依赖直接展开,而不是用间接传递的方式在新应用中体现,会覆盖 parent 和 dependency(因为寻根路径最短,链接器会最先被链接上),而且无法被 exclude 排除

这样,父模块的pom就会非常干净,由专门的packaging为pom来管理依赖,也契合的面向对象设计中的单一职责原则。此外,我们还能够创建多个这样的依赖管理pom,以更细化的方式管理依赖。这种做法与面向对象设计中使用组合而非继承也有点相似的味道。

那么,如何用这个方法来解决SpringBoot的那个继承问题呢?

配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>1.3.3.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

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

这样配置的话,自己的项目里面就不需要继承SpringBoot的module了,而可以继承自己项目的module了。

scope的依赖传递

A–>B–>C。当前项目为A,A依赖于B,B依赖于C。知道B在A项目中的scope,那么怎么知道C在A中的scope呢?答案是:
当C是test或者provided时,C直接被丢弃,A不依赖C;
否则A依赖C,C的scope继承于B的scope。

依赖矩阵表格见:《Introduction to the Dependency Mechanism》

scope 与 optional 的区别

maven的 scope 决定依赖的包是否加入本工程的classpath下。- 某些 scope 连本项目的 classpath 都会被影响。使用本项目的项目无论如何都不能绕过 scope 的影响,scope 才是最彻底的对传播的隔离(比如 provided)。

optional仅限制依赖包的传递性,不影响依赖包的classpath。- 不影响本项目生成的 jar,影响使用本项目的项目。

scope 与 optional 都可以用重新声明依赖的方式来引入缺失依赖。

比如一个工程中

A->B, B->C(scope:compile, optional:true),B的编译/运行/测试classpath都有C,A中的编译/运行/测试classpath都不存在C(尽管C的scope声明为compile),A调用B哪些依赖C的方法就会出错。

A->B, B->C(scope:provided), B的编译/测试classpath有C,A中的编译/运行/测试classpath都不存在C,但是A使用B(需要依赖C)的接口时就会出现找不到C的错误,此时,要么是手动加入C的依赖,即A->C,否则需要容器提供C的依赖包到运行时classpath。

对于纯粹作为 lib 来用的 jar,rovided over optional。因为出了 test 这个 phase,连 jar 都不能独立 run 起来。optional 本身是一个可以自己在各种 phase run,但被依赖的时候则会去除打包配置,依然会影响 classpath。

debug 小技巧

  • 在子工程里显式地指定某个依赖版本看是否能够消除错误。
  • 使用 ide 的依赖分析工具,如 mvn dependency 插件(这个分析工具只是运行时分析,有误导性)或者 idea 的 dependency analyzer。
  • 显式地消除依赖:
1
2
3
4
5
<exclusions>
<exclusion>
<groupId>org.springframework</groupId> <artifactId>spring-context-support</artifactId>
</exclusion>
</exclusions>

《Optional Dependencies and Dependency Exclusions》

Optional dependencies are used when it’s not possible (for whatever
reason) to split a project into sub-modules. The idea is that some of
the dependencies are only used for certain features in the project and
will not be needed if that feature isn’t used. Ideally, such a feature
would be split into a sub-module that depends on the core
functionality project. This new subproject would have only
non-optional dependencies, since you’d need them all if you decided to
use the subproject’s functionality.

However, since the project cannot be split up (again, for whatever
reason), these dependencies are declared optional. If a user wants to
use functionality related to an optional dependency, they have to
redeclare that optional dependency in their own project. This is not
the clearest way to handle this situation, but both optional
dependencies and dependency exclusions are stop-gap solutions.

optional 是大项目无法被切割成小的子模块的无奈选择,如果项目要使用被依赖模块的可选功能,必须显式地再声明一遍可选依赖,否则会产生调用出错。optional 阻断了传递依赖。

1
2
3
4
5
6
7
8
9
10
11
12
13
<project>
...
<dependencies>
<!-- declare the dependency to be set as optional -->
<dependency>
<groupId>sample.ProjectA</groupId>
<artifactId>Project-A</artifactId>
<version>1.0</version>
<scope>compile</scope>
<optional>true</optional> <!-- value will be true or false only -->
</dependency>
</dependencies>
</project>

Project-A -> Project-B Project-X -> Project-A

A 的类路径里有 B,而 X 的类路径里无 B。

怎样使用 maven 的 version 插件?

参考:《Use the Latest Version of a Dependency in Maven》

Maven2 also provided two special metaversion values to achieve the
result: LATEST and RELEASE.

但这两个值已经过期了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>versions-maven-plugin</artifactId>
<version>2.7</version>
<configuration>
<excludes>
<exclude>org.apache.commons:commons-collections4</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
1
2
3
4
5
6
7
8
9
10
# 校验当前的项目有哪些依赖有 newer version
mvn versions:display-dependency-updates

# 会改变 pom 的版本,产生一个备份:pom.xml.versionsBackup,会把 1.1.1-SNAPSHOT,转成 1.1.1
mvn versions:use-releases

# 使用下一个版本的 release
mvn versions:use-next-releases
# 使用最新的 release
mvn versions:use-latest-releases
1
2
3
4
5
6
7
8
9
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>versions-maven-plugin</artifactId>
<version>2.7</version>
<configuration>
<rulesUri>http://www.mycompany.com/maven-version-rules.xml</rulesUri>
<!-- <rulesUri>file:///home/andrea/maven-version-rules.xml</rulesUri> -->
</configuration>
</plugin>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<ruleset comparisonMethod="maven"
xmlns="http://mojo.codehaus.org/versions-maven-plugin/rule/2.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://mojo.codehaus.org/versions-maven-plugin/rule/2.0.0
http://mojo.codehaus.org/versions-maven-plugin/xsd/rule-2.0.0.xsd">
<ignoreVersions>
<ignoreVersion type="regex">.*-beta</ignoreVersion>
</ignoreVersions>
</ruleset>

<ruleset comparisonMethod="maven"
xmlns="http://mojo.codehaus.org/versions-maven-plugin/rule/2.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://mojo.codehaus.org/versions-maven-plugin/rule/2.0.0
http://mojo.codehaus.org/versions-maven-plugin/xsd/rule-2.0.0.xsd">
<rules>
<rule groupId="com.mycompany.maven" comparisonMethod="maven">
<ignoreVersions>
<ignoreVersion type="regex">.*-RELEASE</ignoreVersion>
<ignoreVersion>2.1.0</ignoreVersion>
</ignoreVersions>
</rule>
</rules>
</ruleset>

参考:

  1. 《Maven依赖中scope的含义》
  2. 《maven scope-provided 与 optional 区别》