blog icon indicating copy to clipboard operation
blog copied to clipboard

Maven

Open WangShuXian6 opened this issue 3 years ago • 6 comments

Maven

Maven这个词可以翻译为“知识的积累”,也可以翻译为“专家”或“内行”。 跨平台的项目管理工具。 Apache组织中的一个颇为成功的开源项目 Maven主要服务于基于Java平台的项目构建、依赖管理和项目信息管理。

下载

https://maven.apache.org/download.cgi

安装

https://maven.apache.org/install

mac 上,解压文件,存放路径,示例:/Users/wangshuxian/apache-maven-3.8.2

为zsh添加配置

vi ~/.zshrc

加入如下配置

export M3_HOME=/Users/wangshuxian/apache-maven-3.8.2
export PATH=$M3_HOME/bin:$PATH

加载配置

source ~/.zshrc

查看版本

mvn -v

构建

除了编写源代码,我们每天有相当一部分时间花在了编译、运行单元测试、生成文档、打包和部署等烦琐且不起眼的工作上,这就是构建。 如果我们现在还手工这样做,那成本也太高了, 于是有人用软件的方法让这一系列工作完全自动化,使得软件的构建可以像全自动流水线一样,只需要一条简单的命令,所有烦琐的步骤都能够自动完成,很快就能得到最终结果。

Maven是优秀的构建工具

自动化构建过程,从清理、编译、测试到生成报告,再到打包和部署

我们一直在不停地寻找避免重复的方法。设计的重复、编码的重复、文档的重复,当然还有构建的重复。 Maven最大化地消除了构建的重复,抽象了构建生命周期,并且为绝大部分的构建任务提供了已实现的插件,我们不再需要定义过程,甚至不需要再去实现这些过程中的一些任务。

最简单的例子是测试,我们没必要告诉Maven去测试,更不需要告诉Maven如何运行测试,只需要遵循Maven的约定编写好测试用例,当我们运行构建的时候,这些测试便会自动运行。

Maven抽象了一个完整的构建生命周期模型,这个模型吸取了大量其他的构建脚本和构建工具的优点,总结了大量项目的实际需求。如果遵循这个模型,可以避免很多不必要的错误,可以直接使用大量成熟的Maven插件来完成我们的任务

标准化构建过程

Maven是跨平台的

依赖管理工具和项目信息管理工具

Java不仅是一门编程语言,还是一个平台,通过JRuby和Jython,我们可以在Java平台上编写和运行Ruby和Python程序。

Maven不仅是构建工具,还是一个依赖管理工具和项目信息管理工具。它提供了中央仓库,能帮我们自动下载构件。

通过一个坐标系统准确地定位每一个构件(artifact),也就是通过一组坐标Maven能够找到任何一个Java类库(如jar文件)。>Maven给这个类库世界引入了经纬,让它们变得有秩序,于是我们可以借助它来有序地管理依赖,轻松地解决那些繁杂的依赖问题。

Maven还能帮助我们管理原本分散在项目中各个角落的项目信息,包括项目描述、开发者列表、版本控制系统地址、许可证、缺陷管理系统地址等。

通过Maven自动生成的站点,以及一些已有的插件,我们还能够轻松获得项目文档、测试报告、静态分析报告、源码版本日志报告等非常具有价值的项目信息。

Maven还为全世界的Java开发者提供了一个免费的中央仓库,在其中几乎可以找到任何的流行开源类库。通过一些Maven的衍生工具(如Nexus),我们还能对其进行快速地搜索。只要定位了坐标,Maven就能够帮我们自动下载,省去了手工劳动。

Maven对于项目目录结构、测试用例命名方式等内容都有既定的规则,只要遵循了这些成熟的规则,用户在项目间切换的时候就免去了额外的学习成本,可以说是约定优于配置(Convention Over Configuration)。

WangShuXian6 avatar Aug 24 '21 07:08 WangShuXian6

Maven 配置

安装目录分析

M3_HOME

目录结构和内容

bin

boot

conf

lib

LICENSE.txt

NOTICE.txt

README.txt
bin

该目录包含了mvn运行的脚本,这些脚本用来配置Java命令,准备好classpath和相关的Java系统属性,然后执行Java命令。 其中mvn是基于UNIX平台的shell脚本,mvn.bat是基于Windows平台的bat脚本。 在命令行输入任何一条mvn命令时,实际上就是在调用这些脚本。 该目录还包含了mvnDebug和mvnDebug.bat两个文件,同样,前者是UNIX平台的shell脚本,后者是Windows平台的bat脚本。

mvn和mvnDebug 的区别和关系呢 打开文件我们就可以看到,两者基本是一样的,只是mvnDebug多了一条MAVEN_DEBUG_OPTS配置,其作用就是在运行Maven时开启debug,以便调试Maven本身。 此外,该目录还包含m2.conf文件,这是classworlds的配置文件

boot

该目录只包含一个文件,以maven 3.0为例,该文件为plexus-classworlds-2.2.3.jarplexus-classworlds是一个类加载器框架,相对于默认的java类加载器,它提供了更丰富的语法以方便配置,Maven使用该框架加载自己的类库。 更多关于classworlds的信息请参考http://classworlds.codehaus.org/。对于一般的Maven用户来说,不必关心该文件。

conf

该目录包含了一个非常重要的文件settings.xml。 直接修改该文件,就能在机器上全局地定制Maven的行为。 一般情况下,我们更偏向于复制该文件至~/.m2/目录下(~表示用户目录),然后修改该文件,在用户范围定制Maven的行为。

lib

该目录包含了所有Maven运行时需要的Java类库, Maven本身是分模块开发的,因此用户能看到诸如maven-core-3.0.jar、maven-model-3.0.jar之类的文件。 此外,这里还包含一些Maven用到的第三方依赖,如common-cli-1.2.jar、google-collection-1.0.jar等。 对于Maven 2来说,该目录只包含一个如maven-2.2.1-uber.jar的文件,原本各为独立JAR文件的Maven模块和第三方类库都被拆解后重新合并到了这个JAR文件中。 可以说,lib目录就是真正的Maven。 用户可以在这个目录中找到Maven内置的超级POM。

其他

LICENSE.txt记录了Maven使用的软件许可证Apache License Version 2.0;

NOTICE.txt记录了Maven包含的第三方软件;

README.txt则包含了Maven的简要介绍,包括安装需求及如何安装的简要指令等。


~/.m2

mvn help:system

命令会打印出所有的Java系统属性和环境变量,这些信息对我们日常的编程工作很有帮助。 运行这条命令的目的是让Maven执行一个真正的任务。 可以从命令行输出看到Maven会下载maven-help-plugin,包括pom文件和jar文件。 这些文件都被下载到了Maven本地仓库中。

打开用户目录

比如当前的用户目录是C:\Users\xxx\,你可以在Vista和Windows7中找到类似的用户目录。 如果是更早版本的Windows,该目录应该类似于C:\Document and Settings\xxx\。 在基于UNIX的系统上,直接输入cd回车,就可以转到用户目录。 为了方便,本书统一使用符号指代用户目录。

在用户目录下可以发现.m2文件夹。 默认情况下,该文件夹下放置了Maven本地仓库.m2/repository。 所有的Maven构件都被存储到该仓库中,以方便重用。可以到~/.m2/reposi-tory/org/apache/maven/plugins/maven-help-plugins/目录下找到刚才下载的maven-help-plugin的pom文件和jar文件。 Maven根据一套规则来确定任何一个构件在仓库中的位置。 由于Maven仓库是通过简单文件系统透明地展示给Maven用户的, 有些时候可以绕过Maven直接查看或修改仓库文件,在遇到疑难问题时,这往往十分有用。

~/.m2目录下除了repository仓库之外就没有其他目录和文件了, 不过大多数Maven用户需要复制M2_HOME/conf/settings.xml文件到~/.m2/settings.xml。 这是一条最佳实践

设置HTTP代理

有时候你所在的公司基于安全因素考虑,要求你使用通过安全认证的代理访问因特网。 这种情况下,就需要为Maven配置HTTP代理,才能让它正常访问外部仓库,以下载所需要的资源。

首先确认自己无法直接访问公共的Maven中央仓库,直接运行命令ping repo1.maven.org可以检查网络。 如果真的需要代理,先检查一下代理服务器是否畅通。 比如现在有一个IP地址为218.14.227.197,端口为3128的代理服务,我们可以运行telnet 218.14.227.197 3128来检测该地址的该端口是否畅通

代理配置

编辑~/.m2/settings.xml文件 (如果没有该文件,则复制$M2_HOME/conf/settings.xml)。 添加代理配置如下:

<settings>

……

<proxies>

<proxy>

<id>my-proxy</id>

<active>true</active>

<protocol>http</protocol>

<host>218.14.227.197</host>

<port>3128</port>

<!——

<username>***</username>

<password>***</password>

<nonProxyHosts>repository.mycom.com|*.google.com</nonProxyHosts>

——>

</proxy>

</proxies>

……

</settings>

WangShuXian6 avatar Aug 28 '21 07:08 WangShuXian6

Maven使用入门

编写POM

https://maven.apache.org/guides/introduction/introduction-to-the-pom.html Maven项目的核心是pom.xml。 POM(Project Object Model,项目对象模型)定义了项目的基本信息,用于描述项目如何构建,声明项目依赖

为Hello World项目编写一个最简单的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/maven-v4_0_0.xsd">

    <modelVersion>4.0.0</modelVersion>
    <groupId>com.xxx.book</groupId>
    <artifactId>hello-world</artifactId>
    <version>1.0-SNAPSHOT</version>
    <name>Maven Hello World Project</name>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>${maven.compiler.source}</maven.compiler.target>
    </properties>

</project>
XML头

代码的第一行是XML头,指定了该xml文档的版本和编码方式。

project

紧接着是project元素,project是所有pom.xml的根元素,它还声明了一些POM相关的命名空间及xsd元素,虽然这些属性不是必须的,但使用这些属性能够让第三方工具(如IDE中的XML编辑器)帮助我们快速编辑POM。

modelVersion

根元素下的第一个子元素modelVersion指定了当前POM模型的版本,对于Maven 2及Maven 3来说,它只能是4.0.0。

这段代码中最重要的是包含groupId、artifactId和version的三行。 这三个元素定义了一个项目基本的坐标,在Maven的世界,任何的jar、pom或者war都是以基于这些基本的坐标进行区分的。

groupId

groupId定义了项目属于哪个组,这个组往往和项目所在的组织或公司存在关联。 譬如在googlecode上建立了一个名为myapp的项目,那么groupId就应该是com.googlecode.myapp, 如果你的公司是mycom,有一个项目为myapp,那么groupId就应该是com.mycom.myapp。

artifactId

artifactId定义了当前Maven项目在组中唯一的ID,我们为这个Hello World项目定义artifactId为hello-world,

而在前面的groupId为com.googlecode.myapp的例子中,你可能会为不同的子项目(模块)分配artifactId,如myapp-util、myapp-domain、myapp-web等。

version

version指定了Hello World项目当前的版本——1.0-SNAPSHOT。 SNAPSHOT意为快照,说明该项目还处于开发中,是不稳定的版本。 随着项目的发展,version会不断更新,如升级为1.0、1.1-SNAPSHOT、1.1、2.0等。

name

name元素声明了一个对于用户更为友好的项目名称,虽然这不是必须的,但还是推荐为每个POM声明name,以方便信息交流。

没有任何实际的Java代码,我们就能够定义一个Maven项目的POM,这体现了Maven的一大优点,它能让项目对象模型最大程度地与实际代码相独立,我们可以称之为解耦,或者正交性。 这在很大程度上避免了Java代码和POM代码的相互影响。 比如当项目需要升级版本时,只需要修改POM,而不需要更改Java代码; 而在POM稳定之后,日常的Java代码开发工作基本不涉及POM的修改。


编写主代码

项目主代码和测试代码不同,项目的主代码会被打包到最终的构件中(如jar), 而测试代码只在运行测试时用到,不会被打包。 默认情况下,Maven假设项目主代码位于src/main/java目录, 遵循Maven的约定,创建该目录,然后在该目录下创建文件com/xxx/book/helloworld/HelloWorld.java

src/main/java/com/xxx/book/helloworld/HelloWorld.java

package com.xxx.book.helloworld;

public class HelloWorld

{
    public String sayHello() {
        return "Hello Maven";
    }

    public static void main(String[] args) {
        System.out.print(new HelloWorld().sayHello());
    }
}

这是一个简单的Java类, 它有一个sayHello()方法,返回一个String。 同时这个类还带有一个main方法,创建一个HelloWorld实例, 调用sayHello()方法,并将结果输出到控制台。

关于该Java代码有两点需要注意。

