Maven依赖管理与项目构建

517 阅读14分钟

简介

优秀的构建工具

日常工作中,如果除了编写源码,还需要编译、运行单元测试、生成文档、打包和部署等繁琐且不起眼的工作上,这个过程就是构建。如果还是手工这样做,成本太高了。Maven抽象出来了构建的生命周期模型,并使用插件来完成我们的任务

强大的依赖管理

如果java程序依赖的类库很多,需要引用到项目中来。随着依赖的增多,版本不一致、版本冲突、依赖臃肿的问题接踵而来。Maven提供了一个优秀的解决方案,通过一个坐标系统定位一个构件,也就是通过一组坐标Maven能够找到一个Java类库(如jar文件)。Maven提供了一个免费的中央仓库,几乎囊括了任何流行的开源类库

坐标与依赖

Maven定义规则:任何一个构件都可以使用Maven坐标唯一标志,坐标包括groupId、artifactId、version、packaging、classifer。通过坐标可以在maven仓库中定位到一个构件。构件文件名称与坐标相对应,一般规则为artifactId-version [-classifier].packaging.

坐标

1.groupId:定义当前Maven项目隶属的实际项目

实际项目与Maven项目不是一对一关系,而实际项目可能包含多个Maven项目
实际项目一般不是定义到公司名称,而是定义到公司下的一个项目名称

2.artifactId:定义实际项目中的一个Maven项目

3.version:定义Maven项目当前所有的版本

4.packaging:定义Maven项目的打包方式

5.classifier:定义构件输出的一些附属构件

附属构件与主构件对应,比如主构件app-2.0.0.jar, 附属构件app-2.0.0-javadoc.jar、app-2.0.0-source.jar
该项不能被直接定义,附属构件需要通过插件帮助生成

依赖配置

 <dependency>
      <groupId>...</groupId>
      <artifactId>...</artifactId>
      <version>...</version>
      <type>...</type>  --对应项目坐标的packaging,默认为jar
      <scope>...</scope>  --依赖范围
      <optional>...</optional> --依赖是否可选
      <exclusions>  --排除传递性依赖
        <exclusion> 
        	...
        </exclusion>
      </exclusions>
    </dependency>

依赖范围

一个重要的概念:Maven在编译项目主代码的时候使用一套classpath;在编译和执行测试代码的时候会使用另一套classpath;实际运行项目的时候又使用一套classpath

依赖范围是用来控制依赖与这三种classpath(编译classpath、测试classpath、运行classpath)的关系,具体如下所示,Y表示有效,-表示无效

传递性依赖

如果A依赖B,B依赖C,那么B是A的直接依赖,这是第一直接依赖,C是B的直接依赖,这是第二直接依赖,C是A的传递性依赖。第一直接依赖的范围和第二直接依赖的范围决定了传递性依赖的范围。

如果第二直接依赖声明为了可选依赖,那么不具备传递性

依赖调解

第一原则:路近最近者优先

如果项目A有这样的依赖关系A->B->C->X(1.0),A->D->X(2.0),X是A的传递性依赖,但是两条依赖路径上有两个版本的X,那么哪个X会被引入呢?按照第一原则,X(2.0)会被引入

第二原则:第一声明优先

如果项目A有这样的依赖关系A->B->X(1.0),A->C->X(2.0),X是A的传递性依赖,但是两条依赖路径上有两个版本的X,那么哪个X会被引入呢?按照第一原则已经无法区分,需要按照第二原则引入先声明的X(1.0)

排除依赖

传递性依赖会给项目隐式的引入很多依赖,极大的简化了项目依赖的管理,但是在某些情况下也可能带来问题,因此需要排除依赖。哪些情况下需要排除依赖呢?

情况一:A->B->C,但是C是快照版本,由于不稳定会直接影响当前项目A。通常需要在A引入B的时候排除掉C,同时在A项目中显式的引用依赖C的稳定版本

