Maven基础知识
一、Maven的简介
Maven的初心是方便Java项目开发和管理,在Maven出现之前,我们需要手动管理项目的依赖包,如当项目中使用了commons logging
我们就必须把commons logging的jar包放入classpath,并且依赖包可能还存在上级依赖,使得依赖的管理更加繁琐麻烦;其次,我们要确定项目的目录结构。例如,src
目录存放Java源码,resources
目录存放配置文件,bin
目录存放编译生成的.class
文件;此外,我们还需要配置环境,例如JDK的版本
,编译打包
的流程,当前代码的版本号
。
这些工作难度不大,但是非常琐碎且耗时。如果每一个项目都自己搞一套配置,肯定会一团糟。我们需要的是一个标准化的Java项目管理和构建工具。Maven就是是专门为Java项目打造的管理和构建工具,它的主要功能有:
- 提供了一套标准化的项目结构;
- 提供了一套标准化的构建流程(编译,测试,打包,发布……);
- 提供了一套依赖管理机制。
二、Maven的目录结构
一个使用Maven管理的普通的Java项目,它的目录结构默认如下:
1 | a-maven-project |
项目的根目录a-maven-project
是项目名,它有一个项目描述文件pom.xml
,存放Java源码的目录是src/main/java
,存放资源文件的目录是src/main/resources
,存放测试源码的目录是src/test/java
,存放测试资源的目录是src/test/resources
,最后,所有编译、打包生成的文件都放在target
目录里。这些就是一个Maven项目的标准目录结构。
所有的目录结构都是约定好的标准结构,==我们千万不要随意修改目录结构==。使用标准结构不需要做任何配置,Maven就可以正常使用。
三、Maven的核心文件
我们再来看最关键的一个项目描述文件pom.xml
,它的内容长得像下面:
1 | <project ...> |
其中,groupId
类似于Java的包名,通常是公司或组织名称,artifactId
类似于Java的类名,通常是项目名称,再加上version
,一个Maven工程就是由groupId
,artifactId
和version
作为唯一标识。我们在引用其他第三方库的时候,也是通过这3个变量确定。使用<dependency>
声明一个依赖后,Maven就会自动下载这个依赖包并把它放到classpath中。
注:只有以
-SNAPSHOT
结尾的版本号会被Maven视为开发版本,开发版本每次都会重复下载,这种SNAPSHOT版本只能用于内部私有的Maven repo,公开发布的版本不允许出现SNAPSHOT。
四、Maven的依赖管理
Maven解决了依赖管理问题。例如,我们的项目依赖abc
这个jar包,而abc
又依赖xyz
这个jar包:
1 | ┌──────────────┐ |
当我们声明了abc
的依赖时,Maven自动把abc
和xyz
都加入了我们的项目依赖,不需要我们自己去研究abc
是否需要依赖xyz
。因此,Maven的第一个作用就是==解决依赖管理==。我们声明了自己的项目需要abc
,Maven会自动导入abc
的jar包,再判断出abc
需要xyz
,又会自动导入xyz
的jar包,这样,最终我们的项目会依赖abc
和xyz
两个jar包。
我们来看一个复杂依赖示例:
1 | <dependency> |
当我们声明一个spring-boot-starter-web
依赖时,Maven会自动解析并判断最终需要大概二三十个其他依赖:
如果我们自己去手动管理这些依赖是非常费时费力的,而且出错的概率很大。
五、Maven的依赖范围
Maven定义了几种依赖范围,分别是compile
、test
、runtime
和provided
:
scope | 说明 | 示例 |
---|---|---|
compile | 编译运行时需要用到该jar包(默认) | commons-logging |
test | 测试编译运行时需要用到该jar包 | junit |
runtime | 编译时不需要,但运行时需要用到 | mysql |
provided | 编译时需要用到,但运行时由JDK或某个服务器提供 | servlet-api |
其中,默认的compile
是最常用的,Maven会把这种类型的依赖直接放入classpath。test
依赖表示仅在测试时使用,正常运行时并不需要。最常用的test
依赖就是JUnit:
1 | <dependency> |
runtime
依赖表示编译时不需要,但运行时需要。最典型的runtime
依赖是JDBC驱动,例如MySQL驱动:
1 | <dependency> |
provided
依赖表示编译时需要,但运行时不需要。最典型的provided
依赖是Servlet API,编译的时候需要,但是运行时,Servlet服务器内置了相关的jar,所以运行期不需要:
1 | <dependency> |
最后一个问题是,Maven如何知道从何处下载所需的依赖?也就是相关的jar包?答案是Maven维护了一个中央仓库
(repo1.maven.org),所有第三方库将自身的jar以及相关信息上传至中央仓库,Maven就可以从中央仓库把所需依赖下载到本地。
Maven并不会每次都从中央仓库下载jar包。一个jar包一旦被下载过,就会被Maven自动缓存
在本地目录(用户主目录的.m2
目录),所以,除了第一次编译时因为下载需要时间会比较慢,后续过程因为有本地缓存,并不会重复下载相同的jar包。
六、Maven的构建流程
Maven不但有标准化的项目结构,而且还有一套标准化的构建流程
,可以自动化实现编译,打包,发布等。Maven基于构建生命周期的中心概念,这意味着构建和分发特定工件(项目)的过程是明确定义的。对于构建项目的人来说,这意味着只需要学习一小部分命令即可构建任何Maven项目,POM将确保他们获得所需的结果。共有三个内置构建生命周期:default
、clean
和site
,default
生命周期处理项目部署,clean
生命周期处理项目清理,而site
生命周期处理项目站点的创建。
这些构建生命周期
中的每一个都由不同的构建阶段
列表来定义,其中构建阶段表示生命周期中的一个阶段。例如,default
生命周期由以下phases
组成(有关生命周期阶段的完整列表,请参阅Maven的生命周期手册
):
validate
:验证项目是否正确并且所有必要的信息均可用;compile
:编译项目的源代码;test
:使用合适的单元测试框架测试编译的源代码。这些测试不应要求测试源代码被打包或部署;package
:获取编译后的代码并将其打包为其可分发的格式,例如JAR;verify
:对集成测试的结果进行检查以确保满足质量标准;install
:将包安装到本地存储库中,以用作本地其他项目的依赖项;deploy
:在构建环境中完成,将最终包复制到远程存储库,以便与其他开发人员和项目共享。
这些生命周期阶段(加上此处未显示的其他生命周期阶段)按顺序执行以完成default
生命周期。考虑到上面的生命周期阶段,这意味着当使用default
生命周期时,Maven将首先验证项目,然后尝试编译源代码,针对测试运行这些测试源码,打包二进制文件(例如jar),针对该jar包运行集成测试,验证集成测试,将验证的包安装到本地存储库,然后将安装的包部署到远程存储库。
你应该选择与您的结果相匹配的阶段。如果你想要打包jar,运行package
。如果你想运行单元测试,请运行test
。如果你不清楚,优先运行verify
:
1 | mvn verify |
在执行verify
之前,此命令按顺序执行每个default
生命周期阶段(validate
、compile
、package
等)。你只需要调用要执行的最后一个构建阶段,在本例中为verify
。大多数情况下该命令的效果与mvn package
相同。但是,如果有集成测试,这些测试也将被执行。在verify
阶段还可以进行一些额外的检查,例如如果你的代码是根据预定义的checkstyle规则编写的。
在构建环境中,使用以下调用干净地构建工件并将其部署到共享存储库中。
1 | mvn clean deploy |
同一命令可用于多模块场景(即具有一个或多个子项目的项目)。Maven遍历每个子项目并执行clean,然后执行deploy(包括之前的所有构建阶段步骤)。
然而,尽管构建阶段负责构建生命周期中的特定步骤,但它执行这些职责的方式可能会有所不同。这是通过声明绑定到这些构建阶段的插件目标(plugin goals
)来完成的。插件目标代表一个特定任务(比构建阶段更精细),有助于项目的构建和管理。它可能绑定到零个或多个构建阶段,未绑定到任何构建阶段的目标可以通过直接调用在构建生命周期之外执行。执行顺序取决于目标和构建阶段的调用顺序,例如考虑下面的命令:clean
和package
参数是构建阶段,而dependency:copy-dependencies
是插件目标。
1 | mvn clean dependency:copy-dependencies package |
如果要执行此操作,则将首先执行clean阶段
(这意味着它将运行clean生命周期
的所有先前阶段,加上clean阶段本身),然后是dependency:copy-dependencies目标
,最后执行package阶段
(以及default
生命周期的所有先前构建阶段)。
此外,如果一个目标绑定到一个或多个构建阶段,则该目标将在所有这些阶段中都被调用。此外,构建阶段还可以有零个或多个与其绑定的目标,如果构建阶段没有绑定目标,则该构建阶段将不会执行。但如果它有一个或多个目标与之绑定,它将执行所有这些目标。
其实我们类比一下就明白了:
- lifecycle相当于Java的package,它包含一个或多个phase;
- phase相当于Java的class,它包含一个或多个goal;
- goal相当于class的method,它其实才是真正干活的。
大多数情况,我们只要指定phase,就默认执行这些phase默认绑定的goal,只有少数情况,我们可以直接指定运行一个goal,例如,启动Tomcat服务器:
1 | mvn tomcat:run |
七、Maven的生命周期手册
1.Clean生命周期
Phase | Description |
---|---|
pre-clean |
在实际项目清理之前执行所需的流程 |
clean |
删除先前构建生成的所有文件 |
post-clean |
执行完成项目清理后所需的流程 |
2.Default生命周期
Phase | Description |
---|---|
validate |
验证项目是否正确并且所有必要的信息均可用 |
initialize |
初始化构建状态,例如设置属性或创建目录 |
generate-sources |
生成任何源代码以包含在编译中 |
process-sources |
处理源代码,例如过滤一些值 |
generate-resources |
生成包含在包中的资源 |
process-resources |
将资源复制并处理到目标目录中,准备打包 |
compile |
编译项目的源代码 |
process-classes |
对编译生成的文件进行后处理,例如对Java类进行字节码增强 |
generate-test-sources |
生成任何测试源代码以包含在编译中 |
process-test-sources |
处理测试源代码,例如过滤一些值 |
generate-test-resources |
创建用于测试的资源 |
process-test-resources |
将资源复制并处理到测试目标目录中 |
test-compile |
将测试源代码编译到测试目标目录中 |
process-test-classes |
对测试编译生成的文件进行后处理,例如对Java类进行字节码增强 |
test |
使用合适的单元测试框架测试编译的源代码,这些测试不应要求测试源代码被打包或部署 |
prepare-package |
在实际打包之前执行准备打包所需的任何操作,这通常会产生一个未打包、经过处理的包版本 |
package |
获取编译后的代码并将其打包为其可分发的格式,例如JAR |
pre-integration-test |
在执行集成测试之前执行所需的操作,这可能涉及诸如设置所需环境之类的事情 |
integration-test |
如有必要,处理包并将其部署到可以运行集成测试的环境中 |
post-integration-test |
执行集成测试后执行所需的操作,这可能包括清理环境 |
verify |
对集成测试的结果进行检查以确保满足质量标准 |
install |
将包安装到本地存储库中,以用作本地其他项目的依赖项 |
deploy |
在构建环境中完成,将最终包复制到远程存储库,以便与其他开发人员和项目共享 |
3.Site生命周期
Phase | Description |
---|---|
pre-site |
在实际项目站点生成之前执行所需的流程 |
site |
生成项目的站点文档 |
post-site |
执行完成站点生成后并准备站点部署所需的流程 |
site-deploy |
将生成的站点文档部署到指定的Web服务器 |
八、Maven的生命周期绑定关系
默认情况下,某些阶段有与其绑定的目标。对于默认生命周期,这些绑定取决于packaging值。以下是一些目标到构建阶段的绑定。
1.Clean生命周期的绑定关系
Phase | plugin:goal |
---|---|
clean |
clean:clean |
2.Default生命周期的绑定关系 - Packaging ejb/ejb3/jar/par/rar/war
Phase | plugin:goal |
---|---|
process-resources |
resources:resources |
compile |
compiler:compile |
process-test-resources |
resources:testResources |
test-compile |
compiler:testCompile |
test |
surefire:test |
package |
ejb:ejb or ejb3:ejb3 or jar:jar or par:par or rar:rar or war:war |
install |
install:install |
deploy |
deploy:deploy |
3.Site生命周期的绑定关系
Phase | plugin:goal |
---|---|
site |
site:site |
site-deploy |
site:deploy |
九、Maven的构建实例
参考clean
与package
阶段执行顺序,依次执行了pre-clean
、clean
、validate
、initialize
、generate-sources
、process-sources
、generate-resources
、process-resources
、compile
、process-classes
、generate-test-sources
、process-test-sources
、generate-test-resources
、process-test-resources
、test-compile
、process-test-classes
、test-compile
、test
、prepare-package
、package
等阶段。
值得注意的是,Default
生命周期中test
阶段绑定了surefire:test
插件目标,因此额外生成了测试报告,如上图中surefire-reports
目录下的org.learn.DemoTest.txt
、TEST-org.learn.DemoTest.xml
等各种格式的测试报告。
十、Maven的自定义插件
我们在前面介绍了Maven的lifecycle,phase和goal:使用Maven构建项目就是执行lifecycle,执行到指定的phase为止。每个phase会执行自己默认的一个或多个goal。goal是最小任务单元。我们以compile
这个phase为例,如果执行:
1 | mvn compile |
Maven将执行compile
这个phase,这个phase会调用compiler
插件执行关联的compiler:compile
这个goal。
实际上,执行每个phase,都是通过某个插件(plugin)来执行的,Maven本身其实并不知道如何执行compile
,它只是负责找到对应的compiler
插件,然后执行默认的compiler:compile
这个goal来完成编译。所以,使用Maven,实际上就是配置好需要使用的插件,然后通过phase调用它们。
Maven已经内置了一些常用的标准插件:
插件名称 | 对应执行的phase |
---|---|
clean | clean |
compiler | compile |
surefire | test |
jar | package |
如果标准插件无法满足需求,我们还可以使用自定义插件。使用自定义插件的时候,需要声明。例如,使用maven-shade-plugin
可以创建一个可执行的jar,要使用这个插件,需要在pom.xml
中声明它:
1 | <project> |
自定义插件往往需要一些配置,例如,maven-shade-plugin
需要指定Java程序的入口,它的配置是:
1 | <configuration> |
注意,Maven自带的标准插件例如compiler
是无需声明的,只有引入其它的插件才需要声明。
下面列举了一些常用的插件:
- maven-shade-plugin:打包所有依赖包并生成可执行jar;
- cobertura-maven-plugin:生成单元测试覆盖率报告;
- findbugs-maven-plugin:对Java源码进行静态分析以找出潜在问题。
当自定义maven-shade-plugin
插件并绑定package
阶段与shade
目标后,使用mvn clean package
观察打包结果,发现新生成的maven-test-1.0-SNAPSHOT.jar
包将近13MB,包含了所有依赖包。
十一、Maven的模块管理
在软件开发中,把一个大项目分拆为多个模块是降低软件复杂度的有效方法:
1 | ┌ ─ ─ ─ ─ ─ ─ ┐ |
对于Maven工程来说,原来是一个大项目:
1 | single-project |
现在可以分拆成3个模块:
1 | mutiple-project |
Maven可以有效地管理多个模块,我们只需要把每个模块当作一个独立的Maven项目,它们有各自独立的pom.xml
。例如,模块A的pom.xml
:
1 | <project xmlns="http://maven.apache.org/POM/4.0.0" |
模块B的pom.xml
:
1 | <project xmlns="http://maven.apache.org/POM/4.0.0" |
可以看出来,模块A和模块B的pom.xml
高度相似,因此,我们可以提取出共同部分作为parent
:
1 | <project xmlns="http://maven.apache.org/POM/4.0.0" |
注意到parent的<packaging>
是pom
而不是jar
,因为parent
本身不含任何Java代码。编写parent
的pom.xml
只是为了在各个模块中减少重复的配置。现在我们的整个工程结构如下:
1 | multiple-project |
这样模块A就可以简化为:
1 | <project xmlns="http://maven.apache.org/POM/4.0.0" |
模块B、模块C都可以直接从parent
继承,大幅简化了pom.xml
的编写。
如果模块A依赖模块B,则模块A需要模块B的jar包才能正常编译,我们需要在模块A中引入模块B:
1 | ... |
最后,在编译的时候,需要在根目录创建一个pom.xml
统一编译:
1 | <project xmlns="http://maven.apache.org/POM/4.0.0" |
这样,在根目录执行mvn clean package
时,Maven根据根目录的pom.xml
找到包括parent
在内的共4个<module>
,一次性全部编译。
下面我们实际看一下多模块项目的结构,其中各个pom.xml
的内容之前已经说过:
当我们调用mvn clean package
命令后,发现如下的情况,其中绿色的文字如clean:3.2.0:clean
、surefire:3.0.0:test
、jar:3.3.0:jar
等就是阶段绑定的插件目标,注意:parent
模块没有任何源代码,只执行了clean生命周期的相应阶段,而module-a/b/c
三个模块执行了clean和default生命周期的相应阶段。
参考文档:[Maven官网](Maven – Introduction to the Build Lifecycle (apache.org))、[廖雪峰的Maven基础](Maven基础 - 廖雪峰的官方网站 (liaoxuefeng.com))