Maven 仓库
何为Maven仓库
在Maven世界中,任何一个依赖、插件或者项目构建的输出,都可以称为构件。例如依赖log4j-1.2.15.jar是一个构件,插件maven-compiler-plugin-2.0.2.jar是一个构件,项目构建完成后的输出jar包也是一个构建,任何一个构件都有一组坐标唯一标识。
在一台工作台上,可能会有几十个Maven项目,所有项目都使用maven-compiler-plugin,这些项目中的大部分都用到了log4j,有一小部分用到了Spring Framework,还有另外一小部分用到了Struts2。在每个有需要的项目中都放置一份重复的log4j或者struts2显然不是最好的解决方案,这样做不仅造成了磁盘空间的浪费,而且也难于统一管理,文件的复制等操作也会降低构建的速度。而实际情况是,在不使用Maven的那些项目中,我们往往就能发现命名为lib/的目录,各个项目lib/目录下的内容存在大量的重复。
得益于坐标机制,任何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
private static final char PATH_SEPARATOR = '/';
private static final char GROUP_SEPARATOR = '.';
private static final char ARTIFACT_SEPARATOR = '-';
public String pathOf( Artifact artifact )
{
ArtifactHandler artifactHandler = artifact.getArtifactHandler();
StringBuilder path = new StringBuilder( 128 );
path.append( formatAsDirectory( artifact.getGroupId() ) ).append( PATH_SEPARATOR );
path.append( artifact.getArtifactId() ).append( PATH_SEPARATOR );
path.append( artifact.getBaseVersion() ).append( PATH_SEPARATOR );
path.append( artifact.getArtifactId() ).append( ARTIFACT_SEPARATOR ).append( artifact.getVersion() );
if ( artifact.hasClassifier() )
{
path.append( ARTIFACT_SEPARATOR ).append( artifact.getClassifier() );
}
if ( artifactHandler.getExtension() != null && artifactHandler.getExtension().length() > 0 )
{
path.append( GROUP_SEPARATOR ).append( artifactHandler.getExtension() );
}
return path.toString();
}
private String formatAsDirectory( String directory )
{
return directory.replace( GROUP_SEPARATOR, PATH_SEPARATOR );
}
该pathOf()方法的目的是根据构件信息生成其在仓库中的路径。
仓库的分类
对于Maven来说,仓库只分为两类:本地仓库和远程仓库。当Maven根据坐标寻找构件的时候,它首先会查看本地仓库,如果本地仓库存在此构件,则直接使用;如果本地仓库不存在此构件,或者需要查看是否有更新的构建版本,Maven就会去远程仓库查找,发现需要的构件之后,下载到本地仓库再使用。如果本地仓库和远程仓库都没有需要的构件,Maven就会报错。
在这个最基本分类的基础上,还有必要介绍一些特殊的远程仓库。中央仓库是Maven核心自带的远程仓库,它包含了绝大部分开源的构件。在默认配置下,当本地仓库没有Maven需要的构件的时候,它就会尝试从中央仓库下载。
私服是另一种特殊的远程仓库,为了节省宽带和时间,应该在局域网内架设一个私有的仓库服务器,用其代理所有外部的远程仓库。内部的项目还能部署到私服上供其他项目使用。
除了中央仓库和私服,还有很多其他公开的远程仓库,常见的有Java.net Maven库(http://download.java.net/maven/2)和JBoss Maven库(http://repository.jboss.com/maven2)等。
本地仓库
一般来说,在Maven项目目录下,没有诸如lib/这样用来存放依赖文件的目录。当Maven在执行编译或测试时,如果需要使用依赖文件,它总是基于坐标使用本地仓库的依赖文件。
默认情况下,不管是在Windows还是Linux上,每个用户在自己的用户目录下都有一个路径名为.m2/repository/的仓库目录。
可以编辑文件~/.m2/settings.xml
,设置localRepository元素的指为想要的仓库地址。
<settings>
<localRepository>D:\java\repository\</localRepository>
</setting>
这样,该用户的本地仓库地址就被设置成了D:\java\repository\
。
需要注意的是,默认情况下,~/.m2/setting.xml
文件是不存在的,用户需要从Maven安装目录复制$M2_HOME/conf/settings.xml
文件再进行编辑。不推荐直接修改全局目录的settings.xml文件。
远程仓库
安装好Maven后,如果不执行任何Maven命令,本地仓库目录是不存在的。当用户输入第一条Maven命令后,Maven才会创建本地仓库,然后根据配置和需要,从远程仓库下载构件至本地仓库。
对于Maven来说,每个用户只有一个本地仓库,但可以配置访问很多远程仓库。
中央仓库
由于最原始的本地仓库是空的,Maven必须知道至少一个可用的远程仓库,才能在执行Maven命令的时候下载到需要的构件。中央仓库就是这样一个默认的远程仓库,Maven的安装文件自带了中央仓库的配置。使用解压工具打开jar文件$M2_HOME/lib/maven-model-builder-3.0.jar
然后访问路径org/apache/maven/model/pom-4.0.0.xml
可以看到:
包含这段配置文件是所有Maven项目都会继承的超级POM。这段配置使用id central对中央仓库进行唯一标识,其名称为Central Repository,它使用default仓库布局。需要注意的是snapshots元素,其子元素enabled的值为false,表示不从该中央仓库下载快照版本的构件。
私服
私服是一种特殊的远程仓库,它是假设在局域网内的仓库服务,私服代理广域网上的远程仓库,供局域网内的Maven用户使用。当Maven需要下载构件的时候,它从私服请求,如果私服上不存在该构件,则从外部的远程仓库下载,缓存到私服上之后,再为Maven的下载请求提供服务。此外,一些无法从外部仓库下载到的构件也能从本地上传到私服上供大家使用。
私服可以
- 节省自己的外网带宽。建立私服同样可以减少组织自己的开支,大量的对于外部仓库的重复请求会消耗很大的带宽,利用私服代理外部仓库之后,对外的重复构件下载便得以消除,即降低外网带宽的压力。
- 加速Maven构件。不停地连接请求外部仓库是十分耗时的,但是Maven的一些内部机制(如快照更新检查)要求Maven在执行构建的时候不停地检查远程仓库数据。因此,当项目配置了很多外部远程仓库的时候,构建地速度会被大大降低。使用私服可以很好地解决这一问题,当Maven只需要检查局域网内私服地数据时,构建的速度便能得到很大程度地提高。
- 部署第三方构件。当某个构件无法从任何一个外部远程仓库获得,怎么办?这样地例子有很多,如组织内部生成的私有构件肯定无法从外部仓库获得、Oracle的JDBC驱动由于版本因素不能发布到公共仓库中。建立私服之后,便可以将这些构件部署到这个内部的仓库中,供内部的Maven项目使用。
- 提高稳定性,增强控制。Maven构件高度依赖于远程仓库,因此,当Internet不稳定的时候,Maven构建也会变得不稳定,甚至无法构建。使用私服后,即使暂时没有Internet连接,由于私服中已经缓存了大量构件,Maven也仍然可以正常运行。此外,一些私服软件还提供了很多额外的功能,如权限管理、RELEASE/SNAPSHOT区分等,管理员可以对仓库进行一些更高级的控制。
- 降低中央仓库的负荷。运行并维护一个中央仓库不是一件容易的事情,服务数百万的请求,存储数T的数据,需要相当大的财力。使用私服可以避免很多对中央仓库的重复下载。
远程仓库的配置
很多情况下,默认的中央仓库无法满足项目的需求,可能项目需要的构件存在于另外一个远程仓库中,如JBoss Maven仓库。这时,可以在POM中配置该仓库
<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>
在repositories元素下,可以使用repository子元素声明一个或者多个远程仓库。该例中声明了一个id为jboss,名称为JBoss Repository的仓库。任何一个仓库声明的id必须是唯一的,尤其需要注意的是,Maven自带的中央仓库使用的id为central,如果其他的仓库声明也使用该id,就会覆盖中央仓库的配置。该配置中的url指向了仓库的地址,一般来说,该地址都基于http协议,Maven用户都可以在浏览器中打开仓库地址浏览构件。
该例子中releases的enable值为true,表示开启JBoss仓库的发布版本下载支持,而snapshots的enable值为false,表示关闭JBoss仓库的快照版本的下载支持。因此,根据该配置,Maven只会从JBoos仓库下载发布版的构件,而不会下载快照版的构件。
该例子中的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仓库中时,会同时部署对应的校验和文件。在下载构件的时候,Maven会验证校验和文件,如果校验和验证失败,怎么办?当checksumPolicy的值为默认的warn时,Maven会在执行构建时输出警告信息,其他可用的值包括:fail——Maven遇到校验和错误就让构建失败;ignore——使Maven完全忽略校验和错误。
远程仓库的认证
大部分远程仓库无须认证就可以访问,但有时候出于安全方面的考虑,我们需要提供认证信息才能访问一些远程仓库。例如,组织内部有一个Maven仓库服务器,该服务器为每个项目都提供独立的Maven仓库,为了防止非法的仓库访问,管理员为每个仓库提供了一组用户名及密码。这时,为了能让Maven访问仓库内容,就需要配置认证信息。
配置认证信息和配置仓库信息不同,仓库信息可用直接配置在POM文件中,但是认证信息必须配置在setting.xml文件中。这时因为POM往往是被提交到代码仓库中供所有成员访问的,而settings.xml一般只放在本机。因此,在settings.xml中配置认证信息更为安全。
假设需要为一个id为my-proj的仓库配置认证信息,编辑settings.xml文件
<settings>
<servers>
<server>
<id>my-proj</id>
<username>repo-user</username>
<password>repo-pwd</password>
</server>
</servers>
</settings>
部署至远程仓库
Maven除了能对项目进行编译、测试、打包之外,还能将项目生成的构建部署到仓库中。首先,需要编辑项目的pom.xml文件
<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-releases</url>
</snapshotRepository>
</distributionManagement>
distributionManagement包含repository和snapshotRepository子元素,前者表示发布版本构件的仓库,后者表示快照版本的仓库。
配置正确后,在命令行运行mvn clean deploy,Maven就会将项目构建输出的构件部署到配置对应的远程仓库,如果当前的版本是快照版本,则部署到快照版本仓库地址,否则就部署到发布版本仓库地址。
从仓库解析依赖的机制
当本地仓库没有依赖构件的时候,Maven会自动从远程仓库下载;当依赖版本为快照版本的时候,Maven会自动找到最新的快照。这背后的依赖解析机制可用概括如下:
1)当依赖的范围是system的时候,Maven直接从本地文件系统解析构件。
2)根据依赖坐标计算仓库路径后,尝试直接从本地仓库寻找构件,如果发现相应构件,则解析成功。
3)在本地仓库不存在相应构件的情况下,如果依赖的版本是显式的发布版本构件,如1.2、2.1-beta-1等,则遍历所有的远程仓库,发现后,下载并解析使用。
4)如果依赖的版本是RELEASE或者LATEST,则基于更新策略读取多有远程仓库的元数据groupId/artifactId/maven-metadata.xml,将其与本地仓库的对应元数据合并后,计算出RELEASE或者LATEST真实的值,然后基于这个真是的值检查本地和远程仓库。
5)如果依赖的版本是SNAPSHOT,则基于更新策略读取所有远程仓库的元数据groupId/artifactId/version/maven-meradata.xml,将其与本地仓库的对应元数据合并后,得到最新快照版本的值,然后基于该值检查本地仓库,或者从远程仓库下载。
6)如果最后解析得到的构件版本是时间戳格式的快照,如1.4.1-20091104.121450-121,则复制其时间戳格式的文件至非时间戳格式,如SNAPSHOT,并使用该非时间戳格式的构件。
前面提到的RELEASE和LATEST版本,它们分别对应了仓库中存在的该构件的最新发布版本和最新版本(包含快照),而这两个最新是基于groupId/artifactId/maven-metadata.xml计算出来的。
<metadata>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<versioning>
<latest>2.5.7</latest>
<release>2.5.7</release>
<versions>
<version>1.0.0.RELEASE</version>
<version>1.0.1.RELEASE</version>
<version>1.0.2.RELEASE</version>
<version>1.1.0.RELEASE</version>
<version>1.1.1.RELEASE</version>
<version>1.1.2.RELEASE</version>
<version>1.1.3.RELEASE</version>
<version>1.1.4.RELEASE</version>
<version>1.1.5.RELEASE</version>
...
</versions>
<lastUpdated>20211118113123</lastUpdated>
</versioning>
</metadata>
该XML文件列出了仓库中存在的该构件所有可用的版本,同时latest元素指向了这些版本中最新的那个版本,该例中是2.5.7.而release元素指向了这些版本中最新的发布版本,该例中是2.5.7。Maven通过合并多个远程仓库及本地仓库的元数据,就能计算出基于所有仓库的latest和release分别是什么,然后再解析具体的构件。
需要注意的是,在依赖声明中使用LATEST和RELEASE是不推荐的做法,因为Maven随时都可能解析到不同的构件,可能今天LATEST是1.3.6,明天就成为1.4.0-SNAPSHOT了,且Maven不会明确告诉用户这样的变化。当这种变化造成构件失败的时候,发现问题会变得比较困难。
Maven3不再支持在插件配置中使用LATEST和RELEASE。如果不设置版本,其效果就和RELEASE一样,Maven只会解析最新的发布版本构件。不过即使这样,也还存在潜在问题。
当依赖的版本设为快照版本的时候,Maven也需要检查更新,这时Maven会检查仓库元数据groupId/artifactId/version/maven-metadata.xml
镜像
如果仓库X可用提供仓库Y存储的所有内容,那么就可以认为X是Y的一个镜像。换句话说,任何一个可用从仓库Y获得的构件,都能够从它的镜像中获取。
例如:http://maven.net.cn/content/groups/public/是中央仓库在中国的镜像,由于地理位置的因素,该镜像往往能够提供比中央仓库更快的服务。因此,可以配置Maven使用哦个该镜像来替代中央仓库。编辑settings.xml
<mirrors>
<mirror>
<id>maven.net.cn</id>
<mirrorOf>central</mirrorOf>
<name>one of the central mirrors in China</name>
<url>http://maven.net.cn/content/groups/public/</url>
</mirror>
</mirrors>
该例中,<mirrorOf>
的值为central,表示该配置为中央仓库的镜像,任何对于中央仓库的请求都会转至该镜像。类似地,如果该镜像需要认证,也可以基于该id配置仓库认证。
为了满足一些复杂的需求,Maven还支持更高级的镜像配置:
<mirrorOf>*</mirrorOf>
:匹配所有远程仓库。<mirrorOf>external:*</mirrorOf>
:匹配所有远程仓库,使用localhost的除外,使用file://协议的除外。也就是说,匹配所有不在本机上的远程仓库。<mirrorOf>repo1,repo2</mirrorOf>
:匹配仓库repo1和repo2,使用逗号分隔多个仓库。<mirrorOf>*,! repo1</mirrorOf>
:匹配所有远程仓库,repo1除外,使用感叹号将仓库从匹配中排除。
需要注意的是,由于镜像仓库完全屏蔽了被镜像仓库,当镜像仓库不稳定或停止服务的时候,Maven仍将无法访问被镜像仓库,因而将无法下载构件。