情况二:A->E->F,但是F可能因为版权等问题,在中央仓库没有,当时有一个可替换的实现H。那么可以在A引入E的时候排除掉F,同时在A项目中显式的引用依赖H

请思考:如果不排除直接在A中引入C稳定版本或者H,按照依赖调解原则自行解析是否也是可行的呢?

查看依赖

当依赖经过Maven解析后,构成了一棵依赖树,通过下面命令可以很清楚看到某个依赖是通过那条传递路径引入的

mvn dependency:tree

分析依赖可以通过下面命令,分析结果有两个重要部分, 一部分是没有显式声明的依赖但是被使用了,这部分风险是依赖升级了那么传递性依赖也跟着升级了; 一部分是显式声明了但是未被使用,这部分展示的是编译主代码和测试代码需要用到的依赖,执行测试和运行时需要的依赖无法被该命令发现,比如spring-core

mvn dependency:analye

仓库

布局

任何一个构件都有其唯一的坐标,根据这个坐标可以定义其在仓库中的唯一存储路径,这就是Maven的仓库布局方式。Maven仓库是基于简单文件系统存储的。

分类

Maven仓库分为本地仓库和远程仓库。

当Maven根据坐标寻找构件的时候,首先会查看本地仓库,如果本地仓库存在该构件则直接使用;如果本地仓库不存在该构件,或者需要查看是否有更新的构件版本,Maven会去远程仓库查找,发下需要的构件后下载到本地仓库再使用。如果本地仓库和远程仓库都没有需要的额构件,Maven就会报错。

本地仓库

通过settings.xml文件指定,如果未指定则默认为~/.m2/repository

 <!-- localRepository
   | The path to the local repository maven will use to store artifacts.
   |
   | Default: ${user.home}/.m2/repository
  <localRepository>/path/to/local/repo</localRepository>
  -->

  <localRepository>F:/repository</localRepository>

远程仓库

  • 中央仓库

Maven自带了远程仓库,可以打开$M2_HOME/lib/maven-model-builder-3.0.jar并访问org/apache/maven/model/pom-4.0.0.xml查看,这个文件也是超级POM,所有Maven项目都会继承超级POM

<repositories>
    <repository>
      <id>central</id>
      <name>Central Repository</name>
      <url>https://repo.maven.apache.org/maven2</url>
      <layout>default</layout>
      <snapshots>
        <enabled>false</enabled>
      </snapshots>
    </repository>
  </repositories>
  • 私服

架设在局域网内的仓库服务,代理广域网上的远程仓库

当Maven需要下载构件的首,它从私服请求,如果私服上不存在构件,则从外部的远程仓库下载,缓存在私服上之后,再为Maven的下载请求提供服务。一些无法从外部仓库下载的构件(自定义的构件)也么可以从本地上传到私服上供大家使用

  • 其它仓库

如ali maven仓库等

配置远程仓库

在一些情况下,Maven默认的中央仓库无法满足需求,构件在其它远程仓库里,或者不想用默认的远程仓库(下载慢等原因),可以覆盖Maven的中央仓库,配置其它远程仓库。配置如下:

<repositories>
    <repository>
      <id>ali</id> --仓库唯一id,中央仓库的id是central,如果命名成central会覆盖中央仓库
      <name>Ali Repository</name> --仓库名称
      <url>https://maven.aliyun.com</url> --仓库地址
      <layout>default</layout> --仓库布局
      <releases> --设置稳定版本策略
      	<enabled>true</enabled> --支持下载稳定版本
        <updatePolicy>daliy</updatePolicy> --配置maven从远程仓库检查更新的频率,默认daliy。never表示从来不检查;interval:X表示间隔X分钟检查一次;daliy表示Maven每天检查一次;always表示每次构建都检查更新
        <checksumPolicy>ignore</checksumPolicy> --配置maven检查校验和文件校验失败后的策略,默认warn。ignore表示构建时候忽略校验和失败;warn表示构建时候输出告警信息;fail表示让构建失败
      </releases>
      <snapshots> --设定快照版本策略,同上
        <enabled>false</enabled> --关闭下载快照版本
      </snapshots>
    </repository>
  </repositories>

