《Maven 实战》笔记
当项目需要被其他项目引用时,使用 mvn clean install
安装到本地仓库,其他项目就可以引用该项目。
我们打包的默认 jar 包不能直接运行的,因为带有 main 方法的信息类不会添加到 mainfest 中( jar 文件中 META-INF/MANIFEST.MF 文件,没有 Main-Class
一行)。为了生成可执行的 jar 文件,需要借助 maven-shade-plugin
,配置该插件。
使用 Maven 命令快速创建一个 maven 项目
使用命令 mvn archetype:generate
根据提示选择需要的骨架,输入 groupId,artifactId,version 等信息,创建项目。需要注意的是:maven2 中使用上面的命令,会去下载最新的版本,而不是稳定的版本,可能导致运行失败。maven3 则默认是下载稳定的版本。
坐标
- groupId:命名通常为域名反向一一对应。比如
google.com
可以命名为com.google
。 - artifactId:命名推荐的做法是使用实际的项目名称作为 artifactId 的前缀。比如项目名是
demo
,下面有个hello
模块,就可以命名为demo-hello
。 - version:定义当前项目的版本信息。
- packaging:定义元素打包方式,打包方式通常与所生成的构件文件扩展名对应。例如指定为
jar
则构件的文件扩展名为 .jar,war 也如此。不定义 packaging 时,默认打包为 jar。打包方式会影响到构建的生命周期,比如 jar 和 war 打包会使用不同的命令。 - classifier:定义构建输出的一些附属构建。例如项目打包生成 demo.jar 文件,打包过程中可能还需要 javadoc 文件,就可以配置对应的插件来生成 javadoc 文件。
依赖的配置
- type:依赖的类型,对应于项目坐标定义的 packaging。大部分情况下,该元素不必声明,默认为 jar。
- scope:依赖的作用范围。
- optional:标记依赖是否可选。
- exclusions:用来排除传递性依赖。
依赖的范围
前置知识:Maven 编译项目主代码的时候使用一套 classpath,在编译执行测试的时候使用另一套 classpath,而实际运行 Maven 项目时候又会使用一套 classpath,因此总共有三套 classpath。
依赖范围就是来控制与这三种 classath(编译 classpath,测试 classpath,运行 classpath)的关系。如果没有指定依赖范围,就会默认使用 compile
依赖范围。
依赖范围与 classpath 关系
| 依赖范围
(Scope) | 对于编译 classpath 有效 | 对于测试 classpath有效 | 对于运行时 classpath 有效 | 例子 |
---|---|---|---|---|
compile | Y | Y | Y | spring-core |
test | – | Y | – | JUnit |
provided | Y | Y | – | servlet-api |
runtime | – | Y | Y | JDBC 驱动实现 |
system | Y | Y | – | 本地的 maven 仓库外的类库文件 |
传递性依赖和依赖范围
传递性依赖:A –> B –> C,假设 A 依赖 B,B 依赖 C。称 A 对于 B 是第一直接依赖,B 对于 C 是第二直接依赖,A 对于 C 是传递性依赖。
上面说了依赖范围不仅与三种 classpath 有关系,还对传递性依赖产生影响。如下表格所示,最左边的一行表示第一依赖范围,最上面一行表示第二直接依赖范围。
依赖范围影响传递性依赖
compile | test | provided | runtime | |
---|---|---|---|---|
compile | compile | – | – | runtime |
test | test | – | – | test |
provided | provided | – | provided | provided |
runtime | runtime | – | – | runtime |
助记规律:
- 当第二直接依赖范围是
compile
时,传递性依赖范围与第一直接依赖范围一致。 - 当第二直接依赖范围是
test
时,依赖不会得以传递。 - 当第二直接依赖范围是
provided
时,只传递第一依赖范围也为provided
的依赖。 - 当第二直接依赖范围是
runtime
时,传递性依赖范围与第一直接依赖范围一致,但compile
除外,此时传递性依赖范围为runtime
。
依赖调节
就近原则:A –> B –> C(1.0),A –> B –> D –> C(2.0),前者的路径长度为2,后者为3,因此 C(1.0) 被解析使用。
先声明先使用:遇到长度一样的,无法使用就近原则,就根据声明顺序,先声明的先解析使用。
可选依赖
假设 A 依赖 B,B 依赖 X 和 Y,B 对于 X 和 Y 的依赖是可选依赖。A –> B,B –> X(可选),B –> Y(可选),可选依赖不会得到传递,换句话说选择 X 或者 Y,对于 A 都不会有任何影响。
可选依赖在 dependency
标签中需要添加 <optional>true</optional>
用来标记为可选依赖,因此 B 需要添加 X 和 Y 的依赖,并且分别在 dependency
标签加上 <optional>true</optional>
。
当需要指定所使用的依赖时,需要在外层声明需要使用的依赖,上面的例子就需要在 A 中引入 X 或者 Y 的依赖。在理想的情况下,尽量避免可选依赖的使用。
远程仓库认证
当我们使用 <repository></repository>
配置好一个远程仓库后,大部分情况下无需认证就可以直接访问,但是处于安全考虑,我们需要提供一些认证信息才能访问。此时,我们就需要配置认证信息,一般在 setting.xml
文件中配置,如下:
1 |
|
文件中配置了认证信息,账号密码都是 admin
,需要注意的是 id 的值要与配置的远程仓库的 id 一致。
部署至远程仓库
编写项目的 pom.xml
文件,配置 distributionManagement 元素,代码如下:
1 |
|
其中 distributionManagement 包含两个子元素,前者表示发布版本构件的仓库,后者表示快照版本的仓库。两个元素都需要配置 id,name,url,id 为远程仓库的唯一标识,name 为了方便阅读(取名),url 远程仓库地址。
发布到远程仓库的时候,往往需要认证,认证步骤与远程仓库认证一致。之后使用命令 mvn clean deploy
maven 就会把项目构建输出到对应的远程仓库。
镜像
如果仓库 X 可以提供仓库 Y 存储的所有内容,那么就可以认为 X 是 Y 的一个镜像。换句话说,任何一个从 Y 仓库获取的构件,都可以从它的镜像中获取。由于地理原因,国内访问 maven 中央仓库很慢,因此就需要配置一个镜像,通常使用阿里云的镜像居多。配置文件如下:
1 |
|
改配置文件中 <mirrOf>
的值为 central,表示任何访问该仓库的请求都会转到这个镜像中。如果镜像需要认证,也可以基于这个 <id>
aliyunmaven,配置仓库认证。
Maven 的 <mirrOf>
标签还支持一些更高级的配置:
<mirrorOf>*</mirrorOf>
匹配所有远程仓库<mirrorOf>external: *</mirrorOf>
匹配所有不在本机上的远程仓库。<mirrorOf>repo1,repo2</mirrorOf>
匹配仓库 rpeo1,repo2,使用逗号分隔多个远程仓库。<mirrorOf>*,!repo1</mirrorOf>
匹配所有远程仓库,repo1除外。
生命周期
maven 的生命周期其实就是软件从初始化到发布上线经历的一系列过程。maven 将这个过程抽象化了,每个周期所执行的操作都是交给插件完成。
maven 拥有三套相互独立的生命周期,分别是 clean,default,site。clean 负责清理项目,default 负责构建项目,site 负责建立项目站点。
每个生命周期都包含一些阶段(phase),这些阶段都是有顺序的,并且后面的阶段依赖于前面的阶段。比如 clean 生命周期包含: pre-clean
,clean
,post-clean
阶段,当用户调用 pre-clean,只有 pre-clean 阶段会执行,当调用 post-clean 时,pre-clean,clean,post-clean 都会执行。由于三个生命周期互不影响,当调用某个生命周期的某个阶段时,对其他的生命周期没有任何影响。
clean 生命周期阶段
- pre-clean 执行清理前需要完成的工作
- clean 清理上一次构建生成的文件
- post-clean 执行清理后需要完成的工作
default 生命周期阶段(主要)
- validate 验证项目是否正确并且所有必要的信息都可用
- compile 编译项目的源代码
- test 使用合适的单元测试框架测试编译的源代码。这些测试不应该要求打包或部署代码
- package 将编译后的代码打包成可分发的格式,例如 JAR。
- verify 对集成测试的结果进行任何检查,以确保满足质量标准
- install 将包安装到本地存储库中,作为本地其他项目的依赖项
- deploy 在构建环境中完成,将最终包复制到远程存储库以与其他开发人员和项目共享。
site 生命周期阶段
- pre-site 执行生成项目站点前需要完成的工作
- site 生成项目站点文档
- post-site 执行生成项目站点后需要完成的工作
- site-deploy 将生成的项目站点发布到服务器上
命令行与生命周期
一些常见的 Maven 命令,解释其执行生命周期:
mvn clean
调用 clean 生命周期的 clean 阶段,即执行pre-clean
和clean
阶段。mvn test
调用 default 生命周期的 test 阶段,即执行validate
、compile
、test
阶段。mvn clean install
调用 clean 生命周期的 clean 阶段,以及 default 生命周期的 install 阶段。即执行pre-clean
和clean
阶段,以及validate
到install
的所有阶段。mvn clean deploy site-deploy
调用 clean 生命周期的 clean 阶段,default 生命周期的 deploy 阶段,以及 site 生命周期的 site-deploy 阶段,实际执行过程按照上面类推。
插件目标和插件绑定
插件目标
一个插件往往能执行多个任务,因此插件具有多个功能,插件的每个功能就是一个**插件目标 (goal)**。
maven-dependency-plugin
就有十多个目标,每个目标对应一个功能。例如:dependency:list
、dependency:tree
分别是不同的功能,这是一种通用的写法,冒号前面是插件前缀,冒号后面是该插件的目标。类似的,compiler:compile
(这是 maven-compiler-plugin 的 compile 目标)和 surefire:test(这是 maven-surefire-plugin 的 test 目标)。
插件绑定
maven 的生命周期与插件相互绑定,具体而言就是生命周期的阶段与插件的目标相互绑定。例如 mvn compile
就是 default 生命周期 compile 阶段与 maven-compiler-plugin
插件的 compile
目标绑定。
内置绑定
为了简化配置,maven 为一些核心的生命周期阶段绑定了很多插件的目标。当用户调用对于的生命周期阶段时,绑定的插件目标就会执行相应的任务。
例如:clean 生命周期的 clean 阶段,就绑定了 maven-clean-plugin
插件的 clean
目标绑定。site 生命周期的 site 阶段绑定 maven-site-plugin
插件的 site
目标,site-deploy 阶段绑定到 maven-site-pugin
插件的 deploy
目标。
由于项目的打包类型会影响构建的具体过程,因此 default 的生命周期阶段与插件的目标绑定关系与打包类型有关系。
default 生命周期的内置插件绑定关系(打包类型为 jar)
生命周期阶段 | 插件目标 | 执行任务 |
---|---|---|
process-resources | maven-resources-plugin:resources | 复制主资源文件至输出目录 |
compile | maven-compiler-plugin:compile | 编译主代码至输出目录 |
process-test-resources | maven-resources-plugin:testResources | 复制测试资源文件至测试输出目录 |
test-compile | maven-compiler-plugin:testCompile | 编译测试代码至测试输出目录 |
test | maven-surefire-plugin:test | 执行测试用例 |
package | maven-jar-plugin:jar | 创建项目 jar 包 |
install | maven-install-plugin:install | 将项目构建输出到本地仓库 |
deploy | maven-deploy-plugin:deploy | 将项目构建输出到远程仓库 |
上面只是列举了 default 生命周期绑定了插件的阶段,并非 default 的所有阶段。
自定义绑定
除了上面的内置绑定外,插件还支持自定义绑定,拿 maven-compiler-plugin
插件举例
1 |
|
上面代码将 maven-compiler-plugin
插件的 testCompile
目标绑定到 default 上面周期的 verify 阶段。当运行 mvn verify 时,该插件的 testCompile 目标就会执行。phase 表示绑定的声明周期阶段,因为插件可以在一个生命周期阶段运行多个方法,所以可以配置多个目标,即多个 goal。
有的时候,没有使用 phase,插件目标目标也能绑定到声明周期的阶段中去。这是因为很多插件在目标的编写时候已经定义好了默认绑定的阶段。可以使用命令查看 mvn help:describe -Dplugin=compiler -Ddetail
查看插件的详细信息,默认绑定的阶段等。
当一个阶段有多个插件目标绑定时,执行顺序与插件的声明顺序有关,先声明先执行。
插件配置
插件目标像 Java 中的函数一样,执行相应的功能,并且还有参数,这些参数的具体值,我们可以配置,传递给插件目标。
命令行插件配置
命令行插件配置算是比较经典的配置手段了,在 Java 中可以使用 -D
参数配置系统属性。Maven 简单的重用了该参数,在检查插件的时候检查系统属性,实现了插件参数的配置。
例如:maven-compiler-plugin
插件就提供了一个 maven.test.skip
的系统属性,通过配置是否跳过测试。以下是该插件 testCompile
目标的 skip 参数详细信息。
1 |
|
可以看到该参数可以使用 maven.test.skip
,因此可以编写命令 maven install -Dmaven.test.skip=true
用来跳过测试。
POM 插件配置
对于一些固定常用的参数,可以直接在 pom 文件中配置,使用 configuration
配置 参数,如下:
1 |
|
如果需要插件在不同声明周期执行相同的目标,但是执行的参数不一样,就需要另外配置。
1 |
|
如上同一插件的同一目标绑定了不同的声明周期阶段,具体的参数配置只需要在各自的 configuration
配置即可。
聚合与继承
聚合
为了方便使用命令管理多个模块,就需要用到聚合。例如:项目中有 A,B 两个模块,需要对两个项目都执行清理 操作,因此你需要跑到 A 模块下执行 mvn clean
命令,然后再跑到 B 模块下执行相同的命令。模块较多的话就比较麻烦。
使用聚合就可以解决上述问题。新建一个聚合模块,然后将聚合模块的打包方式修改成 pom,并使用标签 modules
引入模块 A,B,然后对聚合模块操作即可。示例代码:
1 |
|
聚合模块可以与其他模块同级,同级只需要将 module 改为 <module>../A模块目录名称</module>
,特别注意的是聚合模块的打包方式一定要修改成pom。
继承
继承的出现是为了消除重复,Java 如此,Maven 中的 pom 也是如此。假如有 A,B两个模块都需要 spring-core
2.0 版本的依赖,正常的做法就是在两个模块中分别声明同一个 spring-core
依赖。
使用继承消除重复配置,新建一个模块 C
,修改打包方式为 pom (与聚合操作一致),接着引入 spring-core
依赖,即可。这时 A,B 模块只需要继承 C 的 pom 即可,代码如下。
1 |
|
引入 C 模块的坐标信息,并使用 relativePath
指向 C 模块的 pom 文件路径。Maven 会首先根据 relativePath
找 pom,找不到就去本地仓库找,如果不配置 realtivePath,那默认值是 ../pom.xml
,也就是说 Maven 默认父 模块在当前模块的上一层目录。
在上面的例子中 A,B 模块可以不用声明 gruopId 和 version,这些信息都从 C 模块中继承过来了,如果子模块要使用自己的 groupId 和 version 直接显示的声明即可。
Maven 可继承的 POM 元素
- groupId 项目组 ID ,项目坐标的核心元素。
- version 项目版本,项目坐标的核心元素。
- description 项目的描述信息。
- organization 项目的组织信息。
- inceptionYear 项目的创始年份。
- url 项目的 url 地址。
- develoers 项目的开发者信息。
- contributors 项目的贡献者信息。
- distributionManagerment 项目的部署信息。
- issueManagement 缺陷跟踪系统信息。
- ciManagement 项目的持续继承信息。
- scm 项目的版本控制信息。
- mailingListserv 项目的邮件列表信息。
- properties 自定义的 Maven 属性。
- dependencies 项目的依赖配置。
- dependencyManagement 醒目的依赖管理配置。
- repositories 项目的仓库配置。
- build 包括项目的源码目录配置、输出目录配置、插件配置、插件管理配置等。
- reporting 包括项目的报告输出目录配置、报告插件配置等。
依赖管理
上面说到依赖可以继承,但是并不是每一个模块都需要引入父模块的依赖。例如一个工具模块,就不太需要 spring
这些依赖包,但是因为继承关系,导致工具模块也引入了这些包,为了避免这种情况就需要使用新的元素 dependencyManagement。
dependencyManagement 可以让子类自行的选择继承父模块的依赖,只需要将父模块的依赖用元素 dependencyManagement
包裹起来即可。例如父模块依赖代码如下:
1 |
|
子模块引用父模块依赖,代码如下:
1 |
|
子模块 pom 直接引用所需要的依赖坐标即可,上面代码只引用了父模块的 junit 依赖,因此 spring-core 不会被引入。子模块还去掉了依赖的 version 和 scope 信息,这是因为继承了父模块的 pom,完整的依赖声明已经在父 pom 中了,因此子模块只需要 groupId 和 artifactId 就能获取对应的依赖信息,这样使用也能使所有子模块统一依赖版本,降低依赖冲突概率。
import 和 dependencyManagement
上面的章节说到过依赖范围 import
,import 需要配合 dependencyManagement 使用。作用是将目标模块的 pom 中的 dependencyManagement 配置,导入并合并到当前模块的 pom 的 dependencyManagement 配置中。
例如想要在另一个模块中使用父模块完全一样的 dependencyManagement 配置,需要在 pom 编写如下代码:
1 |
|
插件管理
插件管理的思想和依赖管理一致。在父 pom 中使用 pluginManagement
包裹插件,即可达到与依赖管理一样的效果。
子模块使用插件只需要声明 groupid
和 artifactId
即可。若子模块需要修改插件的配置信息,只需要在模块中覆盖父类的配置即可。
继承和聚合关系
都要将打包方式改为 pom,一个模块可以既是聚合模块,又是父模块。
约定优于配置
我们都知道 Maven 项目的源代码文件夹是 src/main/java,编译输出文件夹是 target/classes/,这是 maven 的约定,我们只需要准守即可。当然想要自己改变这些约定也可以做到,但是不推荐。我们准守的这些约定实际上是 maven 的超级 pom 帮我们配置了这些信息。
所有的 maven 项目都隐式地继承了超级 pom,这个超级 pom 配置了源代码目录路径,编译输出路径,测试代码路径等等,这些配置信息就是我们要准守的约定。超级 pom 可以到 maven 的安装目录下 /lib/maven-model-builder-x.x.x.jar
文件(x.x.x 代表的是 maven 版本 ),解压之后找到 org/apache/maven/pom-4.0.0.xml
文件即可查看配置信息。这是我本机的超级 pom 的部分配置信息
1 |
|
反应堆
之前使用到聚合的知识,用一个聚合模块包含多个模块,只要对聚合模块操作即可影响到包含的模块。我们把包含的这一堆模块叫做反应堆。为啥叫反应堆呢,因为需要发生反应,在构建这些模块的时候肯定存在一个构建顺序,依次构建。
例如聚合模块 A,按顺序包含模块 B,C,D。按照正常理解,对 A 模块进行构建时,我们会认为,B,C,D 会按照顺序依次构建,但实际的构建顺序却不一定是我们预期的那样。
实际的构建顺序是这样的:Maven 先按照顺序读取 pom,如果 pom 没有依赖模块,那么就构建该模块,否则就构建其依赖模块,若依赖模块还依赖其他模块,则进一步构建依赖模块。
在实际开发中可以使用一些参数,来决定需要构建的模块,或者指定模块构建的顺序。假设模块 A,模块 B 都依赖于模块 C。
- -am, 同时构建所列模块的依赖模块。例如:
mvn clean install -pl A -am
,结果顺序:C ,A。 - -amd 同时构建依赖于所列模块的模块。例如:
mvn clean install -pl C -amd
,结果顺序:C,A,B - -pl 构建指定的模块,模块间用逗号分隔。例如:
mvn clean install -pl A,B
- -rf 在完整的构建顺序基础上指定从哪个模块开始构建。例如:
mvn clean install -pl C -amd -rf B
结果顺序:C,B,A