首先,在绝大多数情况下,应该把项目主代码放到src/main/java/目录下(遵循Maven的约定),而无须额外的配置,Maven会自动搜寻该目录找到项目主代码。

其次,该Java类的包名是com.xxx.book.helloworld,这与之前在POM中定义的groupId和artifactId相吻合。 一般来说,项目中Java类的包都应该基于项目的groupId和artifactId,这样更加清晰,更加符合逻辑,也方便搜索构件或者Java类。

编译 mvn clean compile

代码编写完毕后,使用Maven进行编译,在项目根目录下运行命令mvn clean compile

clean告诉Maven清理输出目录target/, compile告诉Maven编译项目主代码, 从输出中看到Maven首先执行了clean:clean任务,删除target/目录。 默认情况下,Maven构建的所有输出都在target/目录中; 接着执行resources:resources任务(未定义项目资源,暂且略过); 最后执行compiler:compile任务,将项目主代码编译至target/classes目录(编译好的类为com/xxx/book/helloworld/HelloWorld.Class)。

上文提到的clean:cleanresources:resourcescompiler:compile对应了一些Maven插件及插件目标, 比如clean:clean是clean插件的clean目标, compiler:compile是compiler插件的compile目标

至此,Maven在没有任何额外的配置的情况下就执行了项目的清理和编译任务。

编写测试代码

为了使项目结构保持清晰,主代码与测试代码应该分别位于独立的目录中

Maven项目中默认的主代码目录是src/main/java, 对应地,Maven项目中默认的测试代码目录是src/test/java

JUnit

在Java世界中,由Kent Beck和Erich Gamma建立的JUnit是事实上的单元测试标准。 要使用JUnit,首先需要为Hello World项目添加一个JUnit依赖,修改项目的POM https://junit.org/junit5/ https://doczhcn.gitbook.io/junit5/

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.xxx.book</groupId>
    <artifactId>hello-world</artifactId>
    <version>1.0-SNAPSHOT</version>
    <name>Maven Hello World Project</name>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>${maven.compiler.source}</maven.compiler.target>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.junit</groupId>
                <artifactId>junit-bom</artifactId>
                <version>5.7.2</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
            </plugin>
            <plugin>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.22.2</version>
            </plugin>
        </plugins>
    </build>

</project>

代码中添加了dependencies元素,该元素下可以包含多个dependency元素以声明项目的依赖。 这里添加了一个依赖——groupId是junit,artifactId是junit。

groupId、artifactId和version是任何一个Maven项目最基本的坐标,JUnit也不例外, 有了这段声明,Maven就能够自动下载junit。

Maven从哪里下载这个jar呢?在Maven之前,可以去JUnit的官方网站下载分发包,有了Maven,它会自动访问中央仓库(http://repo1.maven.org/maven2/),下载需要的文件。

可以自己访问该仓库,打开路径junit/junit/4.7/,就能看到junit-4.7.pomjunit-4.7.jar。后续会详细介绍Maven仓库及中央仓库。

上述POM代码中还有一个值为test的元素scope,scope为依赖范围,若依赖范围为test则表示该依赖只对测试有效。 换句话说,测试代码中的import JUnit代码是没有问题的,但是如果在主代码中用import JUnit代码,就会造成编译错误。 如果不声明依赖范围,那么默认值就是compile,表示该依赖对主代码和测试代码都有效。

由于历史原因,Maven的核心插件之一——compiler插件默认只支持编译Java 1.3,因此需要配置该插件使其支持Java 5以及以上

配置了测试依赖,接着就可以编写测试类。

回顾一下前面的HelloWorld类,现在要测试该类的sayHello()方法,检查其返回值是否为“Hello Maven”。 在src/test/java/com/xxx/book/helloworld/HelloWorldTest.java目录下创建文件HelloWorldTest.java

src/test/java/com/xxx/book/helloworld/HelloWorldTest.java

package com.xxx.book.helloworld;

import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;

public class HelloWorldTest {

    @Test
    public void testSayHello() {

        HelloWorld helloWorld = new HelloWorld();

        String result = helloWorld.sayHello();

        assertEquals("Hello Maven", result);
    }
}

一个典型的单元测试包含三个步骤:

①准备测试类及数据; ②执行要测试的行为; ③检查结果。

上述样例首先初始化了一个要测试的HelloWorld实例,接着执行该实例的sayHello()方法并保存结果到result变量中,最后使用JUnit框架的Assert类检查结果是否为我们期望的“Hello Maven”。

在JUnit 3中,约定所有需要执行测试的方法都以test开头,这里使用了JUnit 5,但仍然遵循这一约定。 在JUnit 5中,需要执行的测试方法都应该以@Test进行标注。

测试用例编写完毕之后就可以调用Maven执行测试。 运行mvn clean test

[INFO] Scanning for projects...
[INFO] 
[INFO] ----------------------< com.xxx.book:hello-world >----------------------
[INFO] Building Maven Hello World Project 1.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO] 
[INFO] --- maven-clean-plugin:2.5:clean (default-clean) @ hello-world ---
[INFO] Deleting /Volumes/data/all_file/test/maven-hello/target
[INFO] 
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ hello-world ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory /Volumes/data/all_file/test/maven-hello/src/main/resources
[INFO] 
[INFO] --- maven-compiler-plugin:3.8.1:compile (default-compile) @ hello-world ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 1 source file to /Volumes/data/all_file/test/maven-hello/target/classes
[INFO] 
[INFO] --- maven-resources-plugin:2.6:testResources (default-testResources) @ hello-world ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory /Volumes/data/all_file/test/maven-hello/src/test/resources
[INFO] 
[INFO] --- maven-compiler-plugin:3.8.1:testCompile (default-testCompile) @ hello-world ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 1 source file to /Volumes/data/all_file/test/maven-hello/target/test-classes
[INFO] 
[INFO] --- maven-surefire-plugin:2.22.2:test (default-test) @ hello-world ---
[INFO] 
[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.xxx.book.helloworld.HelloWorldTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.038 s - in com.xxx.book.helloworld.HelloWorldTest
[INFO] 
[INFO] Results:
[INFO] 
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO] 
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  3.634 s
[INFO] Finished at: 2021-08-28T20:04:34+08:00
[INFO] ------------------------------------------------------------------------

命令行输入的是mvn clean test,而Maven实际执行的可不止这两个任务,还有clean:cleanresources:resourcescompiler:compileresources:testResources以及compiler:testCompile

暂时需要了解的是,在Maven执行测试(test)之前,它会先自动执行项目主资源处理、主代码编译、测试资源处理、测试代码编译等工作,这是Maven生命周期的一个特性

从输出中还看到:Maven从中央仓库下载了junit pom和junit .jar 等文件到本地仓库(~/.m2/repository)中,供所有Maven项目使用

测试代码通过编译之后在target/test-classes下生成了二进制文件, 紧接着surefire:test任务运行测试,surefire是Maven中负责执行测试的插件,这里它运行测试用例HelloWorldTest,并且输出测试报告,显示一共运行了多少测试,失败了多少,出错了多少,跳过了多少。 显然,我们的测试通过了。


打包和运行 mvn clean package

将项目进行编译、测试之后,下一个重要步骤就是打包(package)。 Hello World的POM中没有指定打包类型,使用默认打包类型jar。 简单地执行命令mvn clean package进行打包,可以看到如下输出:

[INFO] Scanning for projects...
[INFO] 
[INFO] ----------------------< com.xxx.book:hello-world >----------------------
[INFO] Building Maven Hello World Project 1.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO] 
[INFO] --- maven-clean-plugin:2.5:clean (default-clean) @ hello-world ---
[INFO] Deleting /Volumes/data/all_file/test/maven-hello/target
[INFO] 
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ hello-world ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory /Volumes/data/all_file/test/maven-hello/src/main/resources
[INFO] 
[INFO] --- maven-compiler-plugin:3.8.1:compile (default-compile) @ hello-world ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 1 source file to /Volumes/data/all_file/test/maven-hello/target/classes
[INFO] 
[INFO] --- maven-resources-plugin:2.6:testResources (default-testResources) @ hello-world ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory /Volumes/data/all_file/test/maven-hello/src/test/resources
[INFO] 
[INFO] --- maven-compiler-plugin:3.8.1:testCompile (default-testCompile) @ hello-world ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 1 source file to /Volumes/data/all_file/test/maven-hello/target/test-classes
[INFO] 
[INFO] --- maven-surefire-plugin:2.22.2:test (default-test) @ hello-world ---
[INFO] 
[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.xxx.book.helloworld.HelloWorldTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.043 s - in com.xxx.book.helloworld.HelloWorldTest
[INFO] 
[INFO] Results:
[INFO] 
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO] 
[INFO] 
[INFO] --- maven-jar-plugin:2.4:jar (default-jar) @ hello-world ---
[INFO] Building jar: /Volumes/data/all_file/test/maven-hello/target/hello-world-1.0-SNAPSHOT.jar
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  3.026 s
[INFO] Finished at: 2021-08-28T20:05:43+08:00
[INFO] ------------------------------------------------------------------------

类似地,Maven会在打包之前执行编译、测试等操作。

这里看到jar:jar任务负责打包,实际上就是jar插件的jar目标将项目主代码打包成一个名为hello-world-1.0-SNAP-SHOT.jar的文件。 该文件也位于target/输出目录中,它是根据artifact-version.jar规则进行命名的,

如有需要,还可以使用finalName来自定义该文件的名称

至此,我们得到了项目的输出,如果有需要的话,就可以复制这个jar文件到其他项目的Classpath中从而使用HelloWorld类。

将包安装到本地仓库 mvn clean install

但是,如何才能让其他的Maven项目直接引用这个jar呢? 还需要一个安装的步骤,执行mvn clean install: 输出

[INFO] Scanning for projects...
[INFO] 
[INFO] ----------------------< com.xxx.book:hello-world >----------------------
[INFO] Building Maven Hello World Project 1.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO] 
[INFO] --- maven-clean-plugin:2.5:clean (default-clean) @ hello-world ---
[INFO] Deleting /Volumes/data/all_file/test/maven-hello/target
[INFO] 
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ hello-world ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory /Volumes/data/all_file/test/maven-hello/src/main/resources
[INFO] 
[INFO] --- maven-compiler-plugin:3.8.1:compile (default-compile) @ hello-world ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 1 source file to /Volumes/data/all_file/test/maven-hello/target/classes
[INFO] 
[INFO] --- maven-resources-plugin:2.6:testResources (default-testResources) @ hello-world ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory /Volumes/data/all_file/test/maven-hello/src/test/resources
[INFO] 
[INFO] --- maven-compiler-plugin:3.8.1:testCompile (default-testCompile) @ hello-world ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 1 source file to /Volumes/data/all_file/test/maven-hello/target/test-classes
[INFO] 
[INFO] --- maven-surefire-plugin:2.22.2:test (default-test) @ hello-world ---
[INFO] 
[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.xxx.book.helloworld.HelloWorldTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.042 s - in com.xxx.book.helloworld.HelloWorldTest
[INFO] 
[INFO] Results:
[INFO] 
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO] 
[INFO] 
[INFO] --- maven-jar-plugin:2.4:jar (default-jar) @ hello-world ---
[INFO] Building jar: /Volumes/data/all_file/test/maven-hello/target/hello-world-1.0-SNAPSHOT.jar
[INFO] 
[INFO] --- maven-install-plugin:2.4:install (default-install) @ hello-world ---
[INFO] Installing /Volumes/data/all_file/test/maven-hello/target/hello-world-1.0-SNAPSHOT.jar to /Users/wangshuxian/.m2/repository/com/xxx/book/hello-world/1.0-SNAPSHOT/hello-world-1.0-SNAPSHOT.jar
[INFO] Installing /Volumes/data/all_file/test/maven-hello/pom.xml to /Users/wangshuxian/.m2/repository/com/xxx/book/hello-world/1.0-SNAPSHOT/hello-world-1.0-SNAPSHOT.pom
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  3.215 s
[INFO] Finished at: 2021-08-28T20:06:25+08:00
[INFO] ------------------------------------------------------------------------

在打包之后,又执行了安装任务install:install。 从输出可以看到该任务将项目输出的jar安装到了Maven本地仓库中,可以打开相应的文件夹看到Hello World项目的pom和jar。 之前讲述JUnit的POM及jar的下载的时候,我们说只有构件被下载到本地仓库后,才能由所有Maven项目使用, 这里是同样的道理,只有将Hello World的构件安装到本地仓库之后,其他Maven项目才能使用它。

Maven最主要的命令

mvn clean compilemvn clean testmvn clean packagemvn clean install

执行test之前是会先执行compile的, 执行package之前是会先执行test的, install之前会执行package。

可以在任何一个Maven项目中执行这些命令