配置仓库认证

出于安全的考虑,通常会给仓库加上认证信息(如账号密码)。远程仓库可以配置在pom或setting文件中,但是仓库认证只能配置再setting,这也是出于安全考虑(pom共享的,setting一般只在本机)

 <servers>
    <server>
      <id>ali</id>  --配置哪个仓库的认证信息,id与配置的仓库id必须一致
      <username>admin</username>
      <password>admin123</password>
    </server>
</servers>

部署到远程仓库

私服的一个作用是用于部署第三方构件或者自己写的构件,如何部署将构件部署到私服仓库呢?可以登录对应的仓库手动的上传构件;也可以配置pom文件结合Maven的deploy命令部署构件到远程仓库,配置如下(如果需要认证见上节配置)

 <distributionManagement>
        <snapshotRepository> --发布快照版本,也要看指定的仓库能否支持快照版本
            <id>snapshots</id> --自定义
            <name>snapshots仓库</name> --仓库名称
            <url>http://xx.com.cn/content/repositories/snapshots</url> --仓库地址
        </snapshotRepository>
        <repository> --发布稳定版本,也要看指定的仓库能否支持稳定版本
            <id>thirdparty</id>
            <name>thirdparty仓库</name>
            <url>http://xx.com.cn/content/repositories/thirdparty</url>
        </repository>
    </distributionManagement>

镜像

如果仓库X可以提供仓库Y存储的所有内容,则X是Y的一个镜像。 maven.net.cn/content/gro… 是中央仓库repol.maven.org/maven2 在中国的镜像,由于地址位置的因素,该镜像能够提供比中央仓库更快的服务,因此可以配置Maven使用该镜像俩替代中央仓库。镜像只能配置再setting文件中。

  <mirrors>
	<mirror>
	    <id>nexus-aliyun</id> --镜像唯一标志
	    <mirrorOf>central</mirrorOf> --表示配置为谁的镜像(和仓库id比较),可以用逗号隔开的多个,可以*表示所有仓库
	    <name>Nexus aliyun</name> --镜像名称
	    <url>http://maven.aliyun.com/nexus/content/groups/public</url> --镜像地址
	</mirror>
  </mirrors>

如果需要认证信息,则基于id配置认证信息即可。

从仓库依赖解析的机制

Maven是怎样从仓库解析并使用依赖构件的呢?

  • 当依赖范围是system时候直接从本地文件系统解析构件
  • 根据依赖坐标计算仓库路径后,尝试直接从本地仓库寻找构件,如果发现相应构件,则解析成功
  • 在本地仓库不存在相应构件时,如果依赖的版本是显式发布版本构件,如1.2,则遍历所有远程仓库,发现后下载并使用构件
  • 如果依赖的构件是RELEASE或LATEST,则基于更新策略读取所有远程仓库的元数据groupId/artifactId/maven-metadata.xml,将其与本地仓库对应的元数据合并后计算出RELEASE或LATEST真实的值,然后基于这个值检查本地和远程仓库,如步骤2、3
  • 如果依赖的构件是SNAPSHOT,则基于更新策略读取所有远程仓库的元数据groupId/artifactId/version/maven-metadata.xml,将其与本地仓库对应的元数据合并后计算出SNAPSHOT真实的值,然后基于这个值检查本地和远程仓库,如步骤2、3
  • 如果最后解析到的版本是带时间戳的快照,则复制其时间戳文件(如redis-core-1.0.5-20210223.073400-7.jar)到非时间戳格式文件(redis-core-1.0.5-SNAPSHOT.jar),并使用非时间戳格式的构件

生命周期与插件

Maven的生命周期是对构建过程的抽象和统一,这些生命周期包括清理、编译、测试、打包、部署、站点等几乎所有构建步骤。Maven的生命周期并没有具体实现,具体的功能是通过插件实现的。

三套生命周期

定义了每套生命周期包含的阶段,执行后阶段会先执行同一生命周期的前面阶段,但不会影响其它生命周期

