五. 仓库
在Maven中,任何一个依赖、插件或者项目构建的输出,都可以称之为 构件。Maven在某个统一的位置存储所有项目的共享的构件,这个统一的位置,我们就称之为仓库。(仓库就是存放依赖和插件的地方)
任何的构件都有唯一的坐标,Maven根据这个坐标定义了构件在仓库中的唯一存储路径。
坐标和路径的关系:
如果有如下坐标
<dependency>
<groupId>com.meituan.service.group</groupId>
<artifactId>Test-SDK</artifactId>
<version>1.0</version>
</dependency>
那么对应依赖的jar 包的路径就是:com.meituan.service.group/Test-SDK/1.0.7/Test-SDK-1.0.7.jar
仓库分类:
仓库分为两大类:本地仓库 和 远程仓库
远程仓库又分为:中央仓库、私服 和 其他公共库
1)本地仓库
默认本地仓库的目录:.m2/repository,在安装maven后并不会立即创建,它是在第一次执行maven命令的时候才被创建。
若想改变本地仓库地址,需要在~/.m2/settings.xml,设置localRepository元素的值为想要的仓库地址,注意,此时配置的maven的本地仓库是属于用户范围的,若想更改全局的仓库地址需要在M2_HOME/conf/settings.xml中更改配置,但是当maven 升级时,conf 下的settings 会被重置,所以一般不建议修改全局的设置。
<settings>
<localRepository>new_path</localRepository>
</settings>
一个构件只有在本地仓库中之后,才能由其他Maven项目使用,那么构件如何进入到本地仓库中呢?最常见的是依赖Maven从远程仓库下载到本地仓库中。还有一种常见的情况是,将本地项目的构件安装到Maven仓库中。可以在某个项目下执行mvn clean install命令。
2)中央仓库
中央仓库是默认的远程仓库,maven在安装的时候,自带的就是中央仓库的配置,包含这段配置的文件是所有Maven项目都会继承的超级POM。
<repositories>
<repository>
<id>central</id>
<name>Central Repository</name>
<url>http://repo.maven.apache.org/maven2</url>
<layout>default</layout>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
(关于上例中各个元素的含义可以参考 pom 文件解析的 环境配置 部分)
中央仓库包含了绝大多数流行的开源Java构件,以及源码、作者信息、SCM、信息、许可证信息等。一般来说,简单的Java项目依赖的构件都可以在这里下载到。
3)私服
私服是一种特殊的远程仓库,它是架设在局域网内的仓库服务,私服代理广域网上的远程仓库,供局域网内的Maven用户使用。当Maven需要下载构件的时候,它从私服请求,如果私服不存在该构件,则从外部的远程仓库下载,缓存在私服上之后,再为Maven的下载请求提供服务,此外,一些无法从外部仓库下载的构件也能从本地上传到私服上供大家使用。
私服的特性:
节省自己的外网带宽:减少重复请求造成的外网带宽消耗
加速Maven构件:如果项目配置了很多外部远程仓库的时候,构建速度就会大大降低
部署第三方构件:有些构件无法从外部仓库获得的时候,我们可以把这些构件部署到内部仓库(私服)中,供内部maven项目使用
提高稳定性,增强控制:Internet不稳定的时候,maven构建也会变的不稳定,一些私服软件还提供了其他的功能
降低中央仓库的负荷:maven中央仓库被请求的数量是巨大的,配置私服也可以大大降低中央仓库的压力
当前主流的maven私服:Apache的Archiva、JFrog的Artifactory 以及 Sonatype的Nexus
maven 从仓库解析依赖的机制:
当本地仓库没有依赖构件的时候,Maven会自动从远程仓库下载;当依赖版本为快照版本的时候,Maven会自动找到最新的快照。这背后的依赖解析机制可以概括如下:
当依赖的范围是system的时候,Maven直接从本地文件系统解析构件
根据依赖坐标计算仓库路径后,尝试直接从本地仓库寻找构件,如果发现相应构件,则解析成功
在本地仓库不存在相应构件的情况下,如果依赖的版本是显式的发布版本构件,如1.2、2.1-beta-1等,则遍历所有的远程仓库,发现后,下载并解析使用
如果依赖的版本是RELEASE或LATEST,则基于更新策略读取所有远程仓库的元数据groupId/artifactId/maven-metadata.xml,将其与本地仓库的对应元数据合并后,得到最新快照版本的值,然后基于该值检查本地仓库,或者从远程仓库下载
如果依赖的版本是SNAPSHOT,则基于更新策略读取所有远程仓库的元数据groupId/artifactId/version/maven-metadata.xml,将其与本地仓库的对应元数据合并后,得到最新快照版本的值,然后基于该值检查本地仓库,或者从远程仓库下载
如果最后解析得到的构件版本是时间戳格式的快照,如1.4.1-20091104.121450-121,则复制其时间戳格式的文件至非时间戳格式,如SNAPSHOT,并使用该非时间戳格式的构件
远程仓库的认证:
大部分公共的远程仓库无须认证就可以直接访问,但我们在平时的开发中往往会架设自己的Maven远程仓库,出于安全方面的考虑,我们需要提供认证信息才能访问这样的远程仓库。配置认证信息和配置远程仓库不同,远程仓库可以直接在pom.xml中配置,但是认证信息必须配置在settings.xml文件中。这是因为pom往往是被提交到代码仓库中供所有成员访问的,而settings.xml一般只存在于本机。因此,在settings.xml中配置认证信息更为安全。
<settings>
<!--配置远程仓库认证信息-->
<servers>
<server>
<id>releases</id>
<username>admin</username>
<password>123456</password>
</server>
</servers>
</settings>
这里的关键是id元素,settings.xml中server元素的id必须与pom.xml中需要认证的repository元素的id完全一致。正是这个id将认证信息与仓库配置联系在了一起。
部署构件到远程仓库:
当我们要把自己的项目部署到远程仓库供其他人使用时,需要在pom 中进行配置,主要是对distributionManagement 元素的配置
<distributionManagement>
<repository>
<id>meituan-nexus-releases</id>
<name>Meituan Nexus Repository</name>
<url>http://maven.sankuai.com/nexus/content/repositories/releases/</url>
</repository>
<snapshotRepository>
<id>meituan-nexus-snapshots</id>
<name>Meituan Nexus Repository</name>
<url>http://maven.sankuai.com/nexus/content/repositories/snapshots/</url>
</snapshotRepository>
</distributionManagement>
distributionManagement 包含repository 和snapshotRepository 子元素,前者表示发布版本(稳定版本)构件的仓库,后者表示快照版本(开发测试版本)的仓库。这两个元素都需要配置id、name和url,id为远程仓库的唯一标识,name是为了方便人阅读,关键的url表示该仓库的地址。
往远程仓库部署构件的时候,往往需要认证,配置认证的方式同上。
配置正确后,运行命令mvn clean deploy,Maven就会将项目构建输出的构件部署到配置对应的远程仓库。
配置仓库镜像:
如果仓库X可以提供仓库Y存储的所有内容,那么就可以认为X是Y的一个镜像。换句话说,任何一个可以从仓库Y获得的构件,都能够从它的镜像中获取。举个例子,http://maven.oschina.net/content/groups/public/ 是中央仓库http://repo1.maven.org/maven2/ 在中国的镜像,由于地理位置的因素,该镜像往往能够提供比中央仓库更快的服务。因此,可以配置Maven使用该镜像来替代中央仓库。需要修改settings.xml 中的配置,如下
<mirrors>
<mirror>
<id>maven.oschina.net</id>
<name>maven mirror in China</name>
<url>http://maven.oschina.net/content/groups/public/</url>
<mirrorOf>central</mirrorOf>
</mirror>
</mirrors>
该例中,mirrorOf的值为central,表示该配置为中央仓库的镜像,任何对于中央仓库的请求都会转至该镜像,用户也可以使用同样的方法配置其他仓库的镜像。id表示镜像的唯一标识符,name表示镜像的名称,url表示镜像的地址。
关于镜像的一个更为常见的用法是结合私服。由于私服可以代理任何外部的公共仓库(包括中央仓库),因此,对于组织内部的Maven用户来说,使用一个私服地址就等于使用了所有需要的外部仓库,这可以将配置集中到私服,从而简化Maven本身的配置。在这种情况下,任何需要的构件都可以从私服获得,私服就是所有仓库的镜像。这时,可以配置这样的一个镜像:
<!--配置私服镜像-->
<mirrors>
<mirror>
<id>nexus</id>
<name>internal nexus repository</name>
<url>http://183.238.2.182:8081/nexus/content/groups/public/</url>
<mirrorOf>*</mirrorOf>
</mirror>
</mirrors>
该例中<mirrorOf>的值为星号,表示该配置是所有Maven仓库的镜像,任何对于远程仓库的请求都会被转至http://183.238.2.182:8081/nexus/content/groups/public/ 。如果该镜像仓库需要认证,则配置一个id为nexus的认证信息即可。
需要注意的是,由于镜像仓库完全屏蔽了被镜像仓库,当镜像仓库不稳定或者停止服务的时候,Maven仍将无法访问被镜像仓库,因而将无法下载构件。
六. Maven 生命周期与插件
生命周期
Maven 的生命周期就是为了对所有的构建过程进行抽象和统一。Maven 总结了一套高度完善的、易拓展的生命周期。包括清理、初始化、编译、测试、打包、集成测试、验证、部署和站点生成等几乎所有构建步骤。即几乎所有项目的构建,都能映射到这样一个生命周期上。Maven 的生命周期是抽象的,本身不做任何实际的工作,实际的任务都交给插件来完成。类似于模板方法模式。只定义次序,不提供具体实现。
Maven 提供了三套生命周期,即:clean、default、site,三套生命周期互相独立,每套生命周期都包含一些阶段,阶段是有序的,即要执行后面的阶段,必须会先执行前面的阶段。
1)clean:在构建之前执行一些清理工作
包含阶段:
阶段 | 作用 |
---|---|
pre-clean | 执行一些需要在clean之前完成的工作 |
clean | 移除所有上一次构建生成的文件 |
post-clean | 执行一些需要在clean之后立刻完成的工作 |
2)default:构建项目的核心部分,包括编译、测试、打包、部署等
包含阶段:
阶段 | 作用 |
---|---|
validate | 验证项目是否正确, 以及所有为了完整构建必要 的信息是否可用 |
initialize | 初始化构建状态,比如生成properties,创建目录等 |
generate-sources | 生成所有需要包含在编译过程中的源代码 |
process-sources | 处理源代码,比如过滤一些值 |
generate-resources | 生成所有需要包含在打包过程中的资源文件 |
process-resources | 复制并处理资源文件至目标目录, 准备打包。一般来说,是对src/main/resources目录的内容进行变量替换等工作后,复制到项目输出的主classpath目录中。 |
compile | 编译项目的主源码。一般来说,是编译src/main/java目录下的Java文件至项目输出的主classpath目录下 |
process-classes | 后处理编译生成的文件,例如对Java类进行字节 码增强(bytecode enhancement) |
generate-test-sources | 生成所有包含在测试编译过程中的测试源码 |
process-test-sources | 处理测试源码,比如过滤一些值 |
generate-test-resources | 生成测试需要的资源文件 |
process-test-resources | 复制并处理资源文件至测试的目标目录。一般来说,是对src/test/resources目录的内容进行变量替换等工作后,复制到项目输出的测试classpath目录中 |
test-compile | 编译项目的测试代码。一般来说,是对编译src/test/java目录下的Java文件至项目输出的测试classpath目录中 |
process-test-classes | 处理test-compile 之后生成的文件,比如对Java 类的二进制代码进行增强操作。这个阶段是maven 2.0.5 之后才有 |
test | 使用单元测试框架运行测试,测试代码不会被打包或部署 |
prepare-package | 在真正的打包之前,执行一些准备打包必要的操作。这通常会产生一个包的展开的处理过的版本 |
package | 接受编译好的代码,打包成可发布的格式,如JAR |
pre-integration-test | 执行一些在集成测试运行之前需要的动作。如建立集成测试需要的环境 |
integration-test | 如果有必要的话,处理包并发布至集成测试可以运行的环境 |
post-integration-test | 执行一些在集成测试运行之后需要的动作。如清理集成测试环境 |
verify | 执行所有检查,验证包是有效的,符合质量规范 |
install | 将包安装到Maven本地仓库,供本地其他Maven项目使用 |
deploy | 将最终的包复制到远程仓库,供其他开发人员和Maven项目使用 |
3)site:生成项目报告,站点,发布站点
包含阶段:
阶段 | 作用 |
---|---|
pre-site | 执行一些在生成项目站点之前需要完成的工作 |
site | 生成项目站点的文档 |
post-site | 执行一些在生成项目站点之后需要完成的工作 |
site-deploy | 将生成的项目站点发布在服务器上 |
参考文档:http://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html
插件
1)插件目标:
对于插件,为了能复用代码,一个插件有多个功能,每个功能称为一个插件目标,比如maven-dependency-plugin 插件,可以实现以下三种任务
- dependency:analyze:分析项目依赖,帮助找出潜在的无用依赖
- dependency:tree:列出项目的依赖树,帮助分析依赖来源
- dependency:list:列出项目所有已解析的依赖
那么插件目标是如何实现复用的呢,这就涉及到插件目标和阶段(phase)的绑定
2)插件绑定:
通过将插件的目标(goal)与 build 生命周期 中 phase 绑定到一起,这样,当要执行某个 phase 时,就调用插件来完成绑定的目标。它们之间的一个关系如图所示
从图中可以看出,每一个阶段可以绑定0 个 或 多个目标,每个插件可以提供 1 个或多个目标。
maven 中内置了一些绑定:
当项目的打包方式(<packing/>)为 ejb / ejb3 / jar / par / rar / war 时,内置绑定如下:
阶段 | 目标 |
---|---|
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 |
当打包方式为 maven-plugin 时,内置绑定:
阶段 | 目标 |
---|---|
generate-resources | plugin:descriptor |
process-resources | resources:resources |
compile | compiler:compile |
process-test-resources | resources:testResources |
test-compile | compiler:testCompile |
test | surefire:test |
package | jar:jar and plugin:addPluginArtifactMetadata |
install | install:install |
deploy | deploy:deploy |
当打包方式为 pom 是,内置绑定:
阶段 | 目标 |
---|---|
package | site:attach-descriptor |
install | install:install |
deploy | deploy:deploy |
当然我们也可以自定义绑定,例如:
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>aspectj-maven-plugin</artifactId>
<version>1.8</version>
<executions>
<execution>
<id>name</id>
<phase>process-classes</phase>
<goals>
<goal>compile</goal>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
</plugin>
execlusions下的每一个execlusion用来配置执行一个任务,<id> 是任务名称。绑定之后,利用maven 命令可以看到该插件被使用。
phase 指定 生命周期的阶段,这里我们将AspectJ 插件的目标绑定到了process-classes 阶段,goal 用来指定 插件目标。
3)插件配置:
可以对插件目标进行配置,以满足项目需求,比如自定义插件目标和生命周期阶段的绑定,配置插件目标的参数以调整其所执行的任务等
命令行配置:
例如:mvn install -Dmaven.test.skip=true
参数-D是Java自带的,其功能是通过命令行设置一个Java系统属性,Maven简单地重用了该参数,在准备插件的时候检查系统属性,便实现了插件参数的配置
其他参数:
-D 传入属性参数
-U 强制去远程参考更新snapshot包
-P 使用pom中指定的配置
-e 显示maven运行出错的信息
-o 离线执行命令,即不去远程仓库更新包
-X 显示maven允许的debug信息
使用pom 文件配置:
<plugin>
<configuration>
<compilerId>groovy-eclipse-compiler</compilerId>
<source>${java-version}</source>
<target>${java-version}</target>
<encoding>${encoding}</encoding>
</configuration>
</plugin>
4)插件解析:
与依赖一样,插件同样存储在maven仓库,使用的时候maven从本地仓库寻找插件,找不到就去远程插件仓库寻找,然后下载到本地仓库使用。同样可以在pom或者settings中添加远程插件仓库配置。
pom中是使用 pluginRepositories 元素声明仓库地址。
插件前缀与groupId:artifactId 是一一对应的,匹配关系存在仓库元数据中,这里的元数据为groupId/maven-metadata.xml, groupId 由两个中央仓库确定。
七. POM 的聚合与继承
软件设计人员往往会采用各种方式对软件划分模块,以得到更清晰的设计及更高的重用性。因此当把Maven应用到实际的项目中的时候,也需要将项目分成不同的模块。Maven的聚合特性,能够将项目的各个模块聚合在一起构建,而maven的继承特性则能帮助抽取各模块相同的依赖和插件等配置,在简化 POM的同时,还能促进各模块配置的一致性。
聚合
如果想在一个项目中构建出多个子module,而子module中又有很多公共依赖,此时我们就需要将子module公共部分抽象出来,来生成一个父模块,父模块包含子模块公共部分。
mvnProj 为聚合模块, 没有src,只有个pom,因为聚合模块只是聚合其他模块的,本身无实际内容
对聚合模块的pom来说,packaging的值必须是pom,通过module元素来声明子模块,每个module的值都是当前pom的相对目录
<project>
<packaging>pom</packaging>
<modules>
<module>demo</module>
<module>library</module>
</project>
例子:运行mvn clean install
maven会解析聚合模块的pom,计算出一个构建顺序,然后依次构建各个模块。
继承
继承由聚合发展而来,如上图所示,demo 模块和 library 模块继承了mvnProj 模块,需要在pom 文件中指定parent
<project>
<parent>
<artifactId>mvnProj</artifactId>
<groupId>com.whx</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
</project>
子模块中没有声明自己的groupId和version,默认从父模块中继承这两个元素,那么都有哪些元素可以被继承呢
有:groupId、version、description、organization、contributors、developers、issueManagement、properties、dependecies、dependencyManagement、repositories、build、reporting
现在有一个问题,比如我在原有项目基础上又新增一个模块C,但是它依赖的部分和其他两个模块依赖的部分的关系不确定,这时面临重写父模块依赖的情况,但其实一个更好的做法是在父模块中使用 dependencyManagement 元素来声明依赖,子模块根据自己所需再进行声明,详见之前介绍pom 文件时的 dependencyManagement 用法。
反应堆
反应堆指的就是这个构件结构,他是由Maven自动分析出来的,基本原则就是按照module的声明顺序进行解析,如果某一module依赖另一module,则遍历到他的根module,然后递归的进行构件。
裁剪反应堆:既然Maven有能力解析,那么也能相应的通过命令指定构件的模块
比如如下反应堆:parent, child1, child2
mvn clean install -pl child1 child2 #-pl 指定构件的模块, child1 child2被构件
mvn clean install -pl child1 -am #-am 表示同时构件其父模块,parent child1被构件
mvn clean install -pl parent -amd #-amd 表示同时构件其子模块,parent child1 child2被构件
mvn clean install -rf child1 #-rf 指示从反应堆的哪个模块开始构件,child1 child2被构件
八. 环境差异化构建
Maven 属性
在pom 文件中,maven 可以使用的属性有以下几种:
profile
之前我们在介绍pom.xml 文件时就曾介绍过profile 元素,这个元素就是用来设置或者覆盖配置默认值,为不同的环境定制构建,然后就可以在运行Maven的时候激活相应的profile来使用相应的配置。
最常见的我们在测试和发布时使用的数据库、url、密码等就是不同的,我们都可以通过profile 来定义不同的配置,例如
<profiles>
<profile>
<id>local</id>
<activation>
<!-- 指定默认激活 -->
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<url>localhost</url>
<name>local_name</url>
<pwd>1234567</pwd>
</properties>
<build>
<resources>
<!-- 通过resource指定不同profile加载配置文件的路径, 可以指定多个resource路径 -->
<resource>
<directory>src/main/profiles/local</directory>
</resource>
<resource>
<directory>src/main/resources</directory>
</resource>
</resources>
</build>
</profile>
<profile>
<id>release</id>
<properties>
<url>www.hhh.com</url>
<name>release_name</url>
<pwd>7654321</pwd>
</properties>
<build>
<resources>
<resource>
<directory>src/main/profiles/release</directory>
</resource>
<resource>
<directory>src/main/resources</directory>
</resource>
</resources>
</build>
</profile>
<profiles>
上例中,我们定义的properties 使用时就像在project 节点下定义的properties的使用一样,如 database.jdbc.url = ${url}
激活profile 的方式:
- 命令行激活,使用-P加profile的id,如 mvn compile -Prelease
- settings里显式激活,activeProfiles元素,如 <activeProfiles>local</activeProfiles>
- 系统属性激活,pom里配置activation属性,详见前文pom 文件解析
- 默认激活,定义profile的时候指定其默认激活,即上例中的activeByDefault 元素=true
如果pom中有任何一个profile使用其他任一方式激活了,那么所有默认激活配置失效。
查看当前激活的profile:mvn help:active-profiles
查看当前所有profile:mvn help:all-profiles
profile的种类:
- 在pom中声明,只对当前项目有效
- 用户settings中声明,对当前用户的maven项目有效
- 全局settings中声明,对所有maven项目有效
pom中声明的profile,可使用的元素非常多
settings中声明的profile,由于只是对于本机的配置,存在移植性问题,所以可使用的元素非常少:repositories、pluginRepositories、properties