运行Hello World项目 java -jar xxx.jar

到目前为止,还没有运行Hello World项目,不要忘了HelloWorld类可是有一个main方法的。 默认打包生成的jar是不能够直接运行的,因为带有main方法的类信息不会添加到manifest中(打开jar文件中的META-INF/MANIFEST.MF文件,将无法看到Main-Class一行)。 为了生成可执行的jar文件,需要借助maven-shade-plugin,配置该插件如下: http://maven.apache.org/plugins/maven-shade-plugin/ https://maven.apache.org/plugins/maven-shade-plugin/examples/executable-jar.html

<?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.xxx.book</groupId>
    <artifactId>hello-world</artifactId>
    <version>1.0-SNAPSHOT</version>
    <name>Maven Hello World Project</name>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>${maven.compiler.source}</maven.compiler.target>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.junit</groupId>
                <artifactId>junit-bom</artifactId>
                <version>5.7.2</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-shade-plugin</artifactId>
            <version>3.2.4</version>
            <type>maven-plugin</type>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
            </plugin>
            <plugin>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.22.2</version>
            </plugin>


            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>3.2.4</version>
                <configuration>
                    <filters>
                        <filter>
                            <artifact>*:*</artifact>
                            <excludes>
                                <exclude>module-info.class</exclude>
                                <exclude>META-INF/*.SF</exclude>
                                <exclude>META-INF/*.DSA</exclude>
                                <exclude>META-INF/*.RSA</exclude>
                                <exclude>META-INF/**</exclude>
                            </excludes>
                        </filter>
                    </filters>
                </configuration>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <transformers>
                                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                    <mainClass>com.xxx.book.helloworld.HelloWorld</mainClass>
                                </transformer>
                            </transformers>
                        </configuration>
                    </execution>
                </executions>
            </plugin>

        </plugins>
    </build>

</project>

maven-shade-plugin部分为 project>dependencies>层级下

 <dependency>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-shade-plugin</artifactId>
            <version>3.2.4</version>
            <type>maven-plugin</type>
 </dependency>

project>build>plugins>层级下

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>3.2.4</version>
                <configuration>
                    <filters>
                        <filter>
                            <artifact>*:*</artifact>
                            <excludes>
                                <exclude>module-info.class</exclude>
                                <exclude>META-INF/*.SF</exclude>
                                <exclude>META-INF/*.DSA</exclude>
                                <exclude>META-INF/*.RSA</exclude>
                                <exclude>META-INF/**</exclude>
                            </excludes>
                        </filter>
                    </filters>
                </configuration>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <transformers>
                                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                    <mainClass>com.xxx.book.helloworld.HelloWorld</mainClass>
                                </transformer>
                            </transformers>
                        </configuration>
                    </execution>
                </executions>
            </plugin>

plugin元素在POM中的相对位置应该在<project><build><plugins>下面。 我们配置了mainClass为com.xxx.book.helloworld.HelloWorld,项目在打包时会将该信息放到MANIFEST中。 现在执行mvn clean install,待构建完成之后打开target/目录,可以看到hel-lo-world-1.0-SNAPSHOT.jaroriginal-hello-world-1.0-SNAPSHOT.jar, 前者是带有Main-Class信息的可运行jar,后者是原始的jar, 打开hello-world-1.0-SNAPSHOT.jarMETA-INF/MANIFEST.MF,可以看到它包含这样一行信息:

Main-Class:com.xxx.book.helloworld.HelloWorld

现在,在项目根目录中执行该jar文件:

java -jar target/hello-world-1.0-SNAPSHOT.jar

控制台输出为Hello Maven,这正是我们所期望的。

Hello Maven

截屏2021-08-28 下午8 41 53


使用 Archetype 生成项目骨架 maven-archetype-plugin

https://maven.apache.org/archetype/maven-archetype-plugin/ Hello World项目中有一些Maven的约定: 在项目的根目录中放置pom.xml, 在src/main/java目录中放置项目的主代码, 在src/test/java中放置项目的测试代码。

之所以一步一步地展示这些步骤,是为了能让可能是Maven初学者的你得到最实际的感受。 我们称这些基本的目录结构和pom.xml文件内容称为项目的骨架, 当第一次创建项目骨架的时候,你还会饶有兴趣地去体会这些默认约定背后的思想, 第二次,第三次,你也许还会满意自己的熟练程度, 但第四、第五次做同样的事情,你可能就会恼火了。

为此Maven提供了Archetype 以帮助我们快速勾勒出项目骨架。

以Hello World为例,我们使用maven archetype来创建该项目的骨架,离开当前的Maven项目目录。

如果是Maven 3,简单地运行:

mvn archetype:generate

如果是Maven 2,最好运行如下命令:

mvn org.apache.maven.plugins:maven-archetype-plugin:3.2.0:generate

很多资料会让你直接使用更为简单的mvn archetype:generate命令, 但在Maven 2中这是不安全的,因为该命令没有指定Archetype插件的版本,于是Maven会自动去下载最新的版本,进而可能得到不稳定的SNAPSHOT版本,导致运行失败。 然而在Maven 3中,即使用户没有指定版本,Maven也只会解析最新的稳定版本,因此这是安全的

我们实际上是在运行插件maven-archetype-plugin,注意冒号的分隔,其格式为groupId:artifactId:version:goal, org.apache.maven.plugins是maven官方插件的groupId, maven-archetype-plugin是Archetype插件的artifactId, 3.2.0是目前该插件最新的稳定版, generate是要使用的插件目标。

紧接着会看到一段长长的输出,有很多可用的Archetype供选择, 包括著名的Appfuse项目的Archetype、JPA项目的Archetype等。 每一个Archetype前面都会对应有一个编号,同时命令行会提示一个默认的编号,其对应的Archetype为maven-archetype-quickstart,直接回车以选择该Archetype, 紧接着Maven会提示输入要创建项目的groupIdartifactIdversion以及包名package。 如下输入并确认:

Define value for property 'groupId': com.xxx.book
Define value for property 'artifactId': hello-world
Define value for property 'version' 1.0-SNAPSHOT: : 
Define value for property 'package' com.xxx.book: : 
Confirm properties configuration:
groupId: com.xxx.book
artifactId: hello-world
version: 1.0-SNAPSHOT
package: com.xxx.book
 Y: : y

versionpackage可以直接回车使用默认值

Archetype插件将根据我们提供的信息创建项目骨架。 在当前目录下,Archetype插件会创建一个名为hello-world(我们定义的artifactId)的子目录,

从中可以看到项目的基本结构: 基本的pom.xml已经被创建,里面包含了必要的信息以及一个junit依赖; 主代码目录src/main/java已经被创建,在该目录下还有一个Java类com.juvenxu.mvnbook.helloworld.App, 注意这里使用到了刚才定义的包名,而这个类也仅仅只有一个简单的输出Hello World!的main方法; 测试代码目录src/test/java也被创建好了,并且包含了一个测试用例com.juvenxu.mvnbook.helloworld.AppTest

Archetype可以帮助我们迅速地构建起项目的骨架,在前面的例子中,我们完全可以在Archetype生成的骨架的基础上开发Hello World项目以节省大量时间。

此外,这里仅仅是看到了一个最简单的Archetype, 如果有很多项目拥有类似的自定义项目结构以及配置文件,则完全可以一劳永逸地开发自己的Archetype, 然后在这些项目中使用自定义的Archetype来快速生成项目骨架。

自动生成的 pom.xml maven.compiler.source,maven.compiler.target默认为1.7,已改为1.8

<?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.xxx.book</groupId>
  <artifactId>hello-world</artifactId>
  <version>1.0-SNAPSHOT</version>

  <name>hello-world</name>
  <!-- FIXME change it to the project's website -->
  <url>http://www.example.com</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
  </properties>

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <build>
    <pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
      <plugins>
        <!-- clean lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#clean_Lifecycle -->
        <plugin>
          <artifactId>maven-clean-plugin</artifactId>
          <version>3.1.0</version>
        </plugin>
        <!-- default lifecycle, jar packaging: see https://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_jar_packaging -->
        <plugin>
          <artifactId>maven-resources-plugin</artifactId>
          <version>3.0.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-compiler-plugin</artifactId>
          <version>3.8.0</version>
        </plugin>
        <plugin>
          <artifactId>maven-surefire-plugin</artifactId>
          <version>2.22.1</version>
        </plugin>
        <plugin>
          <artifactId>maven-jar-plugin</artifactId>
          <version>3.0.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-install-plugin</artifactId>
          <version>2.5.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-deploy-plugin</artifactId>
          <version>2.8.2</version>
        </plugin>
        <!-- site lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#site_Lifecycle -->
        <plugin>
          <artifactId>maven-site-plugin</artifactId>
          <version>3.7.1</version>
        </plugin>
        <plugin>
          <artifactId>maven-project-info-reports-plugin</artifactId>
          <version>3.0.0</version>
        </plugin>
      </plugins>
    </pluginManagement>
  </build>
</project>

src/main/java/com/xxx/book/App.java

package com.xxx.book;

/**
 * Hello world!
 *
 */
public class App 
{
    public static void main( String[] args )
    {
        System.out.println( "Hello World!" );
    }
}

src/test/java/com/xxx/book/AppTest.java

package com.xxx.book;

import static org.junit.Assert.assertTrue;

import org.junit.Test;

/**
 * Unit test for simple App.
 */
public class AppTest 
{
    /**
     * Rigorous Test :-)
     */
    @Test
    public void shouldAnswerWithTrue()
    {
        assertTrue( true );
    }
}

WangShuXian6 avatar Aug 28 '21 08:08 WangShuXian6

背景案例

本章是几乎所有后续章节的背景,了解了背景需求,将能够更好地理解相关Maven概念及实践的阐述。

简单的账户注册服务

注册互联网账户是日常生活中再熟悉不过的一件事情,作为一个用户,注册账户的时候往往需要做以下事情:

提供一个未被使用的账号ID

提供一个未被使用的email地址

提供一个任意的显示名称

设置安全密码,并重复输入以确认

输入验证码

前往邮箱查收激活链接并单击激活账号

登录

账号的ID和email地址都可以用来唯一地标识某个账户,而显示名称则用来显示在页面上,方便浏览。注册的时候用户还需要输入两次密码,以确保没有输错。系统则需要负责检查ID和email的唯一性,验证两次输入的密码是否一致。验证码是由系统随机生成的只能由肉眼识别其内容的图片,可以有效防止机器恶意批量注册,若输入正确的验证码信息,系统则会进行检查,如果验证码错误,系统会生成并返回新的验证码。一旦所有检查都没问题了,系统就会生成一个激活链接,并发送到用户的邮箱中。单击激活链接后,账户就被激活了,这时账户注册完成,用户可以进行登录。

对于一个账户注册服务,还需要考虑一些安全因素。 例如,需要在服务器端密文地保存密码,检查密码的强弱程度,更进一步则需要考虑验证码的失效时间,激活链接的失效时间,等等。

本章的主要目的是让读者清楚地了解这个背景案例,即账户注册服务,它的需求是什么,基于这样的一个需求,我们会怎样设计这个小型的系统。 本章的描述几乎不会涉及Maven,但后面的章节在讲述各种Maven概念和实践的时候,都会基于这一实际的背景案例。

需求阐述

了解账户注册服务之后,下面从软件工程的视角来分析一下该服务的需求。

需求用例

为了帮助读者详细地了解账户注册服务的需求,这里正式阐述一下账户注册服务的需求用例


注册账户

主要场景

1 用户访问注册页面 2 系统生成验证码图片 3 用户输入想要的ID 、Email 地址 ,想要的显示名称、密码、确认密码 4 用户输入验证码 5 用户提交注册请求 6 系统检查验证码 7 系统检查 ID 是否已经被注册  Email 是否已经被注册 密码和确认密码是否一致 8 系统保存未激活的账户信息 9 系统生成激活链接, 并发送至用户邮箱 10 用户打开邮箱,访问激活链接 11 系统解析激活链接, 激活相关账户 12 用户使用1ID和密码登录

扩展场景
4a 用户无法看清验证码,请求重新生成

1  跳转到步骤 2

6a  系统检测到用户术入的验证码错误

1 系统提示验证码错误 2.  转到步骤 2

7a 系统检测到ID 已被注册 或 Email 已被注册 或 密码和确认密码不一致

1 系统提示相关错误信息 2.  跳转到步骤 2


该注册账户用例包含了一个主要场景和几个扩展场景。

该用例的角色只有两个:用户和系统。 “主要场景”描述了用户如何与系统一步一步地交互,并且成功完成注册。 “扩展场景”则描述了一些中途发生意外的情形, 比如用户输错验证码的时候,系统就需要重新生成验证码,用户也需要重新输入验证码。