clean生命周期

pre-clean --执行清理前需要完成的工作
clean --清理上一次构建生成的文件
post-clean --执行清理后需要完成的工作

default生命周期

process-resource --对src/main/resources目录的内容进行变量替换等工作后,复制到项目输出的主classpath目录
...
compile --编译src/main/java目录的内容并输出到主classpath目录
...
process-test-resource --对src/main/resources目录的内容进行变量替换等工作后,复制到项目输出的住classpath目录
...
test-compile --编译src/test/java目录的内容并输出到主classpath目录
...
test --使用测试框架运行测试,测试代码不会被打包
...
package --将编译好的代码打包成发布的格式,比如jar、war
...
verify
install --将包安装到本地仓库,提供本地的其它Maven项目使用
deploy --将包复制到远程仓库,提供其它开发人员和Maven项目使用

sit生命周期

pre-sit
sit
post-sit
sit-deploy

调用生命周期

Maven可以通过命令行调用生命周期,比如

mvn clean
mvn package
mvn sit
mvn clean install
mvn clean install sit

插件绑定

插件目标

插件功能通常不是单一的,提供了许多功能,每个功能就对应一个目标。比如maven-dependency-plugin有很多目标,比如dependency:tree、denpendency:analyze,冒号之前是插件前缀,冒号之后是插件目标

插件绑定

Maven生命周期的阶段与插件的目标相互绑定,以完成某个具体的任务。比如

  • 内置绑定

Maven为一些主要的生命周期阶段绑定了很多插件的目标。如下所示,其中default生命周期使用的插件与打包类型有关系,以下default是打包为jar类型的插件使用情况

  • 自定义绑定

除了内置绑定,还可以定义选择将某个目标绑定到生命周期的某个阶段,这样在构建过程中能执行更多富有特色的任务。常见的自定义有生成源码jar包,如下所示,将maven-source-plugin:jar-no-fork目标绑定到default生命周期的compile阶段上

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-source-plugin</artifactId>
                <version>3.0.0</version>
                <executions>
                    <execution>
                        <phase>compile</phase>  --绑定的生命周期阶段
                        <goals> --绑定插件目标
                            <goal>jar-no-fork</goal> --绑定插件的具体目标,可以有多个<goal>标签
                        </goals>
                    </execution>
                </executions>
           </plugin>
       </plugins>
   </build>

如果只是声明插件

    <build>
        <plugins>
           <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-source-plugin</artifactId>
                <version>3.0.0</version>
           </plugin>
       </plugins>
   </build>

执行mvn verify发现jar-no-fork目标也被执行了。这是因为插件的目标已经定义了默认绑定生命周期阶段,如果声明了插件则生命周期被执行的时候对应的目标会被执行。怎么查看一个插件的目标是否默认绑定了生命周期阶段呢?

mvn help:describe -Dplugin=groupId:artifactId:version -Dgoal=目标 -Ddetail

groupId:artifactId:version可以使用目标前缀替代
-Dgoal=目标 指定查看的插件目标
-Ddetail 输出详细信息

示例

如果在生命周期的同一个阶段,那么先声明的插件目标先执行;插件目标绑定到不同的生命周期阶段的时候,执行顺序有生命周期阶段的先后顺序决定

插件解析机制

聚合与继承

聚合

聚合模块知道哪些模块被聚合了,被聚合模块不知道聚合模块

Maven解析校验和模块的POM,分析构建的模块,并计算出一个反应堆构建顺序,然后根据这个顺序依次构建各个模块。

聚合模块有两种目录结构

反应堆

所有模块组成的一个构建结构。如果是单模块的项目,反应堆就是该模块本身。如果是多模块项目,反应堆就包含了各模块之间继承和聚合的关系。

  • account-aggregator聚合以下模块

  • 反应堆

构建顺序

聚合工程的构建顺序是根据反应堆计算出来的。Maven按序读取POM,如果该POM没有依赖模块,那么就构建该模块,否则就先构建其依赖模块,如果该依赖还依赖其它模块,则进一步先构建依赖的依赖。

  • 上述反应堆构建顺序

