Maven学习笔记
- Maven的简要介绍
- Maven的安装和配置
- Maven的安装
- Maven安装的常用配置
- Maven的使用入门
- 编写pom
- 编写主代码
- 编写测试代码
- 打包和运行
- 使用Archetype生成项目骨架
- Maven核心概念的阐述
- 坐标
- 案例分析
- 依赖
- 依赖的范围
- 传递性依赖
- 依赖范围
- 依赖调节
- 可选依赖
- Maven依赖常用的技巧
- 仓库
- 概念
- 布局
- 分类
- 远程仓库的配置
- 快照版本
- 镜像
- 生命周期与插件
- 生命周期详解
- 插件目标
- 插件绑定
- 插件配置
- 聚合与继承
- 聚合
- 继承
- 聚合和继承的关系
- 反应堆
- 使用Nexus建立私服
- Nexus的仓库与仓库组
- 配置Maven从Nexus下载构件
- 版本管理
- 何为版本管理
- Maven的版本号定义约定
- 主干、标签与分支
- Maven属性
Maven的简要介绍
Maven主要服务于基于Java平台的项目构建、依赖管理和项目信息管理。
构建是什么?
编译、运行单元测试、生成文档、打包和部署等。
Maven的用途及优点
- Maven是服务于构建,能够帮我们自动化构建过程,从清洗、编译、测试到生成报告,再到打包和部署。
- Maven是一个依赖管理工具和项目信息管理工具,提供了中央仓库,帮助我们自动下载构件。
- Maven是跨平台的,对外提供了一致的操作接口。
- Maven最大化的消除了构建的重复,抽象了构建生命周期。
- Maven可以标准化构建过程。
- Maven通过一个坐标系统准确地定位每一个构件(artifact),也就是通过一组坐标Maven能够找到任何一个Java类库,使得我们可以借助它有序的管理依赖。
- Maven还能帮助我们管理原本分散在项目中各个角落的项目信息,包括项目描述、开发者列表、版本控制系统地址、许可证、缺陷管理系统地址等。
Maven的安装和配置
Maven的安装
Step1:在安装Maven之前,首先要确认是否已经正确安装了JDK。可使用以下命令来检查Java的安装情况:
echo %JAVA_HOME% # 检查环境变量是否正确指向了JDK目录
java -version # 运行java命令,检测Windows是否可以正常运行Java命令
Step2:访问Maven的下载页面,根据自己的系统选择相应的maven下载文件apache-maven-3.9.5-bin.zip
即可。
Step3:在Windows本地进行安装,将前面下载好的压缩包进行解压到指定目录,如果电脑中有多个盘,尽量不要安装到C盘。
Step4:配置环境变量,将Maven安装配置到操作系统环境中。
Step5:此电脑(鼠标右键点击)–> 属性 –> 高级系统设置 –> 环境变量 –> 新建系统变量:MAVEN_HOME。
Step6:编辑path变量–>双击Path –> 新建变量值:%MAVEN_HOME%\bin。
【注】值得注意的是Path环境变量。当我们在cmd中输入命令时,Windows首先会在当前目录中寻找可执行文件或脚本,如果没有找到,Windows会接着遍历环境变量Path中定义的路径。由于将%MAVEN_HOME%\bin添加到了Path中,而这里%MAVEN_HOME%实际上是引用了前面定义的另一个变量,其值是Maven的安装目录。因此,Windows会在执行命令时搜索目录D:Maven\apache-maven-3.8.6\bin,而mvn执行脚本的位置就是这里。
Step7:打开一个新的cmd窗口(这里强调新的窗口是因为新的环境变量配置需要新的cmd窗口才能生效),可运行以下命令检查Maven的安装情况:
echo "%MAVEN_HOME%" # 检查环境变量是否正确指向了Maven目录
mvn -v # 运行mvn命令,检测Windows是否可以正常运行mvn执行脚本
Maven安装的常用配置
- 设置MAVEN_OPTS环境变量
- 通常需要设置MAVEN_OPTS的值为-Xms128m-Xmx512m,因为Java默认的最大可用内存往往不能够满足Maven运行的需要,比如在项目较大时,使用Maven生成项目站点需要占用大量的内存,如果没有该配置,则很容易得到
java.lang.OutOfMemeoryError
。
- 通常需要设置MAVEN_OPTS的值为-Xms128m-Xmx512m,因为Java默认的最大可用内存往往不能够满足Maven运行的需要,比如在项目较大时,使用Maven生成项目站点需要占用大量的内存,如果没有该配置,则很容易得到
Maven的使用入门
编写pom
Maven项目的核心就是pom.xml。POM(Project Object Model,项目对象模型)定义了项目的基本信息,用于描述项目如何构建,声明项目依赖等。
做一个案例:为Hello World项目编写一个简单的pom.xml。
首先,新建一个hello-world的文件夹,打开该文件夹,新建一个pom.xml的文件,输入以下内容:
<?xml version="1.0"encoding="UTF-8"?>
<project xmlns="https://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/maven-v4_0_0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.kang</groupId><artifactId>hello-world</artifactId><version>1.0-SHAPSHOT</version><name>Maven Hello World Project</name>
</project>
- 代码的第一行是XML头,指定了该XML文档的版本和编码方式。
- 紧接着是project元素,project是所有pom.xml的根元素,它还声明了一些POM相关的命名空间及xsd元素,虽然这些元素不是必须的,但使用这些属性能够让第三方工具帮助我们快速编辑POM。
- 根元素下第一个元素是modelVersion指定了当前POM模型的版本,对于Maven2和Maven3来说,只能是4.0.0。
- 这段代码中最重要的是包含groupId、artifactId和version的三行。这三个元素定义了一个项目基本的坐标,在Maven中,任何的jar、pom和war都是基于这些基本的坐标进行区分的。
- groupId定义了项目属于哪个组,这个组往往和项目所在的组织或者公司存在关联。例如:你的公司是mycom,有一个项目为myapp,那么groupId就应该是com.mycom.myapp。
- artifactId定义了当前Maven项目在组中唯一的ID,我们为这个Hello World项目定义artifactId为hello-world。在前面的com.mycom.myapp的例子中,可能会根据实际情况为不同的子项目(模块)分配artifactId,如myapp-util、myapp-domain、myapp-web等。
- version指定了Hello World项目当前的版本 —— 1.0-SNAPSHOT。SNAPSHOT意为快照,说明该项目还处于开发中,是不稳定的版本。随着项目的发展,version会不断的更新,如升级为1.0、1.1-SNAPSHOT、1.1、2.0等。
- 最后一个name元素声明了一个对于用户更为友好的项目名称,虽然这不是必须的,但还推荐为每个POM声明name。
没有任何实际的Java代码,我们就能够定义出来一个Maven项目的POM,这正是Maven的一大优点。它能够让项目对象模型最大程度地与实际代码相独立,我们可以称之为解耦或者正交性。在很大程度上避免了Java代码和POM代码的相互影响。比如项目需要升级版本,只需要修改POM,而不需要更改Java代码。
编写主代码
项目主代码和测试代码不同,主代码会被打包到最终的构建中(如:jar),而测试代码只在运行测试时用到,不会被打包。
默认情况下,Maven项目的主代码位于src/mian/java目录,遵循Maven的约定,创建该目录,然后在该目录下创建文件com/kang/study/maven/helloworld/HelloWorld.java。具体代码内容如下:
package com.kang.study.maven.helloworld;public class HelloWorld{public String sayHello(){return "Hello Maven";}public static void main(String[] args){System.out.print(new HelloWorld().sayHello());}
}
这是一个简单的Java类,它有一个sayHello()方法,并将结果返回到控制台上。
【注意】1、绝大多数情况下,应该把项目的主代码放到src/main/java目录下,而无需其他的配置,Maven会自动搜寻该目录找到项目的主代码。2、该Java类的包名com.kang.study.maven.helloworld,这与之前的groupId和artifactId相呼应。一般情况下来说,项目的Java类的包都应该基于项目的groupId和artifactId。
然后使用Maven进行编译,在项目的根目录下运行mvn clean compile
,会得到以下输出
C:\CodeRoom\hello-world>mvn clean compile
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------< com.kang.study.maven:hello-world >------------------
[INFO] Building Maven Hello World Project 1.0-SHAPSHOT
[INFO] from pom.xml
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- clean:3.2.0:clean (default-clean) @ hello-world ---
[INFO] Deleting C:\CodeRoom\hello-world\target
[INFO]
[INFO] --- resources:3.3.1:resources (default-resources) @ hello-world ---
[WARNING] Using platform encoding (GBK actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] skip non existing resourceDirectory C:\CodeRoom\hello-world\src\main\resources
[INFO]
[INFO] --- compiler:3.11.0:compile (default-compile) @ hello-world ---
[INFO] Changes detected - recompiling the module! :source
[WARNING] File encoding has not been set, using platform encoding GBK, i.e. build is platform dependent!
[INFO] Compiling 1 source file with javac [debug target 1.8] to target\classes
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 0.700 s
[INFO] Finished at: 2023-11-28T18:22:55+08:00
[INFO] ------------------------------------------------------------------------
- clean告诉Maven清理输出目录target/,compile告诉Maven编译项目主代码。
- 从输出中看到Maven首先执行了clean:clean任务,删除target/目录。默认情况下,Maven构建的所有输出都在target/目录中。
- 接着执行resources:resources任务(未定义项目资源,暂且略过)。
- 最后执行compiler:compiler任务,将项目主代码编译至target/classes目录(编译好的类为com/kang/study/maven/helloworld/HelloWorld.class)。
以上就是Maven在没有额外的配置的情况下,就执行了项目的清理和编译任务。
编写测试代码
为了使项目结构保持清晰,主代码和测试代码应该分别位于独立的目录中。
Maven项目中默认的主代码目录是src/main/java,对应的Maven项目中默认的测试代码目录是src/test/java。因此需要创建此目录,再进行测试代码的编写。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="https://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/maven-v4_0_0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.kang.study.maven</groupId><artifactId>hello-world</artifactId><version>1.0-SHAPSHOT</version><name>Maven Hello World Project</name><dependencies><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.12</version><scope>test</scope></dependency></dependencies>
</project>
- 代码中添加了dependencies元素,该元素下可以包含多个dependency元素以声明项目的依赖。这里面添加了一个依赖 —— groupId是junit,artifactId是junit,version是4.12。有了这段声明,Maven就能够自动下载junit-4.12.jar。
- 上述代码中还有一个值为test的元素scope,scope为依赖范围,若依赖范围为test则表示该依赖只对测试有效,也就是说,在测试代码中import JUnit的代码是可以的,但是如果在主代码中用import JUnit代码,就会编译报错。如果不声明依赖范围,那么默认值就是compile,表示该依赖对主代码和测试代码都有效。
配置完依赖之后,接着就可以写测试类了。在src/test/java目录下创建文件,代码如下:
package com.kang.study.maven.helloworld;import static org.junit.Assert.assertEquals;import org.junit.Test;public class HelloWorldTest{@Testpublic void testSayHello(){HelloWorld helloWorld = new HelloWorld();String result = helloWorld.sayHello();assertEquals("Hello Maven",result);}
}
一个典型的单元测试包含三个步骤:
①准备测试类和数据
②执行要测试的行为
③检查结果
- 上述示例中首先初始化了一个要测试的HelloWorld的实例;
- 接着执行sayHello()方法并保存结果到result变量中;
- 最后使用JUnit框架的Assert类检查结果是否为我们期望的预期“Hello Maven”。
- 在JUnit中,约定所有测试的方法都以应该以
@Test
进行标注。
测试用例编写完毕后,就可以调用Maven执行测试了,运行命令mvn clean test
。
打包和运行
将项目编译、测试之后,下一个步骤就是打包(package)了,在没有指定打包类型的情况下,使用默认的打包类型是jar,运行命令mvn clean package
。
我们的HelloWorld项目得到了输出,可以复制这个jar文件到其他项目的ClassPath中从而使用HelloWorld类。
如果需要其他的Maven项目直接引用这个jar,需要运行命令mvn clean install
。
在打包后,执行了安装任务install:install。从控制台的输出可以看出,该命令将项目输出的jar安装到了Maven本地仓库中,只有将Hello World的构建安装到本地仓库之后,其他Maven项目才能使用它。
默认打包生成的jar是不能够直接运行的,因为带有main方法的类信息不会添加到mainifest中,此信息可以在jar中的META-INF/MANIFEST.MF
文件中可以看到,没有Main-Class一行。为了可以生成可执行的jar文件,需要借助maven-shade-plugin,配置该插件如下:
<build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-shade-plugin</artifactId><version>2.4.3</version><executions><execution><phase>package</phase><goals><goal>shade</goal></goals><configuration><transformers><transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"><mainClass>com.kang.study.maven.helloworld.HelloWorld</mainClass></transformer></transformers></configuration></execution></executions></plugin></plugins>
</build>
plugin元素在POM中的相对位置应该在<project><build><plugins>
下面。配置了mainClass为com.kang.study.maven.helloworld.HelloWorld,项目在打包时会将该信息放到MANIFEST中。重新执行命令mvn clean install,完成之后打开target/目录,可以看到两个jar文件hello-world-1.0-SHAPSHOT.jar和original-hello-world-1.0-SHAPSHOT.jar,第一个是带有Main-Class信息的可运行的jar,第二个是原始的jar。接着打开MANIFEST.MF,就可以发现比之前多列一行内容Main-Class: com.kang.study.maven.helloworld.HelloWorld
,现在就可以在项目的根目录下运行该jar文件了:
C:\CodeRoom\hello-world>java -jar target/hello-world-1.0-SHAPSHOT.jar
Hello Maven
至此获得了我们期望的结果Hello Maven
。
使用Archetype生成项目骨架
Hello World项目中有一些Maven的约定:在项目的根目录中放置pom.xml
,在src/main/java
目录中放置项目的主代码,在src/test/java
中放置项目的测试代码。
我们称这些基本的目录结构和pom.xml文件内容称为项目的骨架。如果一直重复这些步骤自己创建一个Maven项目会使得工作效率降低。
为此Maven提供了Archetype以帮助我们快速勾勒出项目骨架。还是以Hello World为例,我们使用maven archetype来创建该项目的骨架,离开当前的Maven项目目录,运行命令mvn archetype:generate
。
我们会看到一段很长的输出,有很多可用的Archetype供选择。每一个Archetype都会有一个编号,命令行会给出一个默认的编号,对应的是maven-archetype-quickstart
,回车后按要求输入要创建项目的groupId、artifactId、version以及包名package。然后Archetype会根据我们提供的信息创建项目骨架。
......
Define value for property 'groupId': com.kang.study.maven
Define value for property 'artifactId': hello-world-auto-generate
Define value for property 'version' 1.0-SNAPSHOT: :
Define value for property 'package' com.kang.study.maven: : com.kang.study.maven.helloworldauto
Confirm properties configuration:
groupId: com.kang.study.maven
artifactId: hello-world-auto-generate
version: 1.0-SNAPSHOT
package: com.kang.study.maven.helloworldautoY: : Y
[INFO] ----------------------------------------------------------------------------
[INFO] Using following parameters for creating project from Archetype: maven-archetype-quickstart:1.4
[INFO] ----------------------------------------------------------------------------
[INFO] Parameter: groupId, Value: com.kang.study.maven
[INFO] Parameter: artifactId, Value: hello-world-auto-generate
[INFO] Parameter: version, Value: 1.0-SNAPSHOT
[INFO] Parameter: package, Value: com.kang.study.maven.helloworldauto
[INFO] Parameter: packageInPathFormat, Value: com/kang/study/maven/helloworldauto
[INFO] Parameter: package, Value: com.kang.study.maven.helloworldauto
[INFO] Parameter: version, Value: 1.0-SNAPSHOT
[INFO] Parameter: groupId, Value: com.kang.study.maven
[INFO] Parameter: artifactId, Value: hello-world-auto-generate
[INFO] Project created from Archetype in dir: C:\CodeRoom\hello-world-auto-generate
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 49.432 s
[INFO] Finished at: 2023-11-29T13:05:37+08:00
[INFO] ------------------------------------------------------------------------
在当前目录下,Archetype插件会创建一个名为hello-world-auto-generate
(我们定义的artifactId)的子目录,从中可以看到项目的基本结构:
- 基本的
pom.xml
已经被创建,里面包含了必要的信息以及一个junit依赖 - 主代码目录
src/main/java
已经被创建,在该目录下还有一个Java类com.kang.study,maven.helloworldauto.App
,注意这里使用到了刚才定义的包名,而这个类也仅仅只有一个简单的输出Hello World!的main方法; - 测试代码目录
src/test/java
也被创建好了,并且包含了一个测试用例com.kang.study.maven.helloworldauto.AppTest
。
Archetype可以帮助我们迅速地构建起项目的骨架,在前面的例子中,我们完全可以在Archetype生成的骨架的基础上开发Hello World项目以节省大量时间。
Maven核心概念的阐述
坐标
Maven坐标为各种构件引入了秩序,任何一个构件都必须明确定义自己的坐标,而一组Maven坐标是通过一些元素来定义的,有:groupId、artifactId、version、packaging、classifier。下面详细介绍一下各个坐标元素:
<groupId>org.sonatype.nexus</groupId>
<artifactId>nexus-indexer</artifactId>
<version>2.0.0</version>
<packaging>jar</packaging>
- groupId:定义当前项目隶属的实际项目
- 首先,Maven项目和实际项目不一定是一对一的关系。这是由于Maven中模块的概念,因此,一个实际项目往往会被划分成很多模块;
- 其次,groupId不应该对应项目隶属的组织或公司。因为,一个组织下会有很多实际项目,如果groupId只定义到了组织级别的话,artifactId只能定义Maven项目(模块),那么实际项目这一层将很难被定义;
- groupId的表示方式与Java包名的方式类似,通常将域名反向一一对应。例如:groupId为
org.sonatype.nexus
,org.sonatype
表示Sonatype公司建立的一个非盈利性组织,nexus表示Nexus这一实际项目,该groupId与域名nexus.sonatype.org
对应。
- artifactId:定义实际项目中的一个Maven项目(模块)
- 推荐的做法是使用实际项目名称作为artifactId的前缀。比如:上面例子中的artifactId是
nexus-indexer
,使用了实际项目名nexus作为前缀,这样方便寻找实际构件; - 默认情况下,Maven生成的构件,其文件名会以artifactId作为开头,如nexus-indexer-2.0.0.jar,使用实际项目名称作为前缀之后,就能方便从一个lib文件夹中找到某个项目的一组构件。
- 推荐的做法是使用实际项目名称作为artifactId的前缀。比如:上面例子中的artifactId是
- version:定义Maven项目当前所处的版本
- packaging:定义Maven项目的打包方式
- 首先,打包方式通常与所生成构件的文件扩展名对应,如packaging为jar,最终的文件名为nexus-indexer-2.0.0.jar,如果使用war打包方式的话,最终的文件为以.war为后缀的文件名;
- 其次,打包方式会影响构建的生命周期,比如jar打包和war打包会使用不同的命令。当没有定义packaging时,Maven会使用默认值jar。
- classifier:定义构建输出的一些附属构件
- 附属构件与主构件对应,如上例中主构件为nexus-indexer-2.0.0.jar,该项目可能还会使用一些插件来生成
nexus-indexer-2.0.0-javadoc.jar
、nexus-indexer-2.0.0-sources.jar
这样一些附属构件,其包含的了Java源代码和文档。 - 不能直接定义项目的classifier,因为附属构件不是项目直接默认生成的,而是由附加的插件帮助生成的。
- 附属构件与主构件对应,如上例中主构件为nexus-indexer-2.0.0.jar,该项目可能还会使用一些插件来生成
以上的5个元素中,groupId、artifactId、version是必须声明的,packaging是可选的(默认为jar),classifier是不能直接定义的。
项目构件的文件名与坐标是相对应的,一般规则为artifactId-version[-classfier].packaging,[-classfier]是可选的。
案例分析
我们用一个实际的案例来巩固我们前面学到的一些内容:我们现在需要写整个案例中的其中一个模块email负责发送账户激活的电子邮件。
首先先构建这个模块(account-email)的POM
<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.kang.study.account</groupId><artifactId>account-email</artifactId><version>1.0.0-SNAPSHOT</version><packaging>jar</packaging><name>account-email</name><url>http://maven.apache.org</url><properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><dependencies><dependency><groupId>org.springframework</groupId><artifactId>spring-core</artifactId><version>2.5.6</version></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-beans</artifactId><version>2.5.6</version></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>2.5.6</version></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-context-support</artifactId><version>2.5.6</version></dependency><dependency><groupId>javax.mail</groupId><artifactId>mail</artifactId><version>1.4.1</version></dependency><dependency><groupId>com.icegreen</groupId><artifactId>greenmail</artifactId><version>1.3.1b</version><scope>test</scope></dependency><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>3.8.1</version><scope>test</scope></dependency></dependencies><build><pluginManagement><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><configuration><source>1.8</source><target>1.8</target></configuration></plugin></plugins></pluginManagement></build>
</project>
由分析可知:
- groupId:com.kang.study.account,由于该模块属于账户注册服务项目的一部分,因此,其groupId对应了account项目;
- artifactId:account-email,artifactId以account作为前缀,以方便区别其他的模块的构建;
- version:1.0.0-SNAPSHOT,1.0.0-SNAPSHOT表示该版本还处于开发中,还不稳定;
- dependencies元素,其包含了多个dependency子元素,这是POM定义项目依赖的位置,其中有三个元素groupId、artifactId、version这便是依赖的坐标,任何一个Maven项目都需要定义自己的坐标;
- 最后,POM中有一段关于maven-compiler-plugin的配置,其目的是开启Java8的支持。
依赖
一个依赖声明可以包含如下的一些元素:
<project>......<dependencies><dependency><groupId>...</groupId><artifactId>...</artifactId><version>...</version><type>...</type><scope>...</scope><optional>...</optional><exclusions><exclusion>......</exclusion></exclusions></dependency></dependencies>
</project>
根元素project下的dependencies可以包含一个或者多个dependency元素,以声明一个或多个项目依赖。每个依赖可以包含的元素有:
- groupId、artifactId、version:依赖的基本坐标,对于任何一个依赖来说,基本坐标是最重要的,Maven根据坐标才能找到需要的依赖;
- type:依赖的类型,对应于项目坐标定义的packaging。大部分情况下,该元素不予声明,其默认值为jar;
- scope:依赖的范围;
- optional:标记依赖是否可选;
- exclusions:用来排除传递性依赖。
依赖的范围
依赖范围就是用来控制依赖与这三种classpath(编译classpath、测试classpath、运行classpath)的关系。
- compile:编译依赖范围
- 如若没有指定,默认使用该依赖范围。使用此依赖范围的Maven依赖,对于编译、测试、运行这三种classpath都有效。
- test:测试依赖范围
- 使用此依赖范围的Maven依赖,只对测试classpath有效,在编译主代码或运行项目的使用时无法使用此类依赖。
- provided:已提供依赖范围
- 使用此依赖范围的依赖,对于编译和测试classpath都有效,但运行时无效。
- runtime:运行时依赖范围
- 使用此依赖范围的依赖,对于测试和运行classpath都有效,但在编译主代码时无效。
- system:系统依赖范围
- 使用此依赖范围的依赖,对于编译和测试classpath都有效,但运行时无效。使用system范围的依赖时必须通过systemPath元素显示地指定依赖文件的路径。由于此类依赖设备不是通过Maven仓库解析,而是往往与本机系统绑定,可能造成构建的不可移植,因此应该谨慎使用。
- import:导入依赖范围。该依赖范围不会对三种classpath产生实际的影响。
传递性依赖
在Maven中,传递性依赖是指当一个项目依赖于其他项目时,它可以自动获取到被依赖项目所需要的依赖项。换句话说,当一个项目依赖于另一个项目,那么这个项目所依赖的库或者jar文件也会被自动加入到该项目的依赖项中。
举个例子,假设项目A依赖于项目B,而项目B依赖于项目C。在项目A的pom.xml中,只需要声明对项目B的依赖,并没有显式声明对项目C的依赖。当使用Maven构建项目A时,Maven会自动检测到项目B的依赖关系,并将项目C的依赖项添加到项目A的构建路径中。
依赖范围
依赖范围不仅可以控制依赖与三种classpath的关系,还对传递性依赖产生影响。假设A依赖于B、B依赖于C,A是B的第一直接依赖,B是C的第二直接依赖,C是A的传递性依赖。
第一直接依赖的范围和第二直接依赖的范围决定了传递性依赖的范围:最左边一列表示第一直接依赖范围,最上面一行表示第二直接依赖的范围,中间的单元格表示传递性依赖范围。
暂时无法在飞书文档外展示此内容
通过上表,我们可以看出这样的规律
- 当第二直接依赖的范围是compile的时候,传递性依赖的范围与第一直接依赖的范围一致;
- 当第二直接依赖的范围是test的时候,依赖不会得以传递;
- 当第二直接依赖的范围是provided的时候,只传递第一直接依赖范围也为provided的依赖,且传递性依赖的范围同样为provided;
- 当第二直接依赖的范围是runtime的时候,传递性依赖的范围与第一直接依赖的范围一致,但compile例外,此时传递性依赖的范围为runtime。
依赖调节
依赖调节是指在解决依赖冲突时,选择最合适的依赖项的过程。当一个项目具有多个依赖项,有可能会出现同一依赖项的不同版本,或者不同依赖项之间的冲突,这就需要通过依赖调解来解决这个问题。
依赖调节原则:
- 第一原则:路径最近者优先。
- 第二原则:第一声明者优先。在路径长度相同的情况下,声明依赖最靠前的依赖被解析使用
可选依赖
可选依赖是在 Maven 中用于标记某个依赖项为可选的一种机制。可选依赖项通常是指在特定的环境下或者满足特定条件时才需要引入的依赖。
当一个依赖项被标记为可选时,Maven在构建项目时不会强制要求引入该依赖项,即使该依赖项在项目中没有直接引用也不会报错。但是,如果该依赖项在项目中被明确引用了,Maven会尝试解析和加载该可选依赖项。
通过在pom.xml中的元素中添加true标记,可以将某个依赖项标记为可选依赖。示例代码如下:
<dependency><groupId>com.example</groupId><artifactId>example-library</artifactId><version>1.0.0</version><optional>true</optional>
</dependency>
【注意】在理想情况下,是不应该使用可选依赖的;使用可选的原因可能是某个项目实现了多个特性,在面向对象设计中,有个单一职责性原则,意指一个类应该只有一项职责,而不是糅合太多的功能。
Maven依赖常用的技巧
-
排除依赖
-
在 Maven 中,有时候我们希望排除某些传递性依赖项,即不将它们包含在项目构建中。这时可以使用 Maven 的排除依赖机制。
-
排除依赖的作用是在 pom.xml 文件中的相关依赖项中指定不需要的传递性依赖,使其不会被引入到项目中。通常,在某个依赖项的
<dependency>
元素中使用<exclusions>
子元素指定要排除的依赖项。 -
<dependency><groupId>com.example</groupId><artifactId>example-library</artifactId><version>1.0.0</version><exclusions><exclusion><groupId>org.unwanted</groupId><artifactId>unwanted-dependency</artifactId></exclusion></exclusions> </dependency>
-
上述示例中,
example-library
依赖的传递性依赖项中的unwanted-dependency
将被排除,不会被包含在项目构建中。 -
需要注意一点的是,在排除依赖项时需要明确指定其
groupId
和artifactId
,以确保准确排除目标依赖项。排除依赖也可以一次性排除多个依赖项,只需在<exclusions>
下添加多个<exclusion>
元素即可。 -
排除依赖的机制在一些情况下非常有用,比如解决依赖冲突或避免引入不需要的依赖项。不过,使用排除依赖时要注意慎重,确保准确理解依赖关系,并明确排除的依赖项对项目的影响。
-
-
归类依赖
-
在 Maven 中,可以通过使用
<dependencyManagement>
元素以及<dependency>
元素的组合来对依赖进行分类和管理。-
<dependencyManagement>
:在 Maven 项目的顶层 POM 文件中,通过<dependencyManagement>
元素可以集中管理项目中所有模块的依赖版本。在该元素内,可以列出全部依赖项,并指定它们的版本号。这样,项目中的各个模块只需要声明依赖项的groupId
和artifactId
,而不用再指定版本号,Maven 会根据<dependencyManagement>
中的配置自动管理依赖版本。 -
<dependencyManagement><dependencies><dependency><groupId>com.example</groupId><artifactId>example-library</artifactId><version>1.0.0</version></dependency><!-- 其他依赖项 --></dependencies> </dependencyManagement>
-
<dependency>
:在每个模块的 POM 文件中,通过<dependency>
元素引入实际需要的依赖项。在<dependency>
元素中指定依赖项的groupId
、artifactId
,以及任何其他必要的信息,不需要引入version
。
-
-
示例:
-
<dependencies><dependency><groupId>com.example</groupId><artifactId>example-library</artifactId></dependency><!-- 其他依赖项 --> </dependencies>
-
通过这样的方式,依赖项可以被有效地归类和管理。
<dependencyManagement>
提供了一个统一的地方定义依赖版本,避免重复代码,确保依赖版本的一致性。而每个模块的 POM 文件中只需声明实际需要的依赖项,简化了项目的配置和维护工作。 -
需要注意的是,
<dependencyManagement>
中的依赖项并不会真正引入到项目中,它们只是为了统一管理依赖版本。只有在模块的 POM 文件中通过<dependency>
元素引入实际依赖项,才会将其包含在项目构建中。 -
在Maven中,可以使用properties元素定义Maven属性,有了这个属性定义之后,Maven运行的时候会将POM中所有${example-library-version}都替换为实际值1.0.0。也就是说,使用美元符号和大括弧的方式引用Maven属性。
-
<properties><example-library-version>1.0.0</example-library-version> </properties> <dependencies><dependency><groupId>com.example</groupId><artifactId>example-library</artifactId><version>${example-library-version}</version></dependency> </dependencies>
-
仓库
概念
在Maven世界中,任何一个依赖、插件或者项目构建的输出,都可以称为构件。任何一个构件都有一组唯一的标识。得益于坐标机制,任何Maven项目使用任何一个构件的方式是完全相同的,在此基础上,Maven可以在某一个位置统一存储所有Maven项目共享的构件,这个统一的位置就是仓库。
实际的Maven项目将不再各自存储其依赖文件,它们只需要声明这些依赖的坐标,在需要的时候,Maven会自动根据坐标找到仓库中的构件,并使用它们。
布局
任何一个构件都有其唯一的坐标,根据这个坐标可以定义其在仓库中的唯一存储路径,这便是Maven的仓库布局方式。示例如下:
log4j:log4j:1.2.15
这一依赖,其对应的仓库路径为log4j/log4j/1.2.15/log4j-1.2.15.jar
,该路径与坐标的大致对应关系为groupId/artifactId/version/artifactId-version.packaging
分类
对于Maven来说,仓库只分为两种:本地仓库和远程仓库。
- 当Maven根据坐标寻找构件的时候,首先会查看本地仓库,如果本地仓库存在此构件,则直接使用;
- 如果本地仓库不存在此构件,或者需要查看是否有更新的的构件版本,Maven就会去远程仓库去查找,找到需要的构件之后,下载到本地仓库再进行使用;
- 如果本地仓库和远程仓库都没有需要的构件,Maven则会报错。
特殊的远程仓库
- 中央仓库
- 中央仓库是Maven核心自带的远程仓库,它包含了绝大部分开源的构件。在默认配置下,当本地仓库没有Maven需要的构件的时候,他就会尝试从中央仓库下载。
- 私服
- 为了节省带宽和时间,在局域网内架设一个私有的仓库服务器,用其代理所有外部的远程仓库,内部的项目还能部署到私服上供其他项目使用。
- 其他公共库
- 除了中央仓库和私服,还有很多其他公开的远程仓库。
本地仓库
本地仓库是 Maven 在本地计算机上存储构建所需依赖项的地方。当你执行 Maven 构建时,Maven 会自动下载所需的依赖项并缓存到本地仓库中。默认情况下,本地仓库位于用户的主目录下的 .m2
文件夹中。我们也可以自定义本地仓库目录地址,将Maven安装目录下的settings.xml文件进行修改localRepository元素的值为想要的仓库地址。
<localRepository>D:\java\repository\</localRepository>
远程仓库
安装好Maven后,如果不执行任何Maven命令,本地仓库目录是不存在的。当用户输入第一条Maven命令后,Maven才会创建本地仓库,然后根据配置和需要,从远程仓库下载构件到本地仓库。
Maven 支持从远程仓库下载依赖项。远程仓库是指存储 Maven 依赖项的中央服务器,例如 Maven 中央仓库。当 Maven 在本地仓库中找不到所需的依赖项时,它会自动从远程仓库下载。Maven 可以配置多个远程仓库,在 settings.xml
配置文件中指定远程仓库的 URL,以便引入第三方库和插件。
中央仓库
Maven 中央仓库是 Maven 社区提供的一个公共远程仓库,存储了大量的开源库和常见的Maven构建依赖项。中央仓库是 Maven 默认使用的远程仓库,当本地仓库没有所需的依赖项时,Maven 会自动从中央仓库进行下载。中央仓库是Maven构建中最常用的远程仓库。
私服
私服是指由组织或个人自行搭建和维护的 Maven 仓库。私服可以用于存储公司内部开发的库、插件和其他自定义构建资源。它可以作为组织内部的 “中央仓库”,使团队成员可以共享和管理内部构建依赖项。私服可以通过配置 Maven 的 settings.xml
文件来使用。
私服是一种特殊的的远程仓库,它是架设在局域网内的仓库服务,私服代理广域网上的远程仓库,供局域网内的Maven用户使用。当Maven需要下载构件的时候,它从私服请求,如果私服上不存在此构件,则从外部的远程仓库下载,缓存在私服上之后,再为Maven的下载请求提供服务。此外,一些无法从外部仓库下载到本地的构件也能从本地上传到私服上供大家使用。
建立私服的好处:
- 节省自己的外网带宽
- 大量的对于外部仓库的重复请求会消耗很大的带宽,利用私服代理外部仓库之后,对外的重复构件下载得以消除,即降低外网带宽的压力。
- 加速Maven构建
- 不断的连接请求内部仓库是十分耗时的,但是Maven的一些内部机制要求Maven在执行构件的时候不停地检查远程仓库数据。当项目配置了很多外部远程仓库的时候,构建的速度会被大大降低。使用私服可以很好地解决这一问题,当Maven只需要检查局域网内私服的数据时,构建的速度便得以很大程度的提高。
- 部署第三方构件
- 组织内部生成的私有构件肯定无法从外部仓库获得,建立私服之后,便可以将构件部署到这个内部的仓库中,供内部的Maven项目进行使用。
- 提高稳定性,增强控制
- 当Internet不稳定的时候,Maven构建也会变得不稳定,甚至无法构建。使用私服后,即使暂时没有Internet连接,由于私服中已经缓存了大量构件,Maven也能正常运行。
- 降低中央仓库的负荷
远程仓库的配置
默认的中央仓库无法满足项目的需求,可能项目需要的构件存在于另外一个远程仓库中。下面以JBoss Maven仓库为例,在POM中配置使用JBoss Maven仓库
<project>......<repositories><repository><id>jboss</id><name>JBoss Repository</name><url>http://repository.jboss.com/maven2/</url><releases><enabled>true</enabled></releases><snapshots><enabled>false</enabled></snapshots><layout>default</layout></repository></repositories>......
</project>
在repositories元素下,可以使用repository子元素声明一个或多个远程仓库。
- 该例中声明了一个id为jboss,名称为JBoss Repository的仓库。任何一个仓库声明的id必须是唯一的,Maven自带的中央仓库使用的id为central,如果其他的仓库声明也使用该id,就会覆盖中央仓库的配置。
- 该配置的url指向了仓库的地址,一般来说,该地址都基于http协议,Maven用户可以在浏览器中打开仓库地址浏览构件。
- releases用来控制Maven对于发布版构件的下载,其中enabled值为true,表示开启JBoss仓库的发布版本下载支持。
- snapshots用来控制Maven对于快照版构件的下载,其中enabled值为false,表示关闭JBoss仓库的快照版本的下载支持。
- layout元素值default表示仓库的布局是Maven2或者Maven3的默认布局,而不是Maven1的默认布局。
- releases和snapshots,除了enabled,还包含两个子元素updatePolicy和checksumPolicy
<snapshots><enabled>true</enabled><updatePolicy>daily</updatePolicy><checksumPolicy>ignore</checksumPolicy>
</snapshots>
- updatePolicy用来配置Maven从远程仓库检查更新的频率,默认的值是daily,表示Maven每天检查一次。其他取值有:never - 从不检查更新;always - 每次构建都检查更新;interval:X - 每隔X分钟检查一次更新(X为任意整数)。
- checksumPolicy用来配置Maven检查检验和文件的策略。当构件被部署到Maven中央仓库中时,会同时部署对应的校验和文件。当checksumPolicy的值为默认的warn时,Maven会在执行构建时输出警告信息,其他可用的值包括:fail - Maven遇到校验和错误就让构建失败;ignore - 使Maven完全忽略校验和错误。
远程仓库的认证
大部分远程仓库无须认证就可以访问,但有时候处于安全方面考虑,需要提供认证信息才能访问一些远程仓库。配置认证信息和配置仓库信息不同,仓库信息可以直接配置在POM文件中,但是认证信息必须配置到settings.xml文件中。
<settings>......<servers><server><id>my-proj</id><username>repo-user</username><password>repo-pwd</password></server></servers>......
</settings>
Maven使用settings.xml文件中的servers元素及其server子元素配置仓库认证信息。
- 上述案例中,该仓库的认证用户名为repo-user,认证密码为repo-pwd。
- 这里的元素id,必须于POM中需要认证的repository元素的id完全一致
部署至远程仓库
Maven除了可以对项目进行编译、测试、打包以外,还能将项目生成的构建部署到仓库中。需要编辑pom.xml文件,并且配置distributionManagement元素。
<project>......<distributionManagement><repository><id>proj-releases</id><name>Proj Release Repository</name><url>http://192.168.1.100/content/repositories/proj-releases</url></repository><snapshotRepository><id>proj-snapshots</id><name>Proj Snapshot Repository</name><url>http://192.168.1.100/content/repositories/proj-snapshots</url></snapshotRepository></distributionManagement>......
</project>
- distributionManagement包含repository和snapshotRepository子元素,前者表示发布版本构件的仓库,后者表示快照版本的仓库。这两个元素下都需要配置id、name、url,id表示仓库的唯一标识,name是为了方便人阅读,url表示该仓库的地址。
- 往远程仓库部署构件的时候,往往需要认证。配置认证信息前面的介绍中已经解释过了,简言之,就是在settings.xml中创建一个server元素,需要注意的就是其id与仓库的id要相匹配,并配置正确的认证信息。
- 配置正确后,执行mvn clean deploy,Maven就会将项目输出的构件部署到配置对应的远程仓库中。
快照版本
在 Maven 中,快照版本是指开发中的项目版本,这些版本仍处于不稳定的状态,可能会发生变化。快照版本通常用于开发过程中的测试、实验或持续集成构建。
快照版本的命名约定如下:在项目的版本号后添加 “-SNAPSHOT” 后缀。例如,一个项目的稳定版本可能是 1.0,而该项目的快照版本可能是 1.0-SNAPSHOT。
在一些情况下,当使用快照版本时,可能会发现一些依赖项无法稳定地被下载或使用,因为快照版本是不稳定的,可能会在每次构建中有所变化。快照版本只应该在组织内部的项目或模块间依赖使用,此时组织对这些快照版本的依赖具有完全的理解及控制权。项目不应该依赖于任何组织外部的快照版本依赖,由于快照版本的不稳定性,这样的依赖会造成潜在的危险。因此,在生产环境中,建议使用稳定的正式版本而不是快照版本。
镜像
如果仓库X可以提供仓库Y存储的所有内容,那么就可以认为X是Y的一个镜像。任何一个可以从仓库Y获得的构件,都能够从它的镜像中获取。示例:http://maven.net.cn/content/groups/public/
是中央仓库http://repo1.maven.org/maven2/
在中国的镜像,由于地理位子的因素,该镜像往往能够提供比中央仓库更快的服务。编辑settings.xml,来配置Maven使用镜像来替换中央仓库。
<settings>......<mirrors><mirror><id>maven.net.cn</id><name>one of the central mirrors in China</name><url>http://maven.net.cn/content/groups/public/</url><mirrorOf>central</mirrorOf></mirror></mirrors>......
</settings>
示例中mirrorOf的值为central,表示该配置为中央仓库的镜像,任何对于中央仓库的请求都会转至该镜像,其他镜像配置方式同理。
关于镜像的一个更为常见的用法是结合私服,由于私服可以代理任何外部的公共仓库,因此,对于组织内部的Maven用户来说,使用一个私服地址就等于使用了所有需要的外部仓库,这可以将配置集中到私服,从而简化Maven本身的配置。在这种情况下,任何需要的构件都可以从私服获得,私服就是所有仓库的镜像。
配置使用私服作为镜像示例:
<settings>......<mirrors><mirror><id>internal-repository</id><name>Internal Repository Manager</name><url>http://192.168.1.100/maven2/</url><mirrorOf>*</mirrorOf></mirror></mirrors>......
</settings>
- <mirrorOf>*</mirrorOf>:匹配所有远程仓库
- <mirrorOf>external:*</mirrorOf>:匹配所有远程仓库,使用localhost的除外,使用file://协议的除外。换言之,匹配所有不在本机上的远程仓库。
- <mirrorOf>repo1,repo2</mirrorOf>:匹配仓库repo1和repo2,使用逗号分隔多个远程仓库。
- <mirrorOf>*,!repo1</mirrorOf>:匹配所有远程仓库,repo1除外,使用感叹号将仓库从匹配中排除。
【注意】由于镜像仓库完全屏蔽了被镜像仓库,当镜像仓库不稳定或者停止服务的时候,Maven仍将无法访问被镜像仓库,因而将无法下载构件。
生命周期与插件
生命周期详解
Maven拥有三套相互独立的生命周期,它们分别是clean、default和site。clean生命周期的目的是清理项目、default生命周期目的是构建项目,site生命周期的目的是建立项目站点。
每个生命周期包含一些阶段,这些阶段是有顺序的,后面的阶段依赖于前面的阶段去执行。三套生命周期是相互独立的,互相之间并不受影响。
clean生命周期
此生命周期的目的是清理项目:
- pre-clean - 执行一些清理前需要完成的工作
- clean - 清理上一次构建生成的文件
- post-clean - 执行一些清理后需要完成的工作
default生命周期
此生命周期定义了真正构建时所需要执行的所有步骤,它是所有生命周期中最核心的部分,下面对各阶段进行解释:
暂时无法在飞书文档外展示此内容
site生命周期
此生命周期的目的是建立和发布项目站点,Maven能够基于POM所包含的信息,自动生成一个友好的站点,方便团队交流和发布项目信息:
- pre-site - 执行一些在生成项目站点之前需要完成的工作
- site - 生成项目站点文档
- post-site - 执行一些在生成项目站点之后需要完成的工作
- site-deploy - 将生成的项目站点发布到服务器上
命令行与生命周期
- mvn clean - 该命令调用clean生命周期中的clean阶段。实际执行的阶段为clean的生命周期的pre-clean和clean阶段
- mvn test - 该命令调用default生命周期的test阶段。实际的执行阶段为default生命周期的validate、initialize等,直到test的所有阶段。
- mvn clean install - 该命令调用clean生命周期的clean阶段和default生命周期的install阶段。实际执行的阶段为clean生命周期的pre-clean、clean阶段,以及default生命周期的从validate到install的所有阶段。
- mvn clean deploy site-deploy - 该命令调用clean生命周期的clean阶段、default生命周期的deploy阶段以及site生命周期的site-deploy阶段。实际执行的阶段为clean生命周期的pre-clean、clean阶段,default生命周期的所有阶段,以及site生命周期的所有阶段。
插件目标
在 Maven 中,Maven的核心仅仅定义了抽象的生命周期,具体的任务是交由插件完成的,插件以独立的构件形式存在。插件目标是针对插件的具体操作或任务。每个 Maven 插件可以定义不同的目标,用于完成不同的功能和任务。
插件绑定
Maven的生命周期的阶段和插件的目标是相互绑定的,用以完成某个具体的构建任务。
内置绑定
clean生命周期仅有pre-clean、clean和post-clean三个阶段,其中的clean与maven-clean-plugin:clean绑定。maven-clean-plugin仅有clean这一目标,其作用就是删除项目的输出目录。
site生命周期有pre-site、site、post-site和site-deploy四个阶段,其中site和maven-site-plugin:site相互绑定,site-deploy和maven-site-plugin:deploy相互绑定,其中site目标用来生成项目站点,deploy目标用来将项目站点部署到远程服务器上。
default生命周期的阶段与插件目标的绑定关系由项目打包类型所决定。最常见、最重要的打包类型是jar,它也是默认的打包类型。基于这种打包类型的项目,其default生命周期的内置插件绑定关系及具体任务如下所示:
暂时无法在飞书文档外展示此内容
自定义绑定
Maven 插件的自定义绑定允许将某个插件的目标绑定到生命周期的某个阶段上。
<build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-source-plugin</artifactId><version>2.1.1</version><executions><execution><id>attach-sources</id><phase>verify</phase><goals><goal>jar-no-fork</goal></goals></execution></executions></plugin></plugins>
</build>
在POM的build元素下的plugins子元素中声明插件的使用,该例中用到的是maven-source-plugin,其groupId为org.apache.maven.plugins,这是Maven官方插件的groupId,紧接着artifactId为maven-source-plugin,version为2.1.1。除了基本的插件坐标以外,还有插件执行配置,executions下每个execution子元素可以用来配置执行一个任务。该例中配置了一个id为attach-sources的任务,通过phrase配置,将其绑定到verify生命周期阶段上,再通过goals配置指定要执行的插件目标。
插件配置
命令行插件配置
在日常使用中,我们会使用命令行输入并执行Maven命令。很多插件目标的参数都支持从命令行配置,可以在Maven命令中使用-D参数,并伴随一个参数键=参数值的形式,来配置插件目标的参数。例如:
mvn install -Dmaven.test.skip=true
此条命令可以在构建 Maven 项目时跳过运行测试。
POM中插件全局配置
有的参数的值从项目创建到项目发布基本不会改变,在这种情况下,在POM文件中一次性配置就显然比重复命令行输入更方便。例如,我们通常需要配置maven-compile-plugin告诉它编译Java1.8版本的源文件,生成与JVM1.8兼容的字节码文件。
<build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>2.1</version><configuration><source>1.8</source><target>1.8</target></configuration></plugin></plugins>
</build>
POM中插件任务配置
除了可以为插件配置全局参数外,用户还可以为某个插件任务配置特定的参数。
以maven-antrun-plugin为例,它有一个目标run,可以用来在Maven中调用Ant任务。用户将maven-antrun-plugin:run绑定到多个生命周期阶段上,再加以不同的配置,就可以让Maven在不同的生命阶段执行不同的任务。
<build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-antrun-plugin</artifactId><version>1.3</version><executions><execution><id>ant-validate</id><phase>validate</phase><goals><goal>run</goal></goals><configuration><tasks><echo>I'm bound to validate phase.</echo></tasks></configuration></execution><execution><id>ant-verify</id><phase>verify</phase><goals><goal>run</goal></goals><configuration><tasks><echo>I'm bound to verify phase.</echo></tasks></configuration></execution></executions></plugin></plugins>
</build>
- 首先,maven-antrun-plugin:run与validate阶段绑定,从而构成一个id为ant-validate的任务。
- 插件全局配置中的configuration元素位于plugin元素下面,而这里的configuration元素则位于execution元素下,表示这是特定任务的配置,而非插件整体的配置。
- 这个ant-validate任务配置了一个echo Ant任务,向命令行输出一段文字,表示该任务是绑定到validate阶段的。
- 第二个任务的id为ant-verify,它绑定到了verify阶段,同样它也输出一段文字到命令行,告诉该任务绑定到了verify阶段。
聚合与继承
聚合
Maven 的聚合指的是将多个 Maven 项目合并为一个项目,以便进行一起构建、测试、部署和管理。
在 Maven 中,可以使用多个 <module>
元素来指定需要聚合的项目。父 pom.xml 中指定项目的 <module>
如下:
<modules><module>module1</module><module>module2</module><module>module3</module><!-- ... -->
</modules>
将多个项目聚合在一起后, Maven 可以在根目录下执行一系列的 Maven 命令,对所有子模块一同进行处理。例如,执行 mvn clean package
命令可以对所有的模块进行清理和打包操作。
具体举例来说:现有两个Maven模块account-email和account-persist用来实现用户注册的功能。如果我们想要一次性构建两个模块,我们就需要重新创建一个额外的名为account-aggregator的模块,通过这个模块来构建整个项目的所有模块。account-aggregator作为一个Maven项目,并且是一个聚合项目,其POM又有特殊的地方。
<?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.kang.study.account</groupId><artifactId>account-aggregator</artifactId><version>1.0.0-SNAPSHOT</version><packaging>pom</packaging><name>Account Aggregator</name><modules><module>account-email</module><module>account-persist</module></modules><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties></project>
- 上述的POM依旧使用与其他两个模块一样的groupId,artifactId为独立的account-aggregate,版本与其他两个模块也一样。
- 一个特殊的地方是,指定了packaging为pom。对于聚合模块来说,其打包方式packaging的值必须为pom,否则无法构建。
- 第二个特殊的地方是,元素modules,这是实现聚合最核心的配置。可以通过在一个打包方式为pom的Maven项目中声明任意数量的module元素来实现模块的聚合。
- 一般情况下,为了方便构建项目,通常将聚合模块放在项目目录的最顶层,其他模块作为聚合模块的子目录存在。
- 聚合模块仅仅是帮助聚合其他模块构建的工具,它本身并无实质的内容。
继承
Maven 继承指的是子模块可以继承父模块的依赖和插件配置等信息,从而减少模块之间的重复配置,提高项目的可维护性和一致性。
在 Maven 中,继承通常发生在一个父项目和若干个子项目之间。父项目中包含了一些通用的配置信息(如依赖、插件、属性等),子项目可以继承这些信息,同时自定义和重写部分配置,以满足自身的需求。在 Maven 中,会通过 <parent>
元素来声明项目之间的继承关系。
举例说明:我们在account-aggregate下创建一个名为account-parent的子目录,然后在该子目录建立一个除account-aggregate之外模块的父模块,并创建一个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.kang.study.maven</groupId><artifactId>account-parent</artifactId><version>1.0.0-SNAPSHOT</version><packaging>pom</packaging><name>Account Parent</name><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties></project>
此父模块使用了与其他模块一样的groupId、version,使用artifactId为account-parent表示这是一个父模块,其打包方式packaging仍为pom,作为父模块的POM,打包类型必须为pom。父模块是为了消除配置的重复,因此它本身不包含除POM以外的其他项目文件。
有了父模块就需要其他模块来继承它。
子项目继承父项目的配置,可以通过在 pom.xml 文件中包含 <parent>
元素来实现:
<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><parent><groupId>com.kang.study.account</groupId><artifactId>account-parent</artifactId><version>1.0.0-SNAPSHOT</version><relativePath>../account-parent/pom.xml</relativePath></parent><artifactId>account-email</artifactId><packaging>jar</packaging><name>Account Email</name><url>http://maven.apache.org</url><dependencies>......</dependencies><build><plugins>......</plugins></build>
</project>
上述的POM中使用parent元素声明父模块,parent下的子元素groupId、artifactId和version指定了父模块的坐标,这三个是必须元素。元素relativePath表示父模块POM的相对路径。
更新过的account-email中没有声明groupId和version,不过这并不代表account-email没有groupId和version。实际上,子模块隐式地从父模块继承两个元素,这也就消除了一些不必要的配置。如果遇到子模块需要使用和父模块不一样的groupId或者version的情况,子模块应该显示声明。
可继承的POM元素
暂时无法在飞书文档外展示此内容
依赖管理
父项目的配置信息也可以通过使用 <dependencyManagement>
元素来管理依赖信息。这样在子模块中使用依赖时,可以不指定版本,直接引用父项目中定义的版本:
<projectxmlns="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.0http://maven.apache.org/maven-v4_0_0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.kang.study.account</groupId><artifactId>account-parent</artifactId><version>1.0.0-SNAPSHOT</version><packaging>pom</packaging><name>Account Parent</name><properties><springframework.version>2.5.6</springframework.version><junit.version>4.7</junit.version></properties><dependencyManagement><dependencies><dependency><groupId>org.springframework</groupId><artifactId>spring-core</artifactId><version>${springframework.version}</version></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-beans</artifactId><version>${springframework.version}</version></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>${springframework.version}</version></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-context-support</artifactId><version>${springframework.version}</version></dependency><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>${junit.version}</version><scope>test</scope></dependency></dependencies></dependencyManagement>
</project>
- 将springframework和junit依赖的版本以Maven变量的形式提取出来,不仅消除了一些重复,也使得各依赖的版本处于更加明显的位置
- 这里使用dependencyManagement声明的依赖既不会给account-parent引入依赖,也不会给它的子模块引入依赖,这段配置是会被继承的。
如下是继承了dependencyManagement的account-email POM
<properties><javax.mail.version>1.4.1</javax.mail.version><greenmail.version>1.3.1b</greenmail.version>
</properties>
<dependencies><dependency><groupId>org.springframework</groupId><artifactId>spring-core</artifactId></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-beans</artifactId></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-context-support</artifactId></dependency><dependency><groupId>junit</groupId><artifactId>junit</artifactId></dependency><dependency><groupId>javax.mail</groupId><artifactId>mail</artifactId><version>${javax.mail.version}</version></dependency><dependency><groupId>com.icegreen</groupId><artifactId>greenmail</artifactId><version>${greenmail.version}</version><scope>test</scope></dependency>
</dependencies>
- 上述的POM中的依赖配置简化了些,这些信息的省略是因为account-email继承了account-parent中的dependencyManagement的配置,完整的依赖的声明已经包含在父POM中,子模块只需要配置简单的groupId和artifactId就能获得对应的依赖信息,从而引入正确的依赖。
- 如果子模块不声明依赖的使用,即使该依赖在父POM中的dependencyManagement中声明了,也不会产生任何实际的效果。
依赖范围import
import范围的依赖旨在dependencyManagement元素下才有效果,使用该范围的依赖通常指向一个POM,作用是将目标POM中的dependencyManagement配置导入并合并到当前POM的dependencyManagement元素中。
<dependencyManagement><dependencies><dependency><groupId>com.kang.study.account</groupId><artifactId>account-parent</artifactId><version>1.0.0-SNAPSHOT</version><type>pom</type><scope>import</scope></dependency></dependencies>
</dependencyManagement>
- 上述代码中依赖的type值为pom,import范围依赖由于其特殊性,一般都是指向打包类型为pom的模块。
插件管理
Maven提供了pluginManagement元素来管理插件,在该元素中配置的依赖不会造成实际的插件调用行为,当POM中配置了真正的plugin元素,并且其groupId和artifactId与pluginManagement中配置的插件匹配时,pluginManagement的配置才会影响实际的插件行为。
如下为父模块POM配置pluginManagement
<build><pluginManagement><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-source-plugin</artifactId><version>2.1.1</version><executions><execution><id>attach-sources</id><phase>verify</phase><goals><goal>jar-no-fork</goal></goals></execution></executions></plugin></plugins></pluginManagement>
</build>
如下为子模块继承pluginManagement后的插件配置
<build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-source-plugin</artifactId></plugin></plugins>
</build>
聚合和继承的关系
聚合主要是为了方便快速构建项目,继承主要是为了消除重复配置。在实际的项目中,往往会发现一个POM既是一个聚合POM又是一个父POM,所以将聚合和继承两者结合起来也没有什么问题。
例:合并聚合和继承功能后的account-parent
<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.0http://maven.apache.org/maven-v4_0_0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.kang.study.account</groupId><artifactId>account-parent</artifactId><version>1.0.0-SNAPSHOT</version><packaging>pom</packaging><name>Account Parent</name><modules><module>account-email</module><module>account-persist</module></modules><properties><springframework.version>2.5.6</springframework.version><junit.version>4.7</junit.version></properties><dependencyManagement><dependencies><dependency><groupId>org.springframework</groupId><artifactId>spring-core</artifactId><version>${springframework.version}</version></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-beans</artifactId><version>${springframework.version}</version></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>${springframework.version}</version></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-context-support</artifactId><version>${springframework.version}</version></dependency><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>${junit.version}</version><scope>test</scope></dependency></dependencies></dependencyManagement><build><pluginManagement><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><configuration><source>1.8</source><target>1.8</target></configuration></plugin><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-resources-plugin</artifactId><configuration><encoding>UTF-8</encoding></configuration></plugin></plugins></pluginManagement></build>
</project>
- 该POM打包方式为pom,它还包含了一个modules元素,表示用来聚合各个模块
- 包含了properties、dependencyManagement和pluginManagement元素供子模块继承
- 如果父模块在上级目录时不再需要relativePath,Maven会默认识别父模块的位置。
反应堆
在一个多模块的Maven项目中,反应堆是指所以模块组成的一个构建结构。对于单模块项目,反应堆就是该模块本身,但对于多模块项目来说,反应堆就包含了各模块之间继承与依赖的关系,从而能够自动计算出合理的模块构建顺序。
反应堆的构建顺序:Maven按序读取POM,如果该POM没有依赖模块,那么就构建该模块,否则就先构建其依赖模块;如果该依赖还依赖于其他依赖,则进一步先构建依赖的依赖。
模块间的依赖关系会将反应堆构成一个有向非循环图,各个模块是该图的节点,依赖关系构成了有向图,这个图不允许出现循环。
使用Nexus建立私服
Nexus的仓库与仓库组
仓库的四种类型:
- group(仓库组)
- hosted(宿主)
- proxy(代理)
- virtual(虚拟)
Maven可以直接从宿主仓库下载构件;Maven也可以从代理仓库下载构件,而代理仓库会间接地从远程仓库下载并缓存构件;Maven还可以从仓库组下载构件,而仓库组没有实际内容,它会转向其包含的宿主仓库或者代理仓库获得实际构件的内容。
重要仓库的介绍:
- Maven Central:该仓库代理Maven中央仓库,其策略为Release,因此只会下载和缓存中央仓库中的发布版本构件
- Releases:这是有个策略为Release的宿主类型仓库,用来部署组织内部的发布版本构件
- Snapshots:这是一个策略为Snapshot的宿主类型仓库,用来部署组织内部的快照版本构件
- 3rd party:这是一个策略为Release的宿主类型仓库,用来部署无法从公共仓库获得的第三方发布版本构件
- Public Repositories:该仓库组将所有策略为Release的仓库聚合并通过一致的地址提供服务。
配置Maven从Nexus下载构件
当我们需要为项目添加Nexus私服上的public仓库时,可以在POM中进行如下的配置:
<project>......<repositories><repository><id>nexus</id><name>Nexus</name><url>http://localhost:8081/nexus/content/groups/public/</url><releases><enabled>true</enabled></releases><snapshots><enabled>true</enabled></snapshots></repository></repositories><pluginRepositories><pluginRepository><id>nexus</id><name>Nexus</name><url>http://localhost:8081/nexus/content/groups/public/</url><releases><enabled>true</enabled></releases><snapshots><enabled>true</enabled></snapshots></pluginRepository></pluginRepositories>......
</project>
这样的配置有个弊端,只对当前Maven项目,然而我们在实际的应用当中,需要通过一次配置就能让本机所有的Maven项目都使用自己的Maven私服。这时我们就可以在settings.xml中进行配置:
<settings>......<profiles><profile><id>nexus</id><repositories><repository><id>nexus</id><name>Nexus</name><url>http://localhost:8081/nexus/content/groups/public/</url><releases><enabled>true</enabled></releases><snapshots><enabled>true</enabled></snapshots></repository></repositories><pluginRepositories><pluginRepository><id>nexus</id><name>Nexus</name><url>http://localhost:8081/nexus/content/groups/public/</url><releases><enabled>true</enabled></releases><snapshots><enabled>true</enabled></snapshots></pluginRepository></pluginRepositories></profile></profiles><activeProfiles><activeProfile>nexus</activeProfile></activeProfiles>......
</settings>
这个配置中使用了一个profile包含了相关的仓库配置,同时配置中又使用activeProfile元素将nexus这个profile激活,这样当执行Maven构建的时候,激活的profile会将仓库配置应用到项目中去。
这时配置能让Maven项目从Nexus私服下载构件。但是Maven还会不时地访问中央仓库central,如果希望的是所有Maven下载请求都仅仅通过Nexus,以全面发挥私服的作用。可以创建一个匹配任何仓库的镜像,镜像的地址为私服,这样,Maven对任何仓库的构件下载请求都会转到私服中。
<settings>
......<mirrors><mirror><id>nexus</id><mirrorOf>*</mirrorOf><url>http://localhost:8081/nexus/content/groups/public</url></mirror></mirrors><profiles><profile><id>nexus</id><repositories><repository><id>central</id><url>http://central</url><releases><enabled>true</enabled></releases><snapshots><enabled>true</enabled></snapshots></repository></repositories><pluginRepositories><pluginRepository><id>central</id><url>http://central</url><releases><enabled>true</enabled></releases><snapshots><enabled>true</enabled></snapshots></pluginRepository></pluginRepositories></profile></profiles><activeProfiles><activeProfile>nexus</activeProfile></activeProfiles>
......
</settings>
版本管理
何为版本管理
在Maven中,版本管理指的是管理项目中使用的依赖项和插件的版本号。版本管理是确保项目的稳定性、可持续性和可重复构建的重要方面之一。
版本管理中的一个关键问题是管理快照版本和发布版本之间的转换。发布版本是项目的重要里程碑,它们通常是确定、稳定和可靠的版本,适合于部署到生产环境中。而快照版本则是开发版本,它们包含最新的代码更改,但可能还没有经过完整的测试和验证。
在项目开发过程中,通常需要使用快照版本来获取最新的代码更改。但是,当项目达到一个阶段并准备发布时,需要将快照版本转换为发布版本,这个过程通常称为发布。
Maven为管理版本之间转换提供了一套严格的规则和流程。根据这些规则,快照版本的构建声明周期通常是不同于发行版的。例如,在Maven中,快照版本将使用-SNAPSHOT
作为版本后缀,并使用自动增加的构建号,例如1.0.0-SNAPSHOT
和1.0.0-20211207-141526-1
。发行版通常没有后缀,例如1.0.0
。
在将快照版本转换为发布版本之前,需要确保项目达到了一定的稳定性和可靠性标准,例如进行完整的测试和质量保证流程。一旦确定了要发布的版本,需要在pom.xml文件中将版本号更新为没有后缀的版本号,例如1.0.0
。然后,需要打包并发布这个版本。
在版本管理中,还需要考虑如何处理依赖项和插件的版本更新。例如,当版本转换为发布版时,可能需要重新考虑依赖项和插件的版本选择,并确保它们与发布版兼容。
通过合理地管理版本号,可以帮助确保项目的稳定性、可维护性和可重复性。它还可以帮助团队更好地跟踪和管理依赖项的更新和升级。
Maven的版本号定义约定
Maven的版本号定义约定:<主版本>.<次版本>.<增量版本>-<里程碑版本>
例:1.3.4-beta-2
解读:此版本号表示了该产品的第一个重大版本的第三个次要版本的第四次增量版本的beta-2里程碑。分开解释就是:“1”表示了该版本是第一个重大版本,“3“表示这是基于重大版本的第三个次要版本,”4“表示该次要版本的第四个增量;最后的”beta-2“表示该增量的某一个里程碑。
- 主版本:表示项目的重大架构变更。
- 次版本:表示较大范围的功能增加和变化,及bug修复。
- 增量版本:一般表示重大bug的修复。
- 里程碑版本:指某一个版本的里程碑。
【注意】不是每个版本号都必须拥有这四个部分。一般来说,主版本和次版本都会声明,但增量版本和里程碑就不一定了。
主干、标签与分支
主干:项目开发代码的主体,是从项目开始直到当前都处于活动的状态。从这里可以获得项目最新的源代码以及几乎所有的变更历史。
分支:从主干的某个点分离出来的代码拷贝,通常可以在不影响主干的前提的下在这里进行重大bug的修复,或者做一些实验性质的开发。如果分支达到了预期的目的,通常发生在这里的变更会被合并( merge)到主干中。
标签:用来标识主干或者分支的某个点的状态,以代表项目的某个稳定状态,这通常就是版本发布时的状态。
下面举个例子来介绍一下这些操作如何执行的:
- 下方最长的箭头是主干,项目最初的版本是1.0.0-SNAPSHOT,经过一段时间的开发后,1.0.0版本发布,这个时候就需要打一个标签,图中用一个长图表示。
- 然后项目进入1.1.0-SNAPSHOT状态,大量的开发工作都完成在主干中,添加了一些新特性并修复了很多bug之后,项目1.0.0发布,同样,这时候需要打另一种标签。
- 发布过后,项目进入1.2.0-SNAPSHOT,可这个时候用户报告1.1.0版本有一个重大的bug,需要尽快修复,我们不能在主干中修复bug,因为主干有大多的变化,无法在短时间内测试完毕并发布,我们也不能停止1.2.0-SNAPSHOT的开发,因此这时候可以基于1.1.0创建一个1.1.1-SNAPSHOT的分支,在这里进行bug修复,然后为用户发布一个1.1.1增量版本,同时打上标签。
- 当然,还不能忘了把bug修复涉及的变更合并到1.2.0-SNAPSHOT的主干中。主干在开发一段时间之后,发布1.2.0版本,然后进入到新版本1.3.0-SNAPSHOT的开发过程中。
Maven属性
使用Maven属性归类依赖,是最常见的使用Maven属性的方式,通过<properties>
元素用户可以自定义一个或多个Maven属性,然后在POM的其他地方使用${属性名称}的方式引用该属性,这种做法的最大意义在于消除重复。这样不仅减少了日后升级版本的工作量,也能降低错误发生的效率。
Maven属性共有6类:
-
内置属性
- 主要有两个常用的内置属性: b a s e d i r 表示项目根目录,即包含 p o m . x m l 文件的目录; {basedir}表示项目根目录,即包含pom.xml文件的目录; basedir表示项目根目录,即包含pom.xml文件的目录;{version}表示项目的版本。
-
POM属性
- 用户可以使用该类属性引用POM文件中对应元素的值。例如${project.artifactId}就对应了
<project><artifactId>
元素的值,常用的POM属性包括:- ${project.build.sourceDirectory}:项目的主源码目录,默认为src/main/java/。
- ${project.build.testSourceDirectory}:项目的测试源码目录,默认为src/test/java/。
- ${project.build.directory}:项目构建输出目录,默认为target/。
- ${project.outputDirectory}:项目主代码编译输出目录,默认为target/classes/。
- ${project.testOutputDirectory}:项目测试代码编译输出目录,默认为target/test-classes/。
- ${project.groupId}:项目的groupId。
- ${project.artifactId}:项目的artifactId。
- p r o j e c t . v e r s i o n :项目的 v e r s i o n ,与 {project.version}:项目的version,与 project.version:项目的version,与{version}等价。
- p r o j e c t . b u i l d . f i n a l N a m e :项目打包输出文件的名称,默认为 {project.build.finalName}:项目打包输出文件的名称,默认为 project.build.finalName:项目打包输出文件的名称,默认为{project.artifactId}-${project.version}。
- 用户可以使用该类属性引用POM文件中对应元素的值。例如${project.artifactId}就对应了
-
自定义属性
-
用户可以在POM的
<properties>
元素下自定义Maven属性。 -
<project>......<properties><my.prop>hello</my.prop></properties>...... </project>
-
然后在POM中其他地方使用${my.prop}的时候会被替换为hello。
-
-
Settings属性
- 与POM属性同理,用户使用以settings.开头的属性引用settings.xml文件中的XML元素的值,如常用的${settings.localRepository}指向用户本地仓库的地址。
-
Java系统属性
- 所有Java系统属性都可以使用Maven属性引用,例如${user.home}指向了用户目录。
-
环境变量属性
- 所有环境变量都可以使用env.开头的Maven属性引用。例如${env.JAVA_HOME}指代了JAVA_HOME环境变量的值。