该用例没有涉及非功能性需求(如安全性),也没有详细定义用户界面,用例也不会告诉我们使用何种技术。

关于该服务的安全性,你将会看到一些实际的措施,但我们不会过于深入; 关于用户界面,下一小节会给出一个界面原型; 至于使用的技术,该项目会基于大家所熟知的Spring进一步开发。

界面原型

虽然根据文字描述,已经了解了用户注册服务所涉及的内容, 但图示的注册页面更加直观。 图清楚地标示了注册账户所需要填写的各个字段,还展示了一个验证码图片,旁边还有一个简单的链接用来获取新的验证码图片。 00026

简要设计

接口

详细了解了这个简单账户注册服务的需求之后,就能勾勒出该系统对外的接口。 从需求用例中可以看到,系统对外的接口包括生成验证码图片、处理注册请求、激活账户以及处理登录等。 下图描述了系统的接口。

注册账户服务系统接口 00027

首先需要解释的是generateCaptchaKey()generateCaptchaImage()方法, 对于Captcha的简单解释就是验证码。 每个Captcha都需要有一个key,根据这个key,系统才能得到对应的验证码图片以及实际值。 因此,generateCaptchaKey()会生成一个Captcha key, 使用这个key再调用generateImage()方法就能得到验证码图片。 验证码的key以及验证码图片被传送到客户端,用户通过肉眼识别再输入验证码的值,伴随着key再传送到服务器端验证,服务器端就可以通过这个key查到正确的验证码值,并与客户端传过来的值进行比对验证。

SignUpRequest包含了注册用户所需要的信息,包括ID、email、显示名称、密码、确认密码等。 这些信息伴随着Captcha keyCaptcha value构成了一个注册请求, signUp()方法接收SignUpRequest对象,进行验证, 如果验证正确,则创建一个未被激活的账户,同时在后台也需要发送一封带有激活链接的邮件。

activate()方法接收一个激活码,查找对应的账户进行激活。

账户激活之后,用户可以使用login()方法进行登录。

模块结构

定义了系统核心的接口之后,基于功能分割和方便复用的原则,再对系统进一步进行划分。 这里基于包名划分模块,这也是在Java中比较常见的做法。

也许你会觉得为如此简单的一个系统(或许根本就不该称之为系统)划分模块有点小题大做了, 有经验的程序员根本不需要多少设计就能快速完成这样的一个注册功能。 不过目的不在这个功能本身,我们需要一个像模像样的、有很多模块的系统来演示Maven很多非常酷的特性,同时,又不想引入一个拥有成千上万行代码的过于庞大的系统。 账户注册服务的模块划分如图所示。

注册账户服务包图 00028

各个模块(包)的作用:

com.juvenxu.mvnbook.account.service: 系统的核心,它封装了所有下层细节,对外暴露简单的接口。 这实际上是一个Facade模式,了解设计模式的读者应该能马上理解。

com.juvenxu.mvnbook.account.web: 顾名思义,该模块包含所有与web相关的内容, 包括可能的JSP、Servlet、web.xml 等, 它直接依赖于com.juvenxu.mvnbook.account.service模块,使用其提供的服务。

com.juvenxu.mvnbook.account.persist: 处理账户信息的持久化,包括增、删、改、查等,根据实现,可以基于数据库或者文件。

com.juvenxu.mvnbook.account.captcha: 处理验证码的key生成、图片生成以及验证等,这里需要第三方的类库来帮助实现这些功能。

com.juvenxu.mvnbook.account.email: 处理邮件服务的配置、激活邮件的编写和发送等工作。

WangShuXian6 avatar Aug 28 '21 14:08 WangShuXian6

坐标和依赖

Maven的一大功能是管理项目依赖。 为了能自动化地解析任何一个Java构件,Maven就必须将它们唯一标识,这就依赖管理的底层基础——坐标

何为Maven坐标

关于坐标(Coordinate),大家最熟悉的定义应该来自于平面几何。 在一个平面坐标系中,坐标(x,y)表示该平面上与x轴距离为y,与y轴距离为x的一点,任何一个坐标都能够唯一标识该平面中的一点。

Maven的世界中拥有数量非常巨大的构件,也就是平时用的一些jar、war等文件。 在Maven为这些构件引入坐标概念之前,我们无法使用任何一种方式来唯一标识所有这些构件

Maven定义了这样一组规则:世界上任何一个构件都可以使用Maven坐标唯一标识, Maven坐标的元素包括groupId、artifactId、version、packaging、classifier。 只要提供正确的坐标元素,Maven就能找到对应的构件。 比如说,当需要使用Java5平台上TestNG的5.8版本时,就告诉Maven:“groupId=org.testng;artifactId=testng;version=5.8;classifier=jdk15”,Maven就会从仓库中寻找相应的构件供我们使用。