继承

子模块知道继承哪个父模块,父模块不知道被那些子模块继承

所有模块默认都继承超级POM。

子模块的POM需要指定普通父POM的相对位置。

父POM中有很多可以被继承的POM元素。

依赖管理

Maven提供了dependencyManagement元素可以既让子模块继承父模块的依赖配置,又能保证子模块依赖使用的灵活性。在该元素的依赖声明不会引入实际的依赖,不过它能够约束dependencies下的依赖使用。子模块引用父模块dependencyManagement声明的依赖只需指定groupId和artifactId就行

插件管理

Maven提供了pluginManagement元素管理插件依赖。在该元素的插件声明明不会生效,在子模块中声明后才会生效,且声明的时候只需指定groupId和artifactId就行

dependencyManagement和pluginManagement元素能够统一项目范围中依赖的版本

灵活构建

Maven为了支持构建的灵活性,内置了三大特性,即属性、Profile和资源过滤。

Maven属性

内置属性:主要有两个内常用的内置属性,basedir表示项目根目录,即包含pom.xml文件的目录;{basedir}表示项目根目录,即包含pom.xml文件的目录;{version}表示项目版本

自定义属性:在POM的定义的属性

 <properties>
        <encoding>UTF-8</encoding>
        <spring-version>3.2.2.RELEASE</spring-version>
        <spring-integration-version>2.2.2.RELEASE</spring-integration-version>
    </properties>

POM属性:引用POM文件中对应的元素的值。比如${project.artifactId}就对应了元素的值,其它的与此类似

settings属性:与POM属性同理,以settings开头引用settings.xml文件中XML元素的值。比如${settings.localRepository}获取本地仓库的地址

环境变量:所有环境变量都可以使用env.开头引用。比如${env.JAVA_HOME}获取JAVA_HOME环境变量的值

Java系统属性

Maven Profile

定义profile

<profiles>
	<profile>
		<id>dev</id> --profile的id
		<activation> 
			<activeByDefault>true</activeByDefault> --默认激活
		</activation>
		<properties> --自定义属性
			<maven.log-level>DEBUG</maven.log-level>
			<maven.sql-log-level>DEBUG</maven.sql-log-level>
			<maven.console-log-enable>true</maven.console-log-enable>
		</properties>
	</profile>
	<profile>
		<id>uat</id>
		<properties>
			<maven.log-level>INFO</maven.log-level>
			<maven.sql-log-level>INFO</maven.sql-log-level>
			<maven.console-log-enable>false</maven.console-log-enable>
		</properties>
	</profile>
</profiles>

激活profile

  • -P参数加上profile的id
mvn clean package -Puat
  • 默认激活
<activation> 
	<activeByDefault>true</activeByDefault> --默认激活
</activation>

资源过滤

Maven属性默认只有在POM中才会被解析。默认无法解析src/main/resources资源目录下的文件中的maven属性,如何让Maven解析资源文件中的Maven属性?

maven-resources-plugin插件能够让src/main/resources资源目录下文件的Maven属性被正确替换,即开启资源过滤

主资源目录开启过滤
<build>
   <resources>
        <resource>
            <directory>src/main/resources</directory>
            <filtering>true</filtering>
        </resource>
   </resources>
</build>

测试资源目录开启过滤
<build>
   <resources>
        <resource>
            <directory>src/test/resources</directory>
            <filtering>true</filtering>
        </resource>
   </resources>
</build>

以下这个配置

首先获取env的值,比如为uat,然后获取filter-uat.properties文件中的属性值以及其它Maven属性,最后替换src/main/resources文件的属性

<build>
   <filters>
       <filter>src/main/resources/META-INF/filters/filter-${env}.properties</filter>
   </filters>
   <resources>
       <resource>
           <directory>src/main/resources</directory>
           <filtering>true</filtering>
       </resource>
   </resources>
</build>