Maven内置了一个中央仓库的地址( http://repo1.maven.org/maven2 ),该中央仓库包含了世界上大部分流行的开源项目构件,Maven会在需要的时候去那里下载。 https://search.maven.org/

在我们开发自己项目的时候,也需要为其定义适当的坐标,这是Maven强制要求的。 在这个基础上,其他Maven项目才能引用该项目生成的构件,见图

坐标为构件引入秩序 00029

坐标详解

Maven坐标为各种构件引入了秩序,任何一个构件都必须明确定义自己的坐标, 而一组Maven坐标是通过一些元素定义的,它们是groupId、artifactId、version、packaging、classifier。 先看一组坐标定义

<groupId>org.sonatype.nexus</groupId>

<artifactId>nexus-indexer</artifactId>

<version>2.0.0</version>

<packaging>jar</packaging>

这是nexus-indexer的坐标定义, nexus-indexer是一个对Maven仓库编纂索引并提供搜索功能的类库,它是Nexus项目的一个子模块。 上述代码片段中,其坐标分别为groupId:org.sonatype.nexusartifactId:nexus-indexerversion:2.0.0packaging:jar,没有classifier。

groupId:

定义当前Maven项目隶属的实际项目。

首先,Maven项目和实际项目不一定是一对一的关系。

比如SpringFramework这一实际项目,其对应的Maven项目会有很多,如spring-core、spring-context等。 这是由于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文件夹中找到某个项目的一组构件。 考虑有5个项目,每个项目都有一个core模块,如果没有前缀,我们会看到很多core-1.2.jar这样的文件, 加上实际项目名前缀之后,便能很容易区分foo-core-1.2.jar、bar-core-1.2.jar……

version:

该元素定义Maven项目当前所处的版本, 如上例中nexus-indexer的版本是2.0.0。 需要注意的是,Maven定义了一套完整的版本规范,以及快照(SNAPSHOT)的概念。

packaging:

该元素定义Maven项目的打包方式。

首先,打包方式通常与所生成构件的文件扩展名对应,

如上例中packaging为jar,最终的文件名为nexus-indexer-2.0.0.jar, 而使用war打包方式的Maven项目,最终生成的构件会有一个.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文档和源代码。 这时候,javadoc和sources就是这两个附属构件的classifier。 这样,附属构件也就拥有了自己唯一的坐标。 还有一个关于classifier的典型例子是TestNG, TestNG的主构件是基于Java 1.4平台的, 而它又提供了一个classifier为jdk5的附属构件。 注意,不能直接定义项目的classifier, 因为附属构件不是项目直接默认生成的,而是由附加的插件帮助生成。

5个元素

上述5个元素中,groupId、artifactId、version是必须定义的,packaging是可选的(默认为jar),而classifier是不能直接定义的。

项目构件的文件名是与坐标相对应的,

一般的规则为artifactId-version[-classifier].packaging[-classifier]表示可选。 比如上例nexus-indexer的主构件为nexus-indexer-2.0.0.jar,附属构件有nexus-indexer-2.0.0-javadoc.jar。 这里还要强调的一点是,packaging并非一定与构件扩展名对应, 比如packaging为maven-plugin的构件扩展名为jar。

Maven仓库的布局也是基于Maven坐标


account-email 模块的实现

案例中有一个email模块负责发送账户激活的电子邮件, 该模块的实现,包括POM配置、主代码和测试代码。 由于该背景案例的实现是基于Spring Framework,因此还会涉及相关的Spring配置。

account-email的POM

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.xxx.book.account</groupId>
    <artifactId>account-email</artifactId>
    <name>Account Email</name>
    <version>1.0.0-SNAPSHOT</version>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>


    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>5.3.9</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-beans</artifactId>
            <version>5.3.9</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.3.9</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context-support</artifactId>
            <version>5.3.9</version>
        </dependency>
        <dependency>
            <groupId>javax.mail</groupId>
            <artifactId>mail</artifactId>
            <version>1.4.5</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.11</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.icegreen</groupId>
            <artifactId>greenmail</artifactId>
            <version>2.0.0-alpha-1</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <build></build>
</project>
该项目模块的

groupId:com.juvenxu.mvnbook.account;artifactId:account-email;version:1.0.0-SNAPSHOT。 由于该模块属于账户注册服务项目的一部分,因此,其groupId对应了account项目。 紧接着,该模块的artifactId仍然以account作为前缀,以方便区分其他项目的构建。 最后,1.0.0-SNAPSHOT表示该版本处于开发中,还不稳定。

dependencies元素

其包含了多个dependency子元素,这是POM中定义项目依赖的位置。 以第一个依赖为例,其groupId:artifactId:version为org.springframework:spring-core:5.3.9,这便是依赖的坐标, 任何一个Maven项目都需要定义自己的坐标,当这个Maven项目成为其他Maven项目的依赖的时候,这组坐标就体现了其价值。 本例中的spring-core,以及后面的spring-beans、spring-context、spring-context-support是Spring Framework实现依赖注入等功能必要的构件

在spring-context-support之后,有一个依赖为javax.mail:mail:1.6.2,

这是实现发送必须的类库。

junit:junit:4.11,

JUnit是Java社区事实上的单元测试标准,详细信息请参阅http://www.junit.org/, 这个依赖特殊的地方在于一个值为test的scope子元素,scope用来定义依赖范围。 当依赖范围是test的时候,该依赖只会被加入到测试代码的classpath中。 也就是说,对于项目主代码,该依赖是没有任何作用的。 JUnit是单元测试框架,只有在测试的时候才需要,因此使用该依赖范围。

com.icegreen:greenmail:2.0.0-alpha-1,

其依赖范围同样为test。该依赖同样只服务于测试目的, GreenMail是开源的邮件服务测试套件,account-email模块使用该套件来测试邮件的发送。 关于GreenMail的详细信息可访问 http://www.icegreen.com/greenmail/。

POM中有一段关于maven-compiler-plugin的配置,

其目的是开启Java 8的支持

account-email的主代码

account-email项目Java主代码位于src/main/java, 资源文件(非Java)位于src/main/resources目录下。

account-email只有一个很简单的接口

src/main/java/com/xxx/book/account/email/AccountEmailService.java

package com.xxx.book.account.email;

public interface AccountEmailService {
    void sendMail(String to, String subject, String htmlText) throws AccountEmailException;
}

sendMail()方法用来发送html格式的邮件,to为接收地址,subject为邮件主题,html-Text为邮件内容, 如果发送邮件出错,则抛出AccountEmailException异常

src/main/java/com/xxx/book/account/email/AccountEmailException.java

package com.xxx.book.account.email;

public class AccountEmailException extends Exception {
    private static final long serialVersionUID = -4817386460334501672L;

    public AccountEmailException(String message) {
        super(message);
    }

    public AccountEmailException(String message, Throwable throwable) {
        super(message, throwable);
    }
}

对应于该接口的实现见代码

src/main/java/com/xxx/book/account/email/AccountEmailServiceImpl.java

package com.xxx.book.account.email;

import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;

import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;

public class AccountEmailServiceImpl implements AccountEmailService {
    private JavaMailSender javaMailSender;

    private String systemEmail;

    public void sendMail(String to, String subject, String htmlText) throws AccountEmailException {
        try {
            MimeMessage msg = javaMailSender.createMimeMessage();
            MimeMessageHelper msgHelper = new MimeMessageHelper(msg);

            msgHelper.setFrom(systemEmail);
            msgHelper.setTo(to);
            msgHelper.setSubject(subject);
            msgHelper.setText(htmlText, true);

            javaMailSender.send(msg);
        } catch (MessagingException e) {
            throw new AccountEmailException("Faild to send mail.", e);
        }
    }

    public JavaMailSender getJavaMailSender() {
        return javaMailSender;
    }

    public void setJavaMailSender(JavaMailSender javaMailSender) {
        this.javaMailSender = javaMailSender;
    }

    public String getSystemEmail() {
        return systemEmail;
    }

    public void setSystemEmail(String systemEmail) {
        this.systemEmail = systemEmail;
    }
}

首先,该AccountEmailServiceImpl类有一个私有字段javaMailSender,该字段的类型org.springframework.mail.javamail.JavaMailSender是来自于Spring Framework的帮助简化邮件发送的工具类库,对应于该字段有一组getter()和setter()方法,它们用来帮助实现依赖注入。

在sendMail()的方法实现中,首先使用javaMailSender创建一个MimeMessage,该msg对应了将要发送的邮件。 接着使用MimeMessageHelper帮助设置该邮件的发送地址、收件地址、主题以及内容, msgHelper.setText(htmlText,true)中的true表示邮件的内容为html格式。 最后,使用javaMailSender发送该邮件,如果发送出错,则捕捉MessageException异常,包装后再抛出该模块自己定义的AccountEmailException异常。

这段Java代码中没有邮件服务器配置信息,这得益于Spring Framework的依赖注入,这些配置都通过外部的配置注入到了javaMailSender中,相关配置信息都在src/main/resources/account-email.xml这个配置文件中 src/main/resources/account-email.xml`

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">

    <bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
        <property name="location" value="classpath:service.properties" />
    </bean>

    <bean id="javaMailSender" class="org.springframework.mail.javamail.JavaMailSenderImpl">
        <property name="protocol" value="${email.protocol}" />
        <property name="host" value="${email.host}" />
        <property name="port" value="${email.port}" />
        <property name="username" value="${email.username}" />
        <property name="password" value="${email.password}" />
        <property name="javaMailProperties">
            <props>
                <prop key="mail.${email.protocol}.auth">${email.auth}</prop>
            </props>
        </property>
    </bean>

    <bean id="accountEmailService" class="com.xxx.book.account.email.AccountEmailServiceImpl">
        <property name="javaMailSender" ref="javaMailSender" />
        <property name="systemEmail" value="${email.systemEmail}" />
    </bean>
</beans>

Spring Framework会使用该XML配置创建ApplicationContext,以实现依赖注入。 该配置文件定义了一些bean,基本对应了Java程序中的对象。 首先解释下id为propertyConfigurer的bean,其实现为org.springframework.beans.factory.config.PropertyPlaceholderConfigurer, 这是Spring Framework中用来帮助载入properties文件的组件。 这里定义location的值为class-path:email.properties,表示从classpath的根路径下载入名为email.properties文件中的属性。

接着定义id为javaMailSender的bean,其实现为org.springframework.mail.javamail.JavaMail-SenderImpl, 这里需要定义邮件服务器的一些配置,包括协议、端口、主机、用户名、密码,是否需要认证等属性。 这段配置还使用了Spring Framework的属性引用,比如host的值为${email.host},之前定义propertyConfigurer的作用就在于此。 这么做可以将邮件服务器相关的配置分离到外部的properties文件中, 比如可以定义这样一个properties文件,配置javaMailSender使用gmail:

email.protocol=smtps

email.host=smtp.gmail.com

email.port=465

[email protected]

email.password=your-password

email.auth=true

[email protected]

这样,javaMailSender实际使用的protocol就会成为smtps,host会成为smtp.gmail.com,同理还有port、username等其他属性。

最后一个bean是accountEmailService,对应了之前描述的com.juvenxu.mvnbook.account.

email.AccountEmailServiceImpl,配置中将另外一个bean javaMailSender注入,使其成为该类javaMailSender字段的值。

上述就是Spring Framework相关的配置


account-email的测试代码 [代码未验证通过,需修改]

测试相关的Java代码位于src/test/java目录,相关的资源文件则位于src/test/resources目录。

该模块需要测试的只有一个AccountEmailService.sendMail()接口。 为此,需要配置并启动一个测试使用的邮件服务器,然后提供对应的properties配置文件供Spring Framework载入以配置程序。 准备就绪之后,调用该接口发送邮件,然后检查邮件是否发送正确。 最后,关闭测试邮件服务器

src/test/java/com/xxx/book/account/email/AccountEmailServiceTest.java

package com.xxx.book.account.email;

//import static junit.framework.Assert.assertEquals; // 已废弃
import static org.junit.Assert.assertEquals;

import javax.mail.Message;
import javax.mail.internet.MimeMessage;

import com.icegreen.greenmail.util.GreenMailUtil;
import com.icegreen.greenmail.util.ServerSetupTest;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import com.icegreen.greenmail.util.GreenMail;
import com.icegreen.greenmail.util.GreenMailUtil;
import com.icegreen.greenmail.util.ServerSetupTest;

public class AccountEmailServiceTest {
    private GreenMail greenMail;

    @Before
    public void startMailServer() throws Exception {
        greenMail = new GreenMail(ServerSetupTest.SMTP);
        greenMail.setUser("test@localhost", "123456");
        greenMail.start();
    }

    @Test
    public void testSendMail() throws Exception {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("account-email.xml");
        AccountEmailService accountEmailService = (AccountEmailService) ctx.getBean("accountEmailService");

        String subject = "Test Subject";
        String htmlText = "<h3>Test</h3>";
        accountEmailService.sendMail("test2@localhost", subject, htmlText);

        greenMail.waitForIncomingEmail(2000, 1);

        jakarta.mail.internet.MimeMessage[] msgs = greenMail.getReceivedMessages();
        assertEquals(1, msgs.length);
        assertEquals(subject, msgs[0].getSubject());
        assertEquals(htmlText, GreenMailUtil.getBody(msgs[0]).trim());
    }

    @After
    public void stopMailServer() throws Exception {
        greenMail.stop();
    }
}

这里使用GreenMail作为测试邮件服务器,在startMailServer()中,基于SMTP协议初始化GreenMail,然后创建一个邮件账户并启动邮件服务,该服务默认会监听25端口。 如果你的机器已经有程序使用该端口,请配置自定义的ServerSetup实例使用其他端口。startMailServer()方法使用了@before标注,表示该方法会先于测试方法(@test)之前执行。

对应于startMailServer(),该测试还有一个stopMailServer()方法,标注@After表示执行测试方法之后会调用该方法,停止GreenMail的邮件服务。

代码的重点在于使用了@Test标注的testSendMail()方法,该方法首先会根据classpath路径中的account-email.xml配置创建一个Spring Framework的ApplicationContext,然后从这个ctx中获取需要测试的id为accountEmailService的bean,并转换成AccountEmailService接口,针对接口测试是一个单元测试的最佳实践。 得到了AccountEmailService之后,就能调用其sendMail()方法发送电子邮件。当然,这个时候不能忘了邮件服务器的配置,其位于src/test/resources/service.properties:

src/test/resources/service.properties

#email.protocol=smtps
#email.host=smtp.gmail.com
#email.port=465
#[email protected]
#email.password=your-password
#email.auth=true
#[email protected]

email.protocol=smtp
email.host=localhost
email.port=25
email.username=test@localhost
email.password=123456
email.auth=true
email.systemEmail=test1@localhost

这段配置与之前GreenMail的配置对应,使用了smtp协议,使用本机的25端口,并有用户名、密码等认证配置。

回到测试方法中,邮件发送完毕后,再使用GreenMail进行检查。green-Mail.waitForIncomingEmail(2000,1)表示接收一封邮件,最多等待2秒。由于GreenMail服务完全基于内存,实际情况下基本不会超过2秒。随后的几行代码读取收到的邮件,检查邮件的数目以及第一封邮件的主题和内容。

这时,可以运行mvn clean test执行测试,Maven会编译主代码和测试代码,并执行测试,报告一个测试得以正确执行,构建成功。


构建account-email

使用mvn clean install构建account-email,Maven会根据POM配置自动下载所需要的依赖构件,执行编译、测试、打包等工作,最后将项目生成的构件account-email-1.0.0-SNAP-SHOT.jar安装到本地仓库中。这时,该模块就能供其他Maven项目使用了。


依赖的配置

依赖会有基本的groupId、arti-factId和version等元素组成。 其实一个依赖声明可以包含如下的一些元素:

<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:用来排除传递性依赖


依赖范围

JUnit依赖的测试范围是test,测试范围用元素scope表示

Maven在编译项目主代码的时候需要使用一套classpath。 在上例中,编译项目主代码的时候需要用到spring-core,该文件以依赖的方式被引入到classpath中。

Maven在编译和执行测试的时候会使用另外一套classpath。 上例中的JUnit就是一个很好的例子,该文件也以依赖的方式引入到测试使用的classpath中,不同的是这里的依赖范围是test。

实际运行Maven项目的时候,又会使用一套classpath, 上例中的spring-core需要在该classpath中,而JUnit则不需要。

依赖范围就是用来控制依赖与这三种classpath(编译classpath、测试classpath、运行classpath)的关系

Maven有以下几种依赖范围:

compile:编译依赖范围。

如果没有指定,就会默认使用该依赖范围。 使用此依赖范围的Maven依赖,对于编译、测试、运行三种classpath都有效。 典型的例子是spring-core,在编译、测试和运行的时候都需要使用该依赖。

test:测试依赖范围。

使用此依赖范围的Maven依赖,只对于测试classpath有效,在编译主代码或者运行项目的使用时将无法使用此类依赖。 典型的例子是JUnit,它只有在编译测试代码及运行测试的时候才需要。

provided:已提供依赖范围。

使用此依赖范围的Maven依赖,对于编译和测试class-path有效,但在运行时无效。 典型的例子是servlet-api,编译和测试项目的时候需要该依赖,但在运行项目的时候,由于容器已经提供,就不需要Maven重复地引入一遍。

runtime:运行时依赖范围。

使用此依赖范围的Maven依赖,对于测试和运行class-path有效,但在编译主代码时无效。 典型的例子是JDBC驱动实现,项目主代码的编译只需要JDK提供的JDBC接口,只有在执行测试或者运行项目的时候才需要实现上述接口的具体JDBC驱动。

system:系统依赖范围。

该依赖与三种classpath的关系,和provided依赖范围完全一致。但是,使用system范围的依赖时必须通过systemPath元素显式地指定依赖文件的路径。 由于此类依赖不是通过Maven仓库解析的,而且往往与本机系统绑定,可能造成构建的不可移植,因此应该谨慎使用。 systemPath元素可以引用环境变量,如:

<dependency>
    <groupId>javax.sql</groupId>
    <artifactId>jdbc-stdext</artifactId>
    <version>2.0</version>
    <scope>system</scope>
    <systemPath>${java.home}/lib/rt.jar</systemPath>
</dependency>

import(Maven 2.0.9及以上):导入依赖范围。

该依赖范围不会对三种classpath产生实际的影响

上述除import以外的各种依赖范围与三种classpath的关系

00030


传递性依赖

何为传递性依赖

考虑一个基于Spring Framework的项目,如果不使用Maven,那么在项目中就需要手动下载相关依赖。由于Spring Framework又会依赖于其他开源类库,因此实际中往往会下载一个很大的如spring-framework-2.5.6-with-dependencies.zip的包,这里包含了所有Spring Framework的jar包,以及所有它依赖的其他jar包。这么做往往就引入了很多不必要的依赖。 另一种做法是只下载spring-framework-2.5.6.zip这样一个包,这里不包含其他相关依赖,到实际使用的时候,再根据出错信息,或者查询相关文档,加入需要的其他依赖。很显然,这也是一件非常麻烦的事情。

Maven的传递性依赖机制可以很好地解决这一问题。 以account-email项目为例,该项目有一个org.springframework:spring-core:2.5.6的依赖, 而实际上spring-core也有它自己的依赖,我们可以直接访问位于中央仓库的该构件的POM:http://repo1.maven.org/maven2/org/springframework/spring-core/2.5.6/spring-core-2.5.6.pom。 该文件包含了一个commons-logging依赖

spring-core的commons-logging依赖

<dependency>
    <groupId>commons-logging</groupId>
    <artifactId>commons-logging</artifactId>
    <version>1.1.1</version>
</dependency>

该依赖没有声明依赖范围,那么其依赖范围就是默认的compile。 同时回顾一下account-email,spring-core的依赖范围也是compile。

account-mail有一个compile范围的spring-core依赖, spring-core有一个compile范围的commons-logging依赖, 那么commons-logging就会成为account-email的compile范围依赖, commons-logging是account-email的一个传递性依赖

传递性依赖 00031

有了传递性依赖机制,在使用Spring Framework的时候就不用去考虑它依赖了什么,也不用担心引入多余的依赖。 Maven会解析各个直接依赖的POM,将那些必要的间接依赖,以传递性依赖的形式引入到当前的项目中。

传递性依赖和依赖范围

依赖范围不仅可以控制依赖与三种classpath的关系,还对传递性依赖产生影响。

上面的例子中,account-email对于spring-core的依赖范围是compile, spring-core对于commons-logging的依赖范围是compile,那么account-email对于commons-logging这一传递性依赖的范围也就是compile。

假设A依赖于B,B依赖于C,我们说A对于B是第一直接依赖,B对于C是第二直接依赖,A对于C是传递性依赖。 第一直接依赖的范围和第二直接依赖的范围决定了传递性依赖的范围,

如表所示,最左边一列表示第一直接依赖范围,最上面一行表示第二直接依赖范围,中间的交叉单元格则表示传递性依赖范围。 00032

再举个例子。account-email项目有一个com.icegreen:greenmail:1.3.1b的直接依赖,我们说这是第一直接依赖,其依赖范围是test; 而greenmail又有一个javax.mail:mail:1.4的直接依赖,我们说这是第二直接依赖,其依赖范围是compile。 显然javax.mail:mail:1.4是account-email的传递性依赖, 对照表可以知道,当第一直接依赖范围为test,第二直接依赖范围是compile的时候,传递性依赖的范围是test,因此javax.mail:mail:1.4是account-email的一个范围是test的传递性依赖。

仔细观察一下表,可以发现这样的规律: 当第二直接依赖的范围是compile的时候,传递性依赖的范围与第一直接依赖的范围一致; 当第二直接依赖的范围是test的时候,依赖不会得以传递; 当第二直接依赖的范围是provided的时候,只传递第一直接依赖范围也为provided的依赖,且传递性依赖的范围同样为provided; 当第二直接依赖的范围是runtime的时候,传递性依赖的范围与第一直接依赖的范围一致,但compile例外,此时传递性依赖的范围为runtime。


依赖调解

Maven引入的传递性依赖机制,一方面大大简化和方便了依赖声明,另一方面,大部分情况下我们只需要关心项目的直接依赖是什么,而不用考虑这些直接依赖会引入什么传递性依赖。 但有时候,当传递性依赖造成问题的时候,我们就需要清楚地知道该传递性依赖是从哪条依赖路径引入的。

例如,项目A有这样的依赖关系:A->B->C->X(1.0)、A->D->X(2.0),X是A的传递性依赖,但是两条依赖路径上有两个版本的X,那么哪个X会被Maven解析使用呢?两个版本都被解析显然是不对的,因为那会造成依赖重复,因此必须选择一个。

Maven依赖调解(Dependency Mediation)的第一原则是:路径最近者优先。

该例中X(1.0)的路径长度为3,而X(2.0)的路径长度为2,因此X(2.0)会被解析使用。

Maven定义了依赖调解的第二原则

依赖调解第一原则不能解决所有问题,比如这样的依赖关系:A->B->Y(1.0)、A->C->Y(2.0),Y(1.0)和Y(2.0)的依赖路径长度是一样的,都为2。那么到底谁会被解析使用呢?在Maven 2.0.8及之前的版本中,这是不确定的,

但是从Maven 2.0.9开始,为了尽可能避免构建的不确定性,Maven定义了依赖调解的第二原则:第一声明者优先。 在依赖路径长度相等的前提下,在POM中依赖声明的顺序决定了谁会被解析使用,顺序最靠前的那个依赖优胜。 该例中,如果B的依赖声明在C之前,那么Y(1.0)就会被解析使用。


可选依赖 optional

假设有这样一个依赖关系,项目A依赖于项目B,项目B依赖于项目X和Y,B对于X和Y的依赖都是可选依赖:A->B、B->X(可选)、B->Y(可选)。 根据传递性依赖的定义,如果所有这三个依赖的范围都是compile,那么X、Y就是A的compile范围传递性依赖。 然而,由于这里X、Y是可选依赖,依赖将不会得以传递。换句话说,X、Y将不会对A有任何影响,如图 00033

为什么要使用可选依赖这一特性呢? 可能项目B实现了两个特性,其中的特性一依赖于X,特性二依赖于Y,而且这两个特性是互斥的,用户不可能同时使用两个特性。 比如B是一个持久层隔离工具包,它支持多种数据库,包括MySQL、PostgreSQL等,在构建这个工具包的时候,需要这两种数据库的驱动程序,但在使用这个工具包的时候,只会依赖一种数据库。

项目B的依赖声明[可选依赖的配置]

<project>
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.juvenxu.mvnbook</groupId>
    <artifactId>project-b</artifactId>
    <version>1.0.0</version>
    <dependencies>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.10</version>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <version>8.4-701.jdbc3</version>
            <optional>true</optional>
        </dependency>
    </dependencies>
</project>

上述XML代码片段中,使用<optional>元素表示mysql-connector-java和postgresql这两个依赖为可选依赖,它们只会对当前项目B产生影响,当其他项目依赖于B的时候,这两个依赖不会被传递。

因此,当项目A依赖于项目B的时候,如果其实际使用基于MySQL数据库,那么在项目A中就需要显式地声明mysql-connector-java这一依赖 可选依赖不被传递

<project>
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.juvenxu.mvnbook</groupId>
    <artifactId>project-a</artifactId>
    <version>1.0.0</version>
    <dependencies>
        <dependency>
            <groupId>com.juvenxu.mvnbook</groupId>
            <artifactId>project-b</artifactId>
            <version>1.0.0</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.10</version>
        </dependency>
    </dependencies>
</project>

最后,关于可选依赖需要说明的一点是,在理想的情况下,是不应该使用可选依赖的。 前面我们可以看到,使用可选依赖的原因是某一个项目实现了多个特性,在面向对象设计中,有个单一职责性原则,意指一个类应该只有一项职责,而不是糅合太多的功能。 这个原则在规划Maven项目的时候也同样适用。 在上面的例子中,更好的做法是为MySQL和PostgreSQL分别创建一个Maven项目,基于同样的groupId分配不同的artifactId, 如com.juvenxu.mvnbook:project-b-mysql和com.juvenxu.mvnbook:project-b-postgresql,在各自的POM中声明对应的JDBC驱动依赖,而且不使用可选依赖,用户则根据需要选择使用project-b-mysql或者project-b-postgresql。 由于传递性依赖的作用,就不用再声明JDBC驱动依赖。


最佳实践

排除依赖

传递性依赖会给项目隐式地引入很多依赖,这极大地简化了项目依赖的管理,但是有些时候这种特性也会带来问题。

例如,当前项目有一个第三方依赖,而这个第三方依赖由于某些原因依赖了另外一个类库的SNAPSHOT版本,那么这个SNAPSHOT就会成为当前项目的传递性依赖,而SNAPSHOT的不稳定性会直接影响到当前的项目。 这时就需要排除掉该SNAPSHOT,并且在当前项目中声明该类库的某个正式发布的版本。

还有一些情况,你可能也想要替换某个传递性依赖,比如Sun JTA API,Hibernate依赖于这个JAR,但是由于版权的因素,该类库不在中央仓库中,而Apache Geronimo项目有一个对应的实现。 这时你就可以排除Sun JAT API,再声明Geronimo的JTA API实现

排除传递性依赖

<project>
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.juvenxu.mvnbook</groupId>
    <artifactId>project-a</artifactId>
    <version>1.0.0</version>
    <dependencies>
        <dependency>
            <groupId>com.juvenxu.mvnbook</groupId>
            <artifactId>project-b</artifactId>
            <version>1.0.0</version>
            <exclusions>
                <exclusion>
                    <groupId>com.juvenxu.mvnbook</groupId>
                    <artifactId>project-c</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>com.juvenxu.mvnbook</groupId>
            <artifactId>project-c</artifactId>
            <version>1.1.0</version>
        </dependency>
    </dependencies>
</project>

述代码中,项目A依赖于项目B,但是由于一些原因,不想引入传递性依赖C,而是自己显式地声明对于项目C 1.1.0版本的依赖。 代码中使用exclusions元素声明排除依赖,exclusions可以包含一个或者多个exclusion子元素,因此可以排除一个或者多个传递性依赖。

需要注意的是,声明exclusion的时候只需要groupId和artifactId,而不需要version元素,这是因为只需要groupId和artifactId就能唯一定位依赖图中的某个依赖。 换句话说,Maven解析后的依赖中,不可能出现groupId和artifactId相同,但是version不同的两个依赖,这一点在5.6节中已做过解释。该例的依赖解析逻辑如图

排除依赖 00034

归类依赖 properties

有很多关于Spring Framework的依赖,它们分别是org.springframework:spring-core:2.5.6、org.springframework:spring-beans:2.5.6、org.springframework:spring-context:2.5.6和org.springframework:spring-context-support:2.5.6,它们是来自同一项目的不同模块。 因此,所有这些依赖的版本都是相同的,而且可以预见,如果将来需要升级Spring Frame-work,这些依赖的版本会一起升级。

对于account-email中这些Spring Framework来说,也应该在一个唯一的地方定义版本,并且在dependency声明中引用这一版本。 这样,在升级Spring Framework的时候就只需要修改一处

使用Maven属性归类依赖

<project>
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.juven.mvnbook.account</groupId>
    <artifactId>account-email</artifactId>
    <name>Account Email</name>
    <version>1.0.0-SNAPSHOT</version>
    <properties>
        <springframework.version>2.5.6</springframework.version>
    </properties>
    <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>
    </dependencies>
</project>

这里简单用到了Maven属性,首先使用properties元素定义Maven属性,该例中定义了一个springframework.version子元素,其值为2.5.6。 有了这个属性定义之后,Maven运行的时候会将POM中的所有的${springframework.version}替换成实际值2.5.6。 也就是说,可以使用美元符号和大括弧环绕的方式引用Maven属性。 然后,将所有Spring Framework依赖的版本值用这一属性引用表示。 这和在Java中用常量PI替换3.14是同样的道理,不同的只是语法

优化依赖

应该能够对Maven项目的依赖了然于胸,并对其进行优化,如去除多余的依赖,显式地声明某些必要的依赖。

Maven会自动解析所有项目的直接依赖和传递性依赖,并且根据规则正确判断每个依赖的范围,对于一些依赖冲突,也能进行调节,以确保任何一个构件只有唯一的版本在依赖中存在。

在这些工作之后,最后得到的那些依赖被称为已解析依赖(Resolved Dependency)。

查看当前项目的已解析依赖:

mvn dependency:list

每个依赖的范围也得以明确标示

在此基础上,还能进一步了解已解析依赖的信息。 将直接在当前项目POM声明的依赖定义为顶层依赖,而这些顶层依赖的依赖则定义为第二层依赖,以此类推,有第三、第四层依赖。 当这些依赖经Maven解析后,就会构成一个依赖树,通过这棵依赖树就能很清楚地看到某个依赖是通过哪条传递路径引入的。

查看当前项目的依赖树:

mvn dependency:tree

使用dependency:list和dependency:tree可以帮助我们详细了解项目中所有依赖的具体信息,在此基础上,还有dependency:analyze工具可以帮助分析当前项目的依赖。

分析依赖树

为了说明该工具的用途,先将spring-context这一依赖删除,然后构建项目,你会发现编译、测试和打包都不会有任何问题。 通过分析依赖树,可以看到spring-context是spring-context-support的依赖,因此会得以传递到项目的classspath中。 现在再运行如下命令:

mvn dependency:analyze
显式声明任何项目中直接用到的依赖

该结果中重要的是两个部分。 首先是Used undeclared dependencies,意指项目中使用到的,但是没有显式声明的依赖, 这里是spring-context。 这种依赖意味着潜在的风险,当前项目直接在使用它们,例如有很多相关的Java import声明, 而这种依赖是通过直接依赖传递进来的,当升级直接依赖的时候,相关传递性依赖的版本也可能发生变化,这种变化不易察觉,但是有可能导致当前项目出错。 例如由于接口的改变,当前项目中的相关代码无法编译。 这种隐藏的、潜在的威胁一旦出现,就往往需要耗费大量的时间来查明真相。 因此,显式声明任何项目中直接用到的依赖。

结果中还有一个重要的部分是Unused declared dependencies,意指项目中未使用的,但显式声明的依赖, 这里有spring-core和spring-beans。需要注意的是,对于这样一类依赖,我们不应该简单地直接删除其声明,而是应该仔细分析。 由于dependency:analyze只会分析编译主代码和测试代码需要用到的依赖,一些执行测试和运行时需要的依赖它就发现不了。 很显然,该例中的spring-core和spring-beans是运行Spring Framework项目必要的类库,因此不应该删除依赖声明。 当然,有时候确实能通过该信息找到一些没用的依赖,但一定要小心测试。

WangShuXian6 avatar Aug 28 '21 14:08 WangShuXian6

仓库

坐标和依赖是任何一个构件在Maven世界中的逻辑表示方式; 而构件的物理表示方式是文件,Maven通过仓库来统一管理这些文件

何为Maven仓库

在Maven世界中,任何一个依赖、插件或者项目构建的输出,都可以称为构件。

例如,依赖log4j-1.2.15.jar是一个构件,插件maven-compiler-plugin-2.0.2.jar是一个构件,account-email项目构建完成后的输出account-email-1.0.0-SNAPSHOT.jar也是一个构件。

任何一个构件都有一组坐标唯一标识。

在一台工作站上,可能会有几十个Maven项目,所有项目都使用maven-compiler-plugin,这些项目中的大部分都用到了log4j,有一小部分用到了Spring Framework,还有另外一小部分用到了Struts2。 在每个有需要的项目中都放置一份重复的log4j或者struts2显然不是最好的解决方案,这样做不仅造成了磁盘空间的浪费,而且也难于统一管理,文件的复制等操作也会降低构建的速度。 而实际情况是,在不使用Maven的那些项目中,我们往往就能发现命名为lib/的目录,各个项目lib/目录下的内容存在大量的重复。

得益于坐标机制,任何Maven项目使用任何一个构件的方式都是完全相同的。 在此基础上,Maven可以在某个位置统一存储所有Maven项目共享的构件,这个统一的位置就是仓库。 实际的Maven项目将不再各自存储其依赖文件,它们只需要声明这些依赖的坐标,在需要的时候(例如,编译项目的时候需要将依赖加入到classpath中),Maven会自动根据坐标找到仓库中的构件,并使用它们。

为了实现重用,项目构建完毕后生成的构件也可以安装或者部署到仓库中,供其他项目使用。

仓库的布局

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

例如,log4j:log4j:1.2.15这一依赖,其对应的仓库路径为log4j/log4j/1.2.15/log4j-1.2.15.jar 该路径与坐标的大致对应关系为groupId/artifactId/version/artifactId-version.packaging

Maven处理仓库布局的源码

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_SEPA-RATOR);

    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()方法的目的是根据构件信息生成其在仓库中的路径

这里根据一个实际的例子来分析路径的生成,考虑这样一个构件: groupId=org.testng、artifactId=testng、version=5.8、classifier=jdk15、packaging=jar, 其对应的路径按如下步骤生成:

1)基于构件的groupId准备路径,formatAsDirectory()将groupId中的句点分隔符转换成路径分隔符。该例中,groupId org.testng就会被转换成org/testng,之后再加一个路径分隔符斜杠,那么,org.testng就成为了org/testng/。

2)基于构件的artifactId准备路径,也就是在前面的基础上加上artifactId以及一个路径分隔符。该例中的artifactId为testng,那么,在这一步过后,路径就成为了org/testng/testng/。

3)使用版本信息。在前面的基础上加上version和路径分隔符。该例中版本是5.8,那么路径就成为了org/testng/tesgng/5.8/。

4)依次加上artifactId,构件分隔符连字号,以及version,于是构建的路径就变成了org/testng/testng/5.8/testng-5.8。读者可能会注意到,这里使用了artifactId.getVersion(),而上一步用的是artifactId.getBaseVersion(),baseVersion主要是为SNAPSHOT版本服务的,例如version为1.0-SNAPSHOT的构件,其baseVersion就是1.0。

5)如果构件有classifier,就加上构件分隔符和classifier。该例中构件的classifier是jdk15,那么路径就变成org/testng/testng/5.8/testng-5.8-jdk5。

6)检查构件的extension,若extension存在,则加上句点分隔符和extension。从代码中可以看到,extension是从artifactHandler而非artifact获取,artifactHandler是由项目的packaging决定的。因此,可以说,packaging决定了构件的扩展名,该例的packaging是jar,因此最终的路径为org/testng/testng/5.8/testng-5.8-jdk5.jar。

Maven仓库是基于简单文件系统存储的

当Maven无法获得项目声明的依赖时,可以查看该依赖对应的文件在仓库中是否存在,如果不存在,查看是否有其他版本可用,等等。


仓库的分类

对于Maven来说,仓库只分为两类:本地仓库和远程仓库。

当Maven根据坐标寻找构件的时候,它首先会查看本地仓库,如果本地仓库存在此构件,则直接使用;

如果本地仓库不存在此构件,或者需要查看是否有更新的构件版本,Maven就会去远程仓库查找,发现需要的构件之后,下载到本地仓库再使用。

如果本地仓库和远程仓库都没有需要的构件,Maven就会报错。

一些特殊的远程仓库

中央仓库是Maven核心自带的远程仓库,它包含了绝大部分开源的构件。

在默认配置下,当本地仓库没有Maven需要的构件的时候,它就会尝试从中央仓库下载。

私服是另一种特殊的远程仓库,

为了节省带宽和时间,应该在局域网内架设一个私有的仓库服务器,用其代理所有外部的远程仓库。

内部的项目还能部署到私服上供其他项目使用。

其他公开的远程仓库

常见的有Java.net Maven库(http://download.java.net/maven/2/)和JBoss Maven库(http://repository.jboss.com/maven2/)等

Maven仓库的分类

00038

本地仓库

一般来说,在Maven项目目录下,没有诸如lib/这样用来存放依赖文件的目录。

当Maven在执行编译或测试时,如果需要使用依赖文件,它总是基于坐标使用本地仓库的依赖文件。

.m2/repository/

默认情况下,不管是在Windows还是Linux上,每个用户在自己的用户目录下都有一个路径名为.m2/repository/的仓库目录。

例如,笔者的用户名是juven,我在Windows机器上的本地仓库地址为C:\Users\juven.m2\repository\, 而我在Linux上的本地仓库地址为/home/juven/.m2/repository/。 注意,在Linux系统中,以点(.)开头的文件或目录默认是隐藏的,可以使用ls-a命令显示隐藏文件或目录。

自定义本地仓库目录地址

编辑文件~/.m2/settings.xml,设置localRepository元素的值为想要的仓库地址。例如:

<settings>

    <localRepository>D:\java\repository\</localRepository>

</settings>

这样,该用户的本地仓库地址就被设置成了D:\java\repository\。

默认情况下,~/.m2/settings.xml文件是不存在的,用户需要从Maven安装目录复制$M2_HOME/conf/settings.xml文件再进行编辑。 推荐不要直接修改全局目录的settings.xml文件

一个构件只有在本地仓库中之后,才能由其他Maven项目使用

构件如何进入到本地仓库

1 依赖Maven从远程仓库下载到本地仓库中

2 将本地项目的构件安装到Maven仓库中 例如,本地有两个项目A和B,两者都无法从远程仓库获得,而同时A又依赖于B,为了能构建A,B就必须首先得以构建并安装到本地仓库中。 执行mvn clean install命令 Install插件的install目标将项目的构建输出文件安装到本地仓库。 在上述输出中,构建输出文件是account-email-1.0.0-SNAPSHOT.jar,本地仓库地址是D:\java\repository

Maven使用Install插件将该文件复制到本地仓库中,具体的路径根据坐标计算获得

远程仓库

安装好Maven后,如果不执行任何Maven命令,本地仓库目录是不存在的。 当用户输入第一条Maven命令之后,Maven才会创建本地仓库,然后根据配置和需要,从远程仓库下载构件至本地仓库。

Maven需要构件的时候先从本地仓库找 当Maven无法从本地仓库找到需要的构件的时候,就会从远程仓库下载构件至本地仓库

对于Maven来说,每个用户只有一个本地仓库,但可以配置访问很多远程仓库。

中央仓库

由于最原始的本地仓库是空的,Maven必须知道至少一个可用的远程仓库,才能在执行Maven命令的时候下载到需要的构件

中央仓库就是这样一个默认的远程仓库,Maven的安装文件自带了中央仓库的配置。

可以使用解压工具打开jar文件$M2_HOME/lib/maven-model-builder-3.0.jar(在Maven 2中,jar文件路径类似于$M2_HOME/lib/maven-2.2.1-uber.jar),然后访问路径org/apache/maven/model/pom-4.0.0.xml(maven 2中为org/apache/maven/project/pom-4.0.0.xml),可以看到

<repositories>
    <repository>
        <id>central</id>
        <name>Maven Repository Switchboard</name>
        <url>http://repo1.maven.org/maven2</url>
        <layout>default</layout>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
    </repository>
</repositories>

包含这段配置的文件是所有Maven项目都会继承的超级POM,

这段配置使用id central对中央仓库进行唯一标识,其名称为Maven Repository Switchboard,它使用default仓库布局

对于Maven 1的仓库,需要配置值为legacy的layout

snapshots元素,其子元素enabled的值为false,表示不从该中央仓库下载快照版本的构件

中央仓库包含了这个世界上绝大多数流行的开源Java构件,以及源码、作者信息、SCM、信息、许可证信息等,每个月这里都会接受全世界Java程序员大概1亿次的访问

私服

私服是一种特殊的远程仓库, 它是架设在局域网内的仓库服务,私服代理广域网上的远程仓库,供局域网内的Maven用户使用。

当Maven需要下载构件的时候,它从私服请求, 如果私服上不存在该构件,则从外部的远程仓库下载, 缓存在私服上之后,再为Maven的下载请求提供服务。

一些无法从外部仓库下载到的构件也能从本地上传到私服上供大家使用

私服的用途 00039

即使在一台直接连入Internet的个人机器上使用Maven,也应该在本地建立私服。

节省自己的外网带宽。

建立私服同样可以减少组织自己的开支,大量的对于外部仓库的重复请求会消耗很大的带宽,利用私服代理外部仓库之后,对外的重复构件下载便得以消除,即降低外网带宽的压力

加速Maven构建。

不停地连接请求外部仓库是十分耗时的,但是Maven的一些内部机制(如快照更新检查)要求Maven在执行构建的时候不停地检查远程仓库数据。 因此,当项目配置了很多外部远程仓库的时候,构建的速度会被大大降低。 使用私服可以很好地解决这一问题,当Maven只需要检查局域网内私服的数据时,构建的速度便能得到很大程度的提高。

部署第三方构件。

当某个构件无法从任何一个外部远程仓库获得,怎么办? 这样的例子有很多,如组织内部生成的私有构件肯定无法从外部仓库获得、Oracle的JDBC驱动由于版权因素不能发布到公共仓库中。 建立私服之后,便可以将这些构件部署到这个内部的仓库中,供内部的Maven项目使用。

提高稳定性,增强控制。

Maven构建高度依赖于远程仓库,因此,当Internet不稳定的时候,Maven构建也会变得不稳定,甚至无法构建。 使用私服后,即使暂时没有Internet连接,由于私服中已经缓存了大量构件,Maven也仍然可以正常运行。 此外,一些私服软件(如Nexus)还提供了很多额外的功能,如权限管理、RELEASE/SNAPSHOT区分等,管理员可以对仓库进行一些更高级的控制。

降低中央仓库的负荷。

运行并维护一个中央仓库不是一件容易的事情,服务数百万的请求,存储数T的数据,需要相当大的财力。 使用私服可以避免很多对中央仓库重复的下载,想象一下,一个有数百位开发人员的公司,在不使用私服的情况下,一个构件往往会被重复下载数百次; 建立私服之后,这几百次下载就只会发生在内网范围内,私服对于中央仓库只有一次下载。

建立私服是用好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和snapshots元素比较重要,它们用来控制Maven对于发布版构件和快照版构件的下载。

enabled子元素,该例中releases的enabled值为true,表示开启JBoss仓库的发布版本下载支持,而snapshots的enabled值为false,表示关闭JBoss仓库的快照版本的下载支持。 因此,根据该配置,Maven只会从JBoss仓库下载发布版的构件,而不会下载快照版的构件。

该例中的layout元素值default表示仓库的布局是Maven 2及Maven 3的默认布局,而不是Maven 1的布局。

对于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完全忽略校验和错误。

远程仓库的认证 settings.xml

配置认证信息和配置仓库信息不同,仓库信息可以直接配置在POM文件中,但是认证信息必须配置在settings.xml文件中。 这是因为POM往往是被提交到代码仓库中供所有成员访问的,而settings.xml一般只放在本机。 因此,在settings.xml中配置认证信息更为安全。

假设需要为一个id为my-proj的仓库配置认证信息,编辑settings.xml文件见代码 在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元素,settings.xml中server元素的id必须与POM中需要认证的repository元素的id完全一致。 正是这个id将认证信息与仓库配置联系在了一起。

部署至远程仓库 distributionManagement

私服的一大作用是部署第三方构件,包括组织内部生成的构件以及一些无法从外部仓库直接获取的构件。 无论是日常开发中生成的构件,还是正式版本发布的构件,都需要部署到仓库中,供其他团队成员使用。

Maven除了能对项目进行编译、测试、打包之外,还能将项目生成的构建部署到仓库中。 首先,需要编辑项目的pom.xml文件。配置distributionManagement元素见代码清单

在POM中配置构件部署地址
<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的世界中,任何一个项目或者构件都必须有自己的版本。 版本的值可能是1.0.0、1.3-alpha-4、2.0、2.1-SNAPSHOT或者2.1-20091214.221414-13。 其中,1.0.0、1.3-alpha-4和2.0是稳定的发布版本, 而2.1-SNAPSHOT和2.1-20091214.221414-13是不稳定的快照版本。

为什么要区分发布版和快照版

试想一下这样的情况,小张在开发模块A的2.1版本,该版本还未正式发布,与模块A一同开发的还有模块B,它由小张的同事季MM开发,B的功能依赖于A。在开发的过程中,小张需要经常将自己最新的构建输出,交给季MM,供她开发和集成调试,问题是,这个工作如何进行呢?

1.方案一

让季MM自己签出模块A的源码进行构建。这种方法能够确保季MM得到模块A的最新构件,不过她不得不去构建模块A。多了一些版本控制和Maven操作还不算,当构建A失败的时候,她会是一头雾水,最后不得不找小张解决。显然,这种方式是低效的。

2.方案二

重复部署模块A的2.1版本供季MM下载。虽然小张能够保证仓库中的构件是最新的,但对于Maven来说,同样的版本和同样的坐标就意味着同样的构件。因此,如果季MM在本机的本地仓库包含了模块A的2.1版本构件,Maven就不会再对照远程仓库进行更新。除非她每次执行Maven命令之前,清除本地仓库,但这种要求手工干预的做法显然也是不可取的。

3.方案三

不停更新版本2.1.1、2.1.2、2.1.3……。首先,小张和季MM两人都需要频繁地更改POM,如果有更多的模块依赖于模块A,就会涉及更多的POM更改;其次,大量的版本其实仅仅包含了微小的差异,有时候是对版本号的滥用。

Maven的快照版本机制就是为了解决上述问题。

在该例中,小张只需要将模块A的版本设定为2.1-SNAPSHOT,然后发布到私服中, 在发布的过程中,Maven会自动为构件打上时间戳。 比如2.1-20091214.221414-13就表示2009年12月14日22点14分14秒的第13次快照。 有了该时间戳,Maven就能随时找到仓库中该构件2.1-SNAPSHOT版本最新的文件。 这时,季MM配置对于模块A的2.1-SNAPSHOT版本的依赖,当她构建模块B的时候,Maven会自动从仓库中检查模块A的2.1-SNAPSHOT的最新构件,当发现有更新时便进行下载。 默认情况下,Maven每天检查一次更新(由仓库配置的updatePolicy控制), 用户也可以使用命令行-U参数强制让Maven检查更新,如mvn clean install-U

基于快照版本机制,小张在构建成功之后才能将构件部署至仓库,而季MM可以完全不用考虑模块A的构建, 并且她能确保随时得到模块A的最新可用的快照构件,而这一切都不需要额外的手工操作。

当项目经过完善的测试后需要发布的时候,就应该将快照版本更改为发布版本。 例如,将2.1-SNAPSHOT更改为2.1,表示该版本已经稳定,且只对应了唯一的构件。 相比之下,2.1-SNAPSHOT往往对应了大量的带有不同时间戳的构件,这也决定了其不稳定性。

快照版本只应该在组织内部的项目或模块间依赖使用,因为这时,组织对于这些快照版本的依赖具有完全的理解及控制权。 项目不应该依赖于任何组织外部的快照版本依赖,由于快照版本的不稳定性,这样的依赖会造成潜在的危险。 也就是说,即使项目构建今天是成功的,由于外部的快照版本依赖实际对应的构件随时可能变化,项目的构建就可能由于这些外部的不受控制的因素而失败。

从仓库解析依赖的机制

当本地仓库没有依赖构件的时候,Maven会自动从远程仓库下载; 当依赖版本为快照版本的时候,Maven会自动找到最新的快照。 这背后的依赖解析机制可以概括如下:

1)当依赖的范围是system的时候,Maven直接从本地文件系统解析构件。

2)根据依赖坐标计算仓库路径后,尝试直接从本地仓库寻找构件,如果发现相应构件,则解析成功。

3)在本地仓库不存在相应构件的情况下,如果依赖的版本是显式的发布版本构件,如1.2、2.1-beta-1等,则遍历所有的远程仓库,发现后,下载并解析使用。

4)如果依赖的版本是RELEASE或者LATEST,则基于更新策略读取所有远程仓库的元数据groupId/artifactId/maven-metadata.xml,将其与本地仓库的对应元数据合并后,计算出RELEASE或者LATEST真实的值,然后基于这个真实的值检查本地和远程仓库,如步骤2)和3)。

5)如果依赖的版本是SNAPSHOT,则基于更新策略读取所有远程仓库的元数据groupId/artifactId/version/maven-metadata.xml,将其与本地仓库的对应元数据合并后,得到最新快照版本的值,然后基于该值检查本地仓库,或者从远程仓库下载。

6)如果最后解析得到的构件版本是时间戳格式的快照,如1.4.1-20091104.121450-121,则复制其时间戳格式的文件至非时间戳格式,如SNAPSHOT,并使用该非时间戳格式的构件。

当依赖的版本不明晰的时候,如RELEASE、LATEST和SNAPSHOT,Maven就需要基于更新远程仓库的更新策略来检查更新。 有一些配置与此有关:首先是<releases><enabled>和<snapshots><enabled>,只有仓库开启了对于发布版本的支持时,才能访问该仓库的发布版本构件信息,对于快照版本也是同理;其次要注意的是<releases>和<snapshots>的子元素<updatePolicy>,该元素配置了检查更新的频率,每日检查更新、永远检查更新、从不检查更新、自定义时间间隔检查更新等。最后,用户还可以从命令行加入参数-U,强制检查更新,使用参数后,Maven就会忽略<updatePolicy>的配置。

仓库元数据 maven-metadata.xml

当Maven检查完更新策略,并决定检查依赖更新的时候,就需要检查仓库元数据maven-metadata.xml。

RELEASE和LATEST版本,它们分别对应了仓库中存在的该构件的最新发布版本和最新版本(包含快照), 而这两个“最新”是基于groupId/artifactId/maven-metadata.xml计算出来的

基于groupId和artifactId的maven-metadata.xml
<?xml version="1.0" encoding="UTF-8"?>
<metadata>
    <groupId>org.sonatype.nexus</groupId>
    <artifactId>nexus</artifactId>
    <versioning>
        <latest>1.4.2-SNAPSHOT</latest>
        <release>1.4.0</release>
        <versions>
            <version>1.3.5</version>
            <version>1.3.6</version>
            <version>1.4.0-SNAPSHOT</version>
            <version>1.4.0</version>
            <version>1.4.0.1-SNAPSHOT</version>
            <version>1.4.1-SNAPSHOT</version>
            <version>1.4.2-SNAPSHOT</version>
        </versions>
        <lastUpdated>20091214221557</lastUpdated>
    </versioning>
</metadata>

该XML文件列出了仓库中存在的该构件所有可用的版本,同时latest元素指向了这些版本中最新的那个版本,该例中是1.4.2-SNAPSHOT。 而release元素指向了这些版本中最新的发布版本,该例中是1.4.0。 Maven通过合并多个远程仓库及本地仓库的元数据,就能计算出基于所有仓库的latest和release分别是什么,然后再解析具体的构件。

需要注意的是,在依赖声明中使用LATEST和RELEASE是不推荐的做法,因为Maven随时都可能解析到不同的构件,可能今天LATEST是1.3.6,明天就成为1.4.0-SNAPSHOT了,且Maven不会明确告诉用户这样的变化。 当这种变化造成构建失败的时候,发现问题会变得比较困难。RELEASE因为对应的是最新发布版构建,还相对可靠, LATEST就非常不可靠了,为此,Maven 3不再支持在插件配置中使用LATEST和RELEASE。 如果不设置插件版本,其效果就和RELEASE一样,Maven只会解析最新的发布版本构件。 不过即使这样,也还存在潜在的问题。 例如,某个依赖的1.1版本与1.2版本可能发生一些接口的变化,从而导致当前Maven构建的失败。

基于groupId、artifactId和version的maven-metadata.xml

当依赖的版本设为快照版本的时候,Maven也需要检查更新, 这时,Maven会检查仓库元数据groupId/artifactId/version/maven-metadata.xml

<?xml version="1.0" encoding="UTF-8"?>
<metadata>
    <groupId>org.sonatype.nexus</groupId>
    <artifactId>nexus</artifactId>
    <version>1.4.2-SNAPSHOT</version>
    <versioning>
        <snapshot>
            <timestamp>20091214.221414</timestamp>
            <buildNumber>13</buildNumber>
        </snapshot>
        <lastUpdated>20091214221558</lastUpdated>
    </versioning>
</metadata>

该XML文件的snapshot元素包含了timestamp和buildNumber两个子元素,分别代表了这一快照的时间戳和构建号,基于这两个元素可以得到该仓库中此快照的最新构件版本实际为1.4.2-20091214.221414-13。通过合并所有远程仓库和本地仓库的元数据,Maven就能知道所有仓库中该构件的最新快照。

最后,仓库元数据并不是永远正确的,有时候当用户发现无法解析某些构件,或者解析得到错误构件的时候,就有可能是出现了仓库元数据错误,这时就需要手工地,或者使用工具(如Nexus)对其进行修复。

镜像

如果仓库X可以提供仓库Y存储的所有内容,那么就可以认为X是Y的一个镜像。 换句话说,任何一个可以从仓库Y获得的构件,都能够从它的镜像中获取

http://maven.net.cn/content/groups/public/ 是中央仓库http://repo1.maven.org/maven2/在中国的镜像, 由于地理位置的因素,该镜像往往能够提供比中央仓库更快的服务。 因此,可以配置Maven使用该镜像来替代中央仓库。编辑settings.xml

<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,表示该配置为中央仓库的镜像, 任何对于中央仓库的请求都会转至该镜像,用户也可以使用同样的方法配置其他仓库的镜像。 另外三个元素id、name、url与一般仓库配置无异,表示该镜像仓库的唯一标识符、名称以及地址。 类似地,如果该镜像需要认证,也可以基于该id配置仓库认证。

配置使用私服作为镜像

关于镜像的一个更为常见的用法是结合私服。 由于私服可以代理任何外部的公共仓库(包括中央仓库),因此,对于组织内部的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>的值为星号,表示该配置是所有Maven仓库的镜像,任何对于远程仓库的请求都会被转至http://192.168.1.100/maven2/。 如果该镜像仓库需要认证,则配置一个id为internal-repository的<server>即可

高级的镜像配置:

<mirrorOf>*</mirrorOf>:匹配所有远程仓库。

<mirrorOf>external:*</mirrorOf>:匹配所有远程仓库,使用localhost的除外,使用file://协议的除外。也就是说,匹配所有不在本机上的远程仓库。

<mirrorOf>repo1,repo2</mirrorOf>:匹配仓库repo1和repo2,使用逗号分隔多个远程仓库。

<mirrorOf>*,!repo1</mirrorOf>:匹配所有远程仓库,repo1除外,使用感叹号将仓库从匹配中排除。

需要注意的是,由于镜像仓库完全屏蔽了被镜像仓库,当镜像仓库不稳定或者停止服务的时候,Maven仍将无法访问被镜像仓库,因而将无法下载构件。

仓库搜索服务

我们可能只知道需要使用类库的项目名称,但添加Maven依赖要求提供确切的Maven坐标

Sonatype Nexus

http://repository.sonatype.org/

Nexus是当前最流行的开源Maven仓库管理软件

Nexus提供了关键字搜索、类名搜索、坐标搜索、校验和搜索等功能。 搜索后,页面清晰地列出了结果构件的坐标及所属仓库。用户可以直接下载相应构件,还可以直接复制已经根据坐标自动生成的XML依赖声明

Jarvana

http://www.jarvana.com/jarvana/

Jarvana提供了基于关键字、类名的搜索,构件下载、依赖声明片段等功能也一应俱全。 值得一提的是,Jarvana还支持浏览构件内部的内容。此外,Jarvana还提供了便捷的Java文档浏览的功能

MVNbrowser

http://www.mvnbrowser.com

MVNbrowser只提供关键字搜索的功能,除了提供基于坐标的依赖声明代码片段等基本功能之外,MVNbrowser的一大特色就是,能够告诉用户该构件的依赖于其他哪些构件(Dependencies)以及该构件被哪些其他构件依赖(Referenced By)

MVNrepository

http://mvnrepository.com/

MVNrepository的界面比较清新,它提供了基于关键字的搜索、依赖声明代码片段、构件下载、依赖与被依赖关系信息、构件所含包信息等功能。MVNrepository还能提供一个简单的图表,显示某个构件各版本间的大小变化。

WangShuXian6 avatar Aug 29 '21 09:08 WangShuXian6

生命周期和插件

WangShuXian6 avatar Aug 29 '21 16:08 WangShuXian6