Maven实战

本文目录

Maven简介

何为maven

Maven被翻译成知识的积累,也可以翻译成“专家”、“内行”,他是Apache组织中的一个优秀的开源项目,Maven主要服务于基于java平台的项目构建、依赖管理和项目信息管理。

构建工作

之前的构建

程序员工作中会有大量的构建(build)工作,众多的IDE中大都是一个锤子的图标。我们会从仓库中拉取代码,在本地通过编译运行测试代码的各种问题;在以前开发的时候,不同项目调用相同的jar包,需要将jar包分别手动复制到对应的工程下,通过工程的build path关联路径,从而使得该jar包能够被各个项目所使用。
在这里插入图片描述
这种结构使得很多公共的jar包会被复制很多份,分别放到不同的项目中,这种方式不仅增加了构建工作、浪费计算机资源,而且在jar包变动的时候也难以管理,也可能手动导入许多没用的jar包等等一系列的问题。

Maven构建

Maven有仓库的概念,将依赖统一放到仓库中管理通过坐标定位每一个jar包的位置,项目中只需要设置坐标就能自动的跟进所需要的jar包。这种方式节约了计算机资源、易于管理,大大提高了程序员的构建效率。
在这里插入图片描述
不仅如此,Maven可以自动化构建过程,从清理、编译、测试到生成报告,再到打包和部署。我们不需要一遍遍的输入命令,进行各种繁杂的操作,我们要做的就是Maven配置好了之后,输入简单的命令(例如mvn clean install等,大多IDE上还有对应的界面操作),Maven就会帮我们完成哪些复杂的操作。

Maven是优秀的构建工具

Maven最大化的消除了构建的重复,抽象了构建生命周期,并且为绝大部分的构建任务提供了已实现的插件,通过这些插件,Maven能够实现更多的事情。同时,Maven还标准化了构建的过程,极大的避免了不必要的学习成本等。

Maven不仅仅是构建工具

Java不仅仅是一门编程语言,还是一个平台,通过JRuby和Jython,我们可以在java平台上运行Ruby和Python程序。Maven也不仅仅是一个构建工具,还是一个依赖管理工具和项目信息管理工具。他提供了中央仓库,能帮助我们自动下载构件。
Maven为全世界java开发者免费提供了一个中央仓库,在其中几乎可以找到任何开源的类库,国内也通了一些镜像可供我们提升下载速度。

Maven的安装和配置

在windows上安装Maven

Maven不是多么高级的技术,他是用java写的,所以使用Maven就必须要有JDK。Windows上查看JDK安装和配置:
使用java -version命令或者使用echo %JAVA_HOME%
在这里插入图片描述

下载Maven

官方下载地址
在这里插入图片描述
可以下载bin或者附带源码的src,官网上还提供了MD5校验和(checksum)文件、asc数字签名文件等,可以用来验证Maven分发包的正确性和安全性。

本地安装

与tomcat类似,windows上安装Maven只需要解压、配置环境变量即可使用,环境变量的配置最终能定位bin目录即可,这里不做过多的描述。

在基于Unix的系统上安装Maven

Maven是跨平台的,它可以在任何一种主流操作系统上运行。

Maven的目录结构

解压目录结构

Maven解压后的目录结构如下:
在这里插入图片描述

  1. bin:该目录包含了mvn运行的脚本,这些脚本用来配置java命令,准备好classpath和相关的java系统属性,然后执行java命令。其中mvn是基于Unix平台的shell脚本,mvn.bat是基于windows的bat脚本。在命令行输入任何一条mvn命令时,实际上就是在调用这些脚本。该目录还包含了mvnDebug和mvnDebug.bat两个文件。
  2. boot:该目录包含一个文件jar文件,该jar文件一般是类加载器的jar包,一般用户不用关心他
  3. conf:该目录包含了一个非常重要的setting.xml。直接修改该文件,就能在机器上全局的订制Maven的行为。一般情况下,我们更偏向于将该文件复制至~/.m2/目录下( ~表示用户目录)。然后修改该文件,在用户范围订制Maven行为。
  4. lib:该目录包含了所有Maven运行时需要的java类库,Maven本身是分模块开发的,因此用户能看到诸如maven-core-3.8.1.jar、maven-model-3.8.1.jar等文件。此外,这里还能看到maven用到的一些第三方依赖,如commons-cli-1.4.jar、jsr250-api-1.0.jar等
  5. 其他:LICENSE记录了Maven使用许可证;NOTICE记录了Maven包含的第三方软件;README.txt包含了Maven的简要介绍,包括安装需求及如何安装的简要指令等。

用户目录结构(~/.m2)

用户目录下可以发现一个.m2文件夹,默认情况下,该文件夹下放置了Maven本地仓库.m2/repository。所有的Maven构件都被存储到该仓库中,可以方便重用。Maven根据一套规则来确定任何一个构件在仓库中的具体位置。

设置HTTP代理

一些公司出于安全考虑,要求使用安全认证的代理访问因特网。这种情况下,就需要为Maven配置HTTP代理,才能让他正常的访问外部仓库,下载所需要的资源。
首先确认是否能够访问公共的Maven中央仓库,直接运行命令

ping repo1.maven.org

可以检查网络,如果需要代理,检查一下代理服务器是否能够连同,比如有一个代理为218.14.227.197,端口号为3128的代理服务,可以运行

telnet 218.14.227.197 3128

来检测该地址的该端口是否畅通,检查如果没有问题,编辑~/.m2/setting.xml文件(如果没有该文件,复制解压路径中conf目录下的setting.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>resposity.mycom.cn|*.google.com</nonProxyHosts>
			 -->
		</proxy>
	</proxies>
...
</settings>

这段配置十分简单,proxies下可以有多个proxy元素,如果声明了多个proxy元素,默认情况下第一个proxy会生。这里声明了一个id为my-proxy的代理,active值为true表示激活该代理,protocol表示使用的代理协议,这里是http。这里指定了主机名(host)和端口号(port)。上述xml中注释掉了username、password、nonProxyHosts元素,当需要认证时,就需要配置这些了,nonProxyHosts用来表示哪些主机不需要代理,中间用“|”分隔多个主机名这里也支持通配符,例如*.google.com表示所有以google.com结尾的域名访问都不需要代理。

MAVEN最佳实践

设置MAVEN_OPTS环境变量

运行mvn实际上是执行了java命令,既然是运行java命令,那么java命令可用的参数当然在运行mvn时也可用,这时候,MAVEN_OPTS环境变量就能派上用场了。通常需要设置MAVEN_OPTS的值为-Xms128m -Xmx512m,以为java默认的最大可用内存往往不能够满足MAVEN运行的需求,比如在项目比较时,使用MAVEN生成站点需要占用大量内存,如果没有配置,很容易得到java.lang.OutOfMemeoryError。

配置用户范围setting.xml

MAVEN解压路径下conf目录的setting.xml是全局配置,~/.m2/setting.xml配置的是用户范围内的配置。

MAVEN使用入门

编写POM

就像Make的Makefile、Ant的build.xml一样,Maven项目的核心是pom.xml。POM(Project Object Model,项目对象模型)定义了项目的基本信息,用于描述项目如何构建,声明项目依赖等等。现在为HelloWorld项目编写一个最简单的POM。
首先创建一个名为hello-world的文件夹,打开文件夹,新建一个名为pom.xml的文件,输入一下内容:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
         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.juvenxu.mvnbook</groupId>
    <artifactId>hello-world</artifactId>
    <version>1.0-SNAPSHOT</version>
    <name>maven hello world project</name>
</project>

代码的第一行是XML头,指定了XML版本和编码格式,紧接着是project元素,project是所有pom.xml的根元素,他还声明了一些POM相关的命名空间及xsd元素,虽然这些属性不是必须的,但是这些能够让第三方工具(IDE中的XML编辑器)帮助我们更快速的编辑POM。
modelVersion指定了当前POM模型的版本,对于Maven2及Maven3来说,它只能是4.0.0。
该代码中最重要的是包含groupId、artifactId、和version三行。这三个元素定义了一个项目基本的坐标,在Maven的世界,任何jar、pom或者war都是可以基于这些基本的坐标进行区分的。
groupId定义了当前Maven在组中唯一的ID。
version指定了helloworld项目当前的版本。1.0SNAPSHOT为该配置的版本,SNAPSHOT意为快照,说明当前的项目还在开发中,是不稳定的版本。随着项目的发展,version会不断更新,如升级为1.1-SNAPSHOT、2.0等。
name为该项目指定了一个更友好的项目名称,虽然这个不是必须的,但还是推荐为每个POM声明name,以方便信息交流。
没有任何的实际的java代码,我们就能定义一个MAVEN项目的POM,这体现了MAVEN的一大优点,它能让项目对象模型最大程度的与实际代码相互独立,我们可以称之为解耦,或者正交性。这在很大程度上避免了java代码和POM代码的相互影响。比如当项目需要升级版本时,只需要修改POM,而不需要更改java代码;而在POM稳定之后,日常的java代码开发工作基本不会涉及POM的修改。

编写主代码

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

package com.juvenxu.mvnbook.helloworld;
public class HelloWorld{
	public String SayHello(){
		return "Hello Maven";
	}
	public static void main(String[] args){
		System.out.println(new HelloWorld().SayHello());
	}
}

这是一个简单的java类,它有一个sayHello方法,返回一个String。同时这个类还有一个main方法,创建一个HelloWorld实例,调用sayHello()方法,并将结果输出到控制台。
关于java代码有两点需要注意,首先,在绝大多数数情况下,应该把项目主代码放到src/main/java目录下(遵循Maven约定),而无序额外的配置,Maven会自动搜索该目录找到主代码。其次,该java类的包名是com.juvenxu.mvnbook.helloworld,这与之前在POM中定义的groupId和artifactId相吻合。一般来说,项目中java类的包应该都基于项目的groupId和artifactId,这样更加清晰,更加符合逻辑,也方便检索构建和java类。
代码编写完毕之后,在项目根目录下运行命令:

mvn clean compile

clean告诉Maven清理输出目录target/(删除该目录),compile告诉Maven编译目标主代码(执行javac)。
首先执行了clean:clean任务,删除target/目录。默认情况下,Maven构建的所有输出都在target/目录中;接着执行了resource:resource任务(未定义资源,暂且略过);最后执行compiler:compile任务,将该项目主代码编译至target/class目录(编译好的类为:com/juvenxu/mvnbook/helloworld/HelloWorld.class。
上文提到的clean:clean、resources:resources和compiler:compile对应了一些Maven插件及插件目标,比如clean:clean是clean插件的clean目标,compiler:compile是compiler插件的compile目标。
至此,Maven在没有任何额外配置的情况下就执行了项目的清理和编译任务。接下来,编写一些测试代码并让Maven自动化测试。

编写测试代码

为了使项目结构保持清晰,主代码与测试代码应该分别位于独立的项目中。Maven项目中默认的主代码目录是src/main/java,对应的,Maven项目中默认的测试代码目录是src/test/java。因此,在编写测试用例之前,应当先创建该目录。
在java中,由Kent Beck和Erich Gamma建立的JUnit是事实上的单元测试标准。要是用JUnit,首先要为HelloWorld项目添加一个JUnit依赖,修改POM如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
         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>cn.hsa.ces</groupId>
    <artifactId>hsa-ces-local-web</artifactId>
    <version>1.0.0</version>
    <name>hsa-ces-local-web</name>

    <dependencies>
          <dependency>
                    <groupId>junit</groupId>
                    <artifactId>junit</artifactId>
                    <scope>test</scope>
          </dependency>
    </dependencies>
    
</project>

代码中添加了dependencies元素,该元素下可以包含多个dependency元素以声明项目的依赖。这里添加了一个依赖----groupId是junit,artifactId是junit。前面讲到groupId和artifactId、version是任何一个Maven项目最基本的坐标,JUnit也不例外,有了这段声明,Maven就能够自动下载junit-4.7.jar。也许你会问,Maven从哪里下载这个jar呢?在Maven之前,可以去JUnit官网下载分发包,有了Maven,他会自动访问中央仓库(http://repo1.maven.org/maven2/),下载需要的文件读者也可以自己访问该仓库,打开路径,junit/junit/4.7/,就能看到junit-4.7.pom和junit-4.7.jar。
上述的POM代码中还有一个值为test的元素scope,scope表示为依赖范围,若依赖范围为test则表示该依赖只对测试有效,换句话说,测试代码中import JUnit是没有问题的,但是在主代码中import JUnit代码,就会造成编译错误。如果不声明范围,默认值为compile,表示该代码对主代码和测试代码都有用。
配置了测试依赖,接着就可以编写测试类。回顾一下前面的HelloWorld类,现在要测试一下该类的sayHello()方法是否返回“Hello Maven”。在src/test/java目录下创建HelloWorldTest类,代码如下:

package com.juvenxu.mvnbook.helloworld;
import static org.junit.Assert.assertEquals;
import org.junit.Test

public class HelloWorldTest{
	@Test
	public void testSayHello(){
		HelloWorld helloWorld = new HelloWorld();
		String result = helloWorld.sayHellow();
		assentEquals("Hello Maven",reslut);
	}
}

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

  1. 准备测试类及数据
  2. 执行要测试的行为
  3. 检查结果
    上述样例首先初始化了一个要测试的HelloWorld实例,接着执行该实例的sayHello()方法,并保存到result变量中,最后使用Junit框架的Assert类检查结果是否为我们期望的“Hello Maven”。在Maven3中,约定所有要执行的类的方法都以test开头,这里使用了JUnit4,但仍遵循这个约定。在Junit4中,需要执行方法都是以@Test进行标注。
    测试用例编写完了之后就可以调用Maven执行测试。运行

mvn clean test

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

打包和运行

将项目进行编译、测试,下一个重要步骤就是打包(package)。HelloWorld的POM没有指定打包类型,使用默认打包类型jar。简单的执行package命令进行打包。

mvn clean package

Maven在打包之前会进行编译、测试等工作。打包完成之后,会在target/目录下生成一个jar文件(这里的为HelloWorldMaven-1.0-SNAPSHOT.jar),文件名也可以通过finalName来指定。将得到的jar包复制到别的项目的classpath中就可以在别的项目中使用HelloWorld类了。但是,如何让Maven项目直接使用这个jar呢?还需要一个安装步骤Install

mvn clean install

该命令可以将项目输出的jar安装到本地仓库中,可以打开相应的文件夹看到HelloWorld项目的POM和jar。与之前说的JUnit的POM及jar下载到本地仓库之后才能使用的道理是一样的,我们只有将HelloWorld项目的构建安装到本地之后,其他Maven项目才能使用它。
我们已经体验了Maven最主要的命令:mvn clean compile、mvn clean test、mvn clean package、mvn clean install。执行test之前会先执行compile,执行package之前会先执行test,类似的,执行install之前会先执行package。可以在任何一个Maven项目中执行这些命令,而且我们已经清除他们是用来做什么的。
到目前为止,还没有运行HelloWorld项目,HelloWorld类有一个main方法的。默认生成的jar包是不能直接运行的,因为带有main方法的类信息不会添加到manifest中(打开jar文件中的META-INF/MANIFEST.MF文件,将无法看到Main-Class一行)。为了生成可执行的jar,需要借助maven-shade-plugin,配置该插件的代码如下:

<plugin>
	<groupId>org.apache.maven.plugins</groupId>
	<artifactId>maven-shade-plugin</artifaceId>
	<version>1.2.1</version>
	<executions>
		<execution>
			<phase>package</phase>
			<goals>
				<goal>shade</goal>
			</goals>
			<configuration>
				<transformers>
					<transformer implemention="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
						<mainClass>com.juvenxu.mvnbook.helloworld.HelloWorld</mainClass>
					</transformer>
				</transformers>
			</configuration>
		</execution>
	</executions>
</plugin>

plugin元素在POM中的相对位置应该在 < project>< build>< plugins>下面。我们配置了mainClass为com.juvenxu.mvnbook.helloworld.HelloWorld,项目在打包时会将该信息放到MANIFEST中。现在执行mvn clean install,待构建完成之后打开target/目录,可以看到HelloWorldMaven-1.0-SNAPSHOT.jar和original-HelloWorldMaven-1.0-SNAPSHOT.jar,前者是再有Main-Class信息可执行的jar,后者是原始的jar,打开HelloWorldMaven-1.0-SNAPSHOT.jar的META-INF/MANIFEST.MF,可以看到包含这样的一行信息:
Main-Class:com.juvenxu.mvnbook.helloworld.HelloWorld。现在,这里的jar文件就可以直接使用java-jar命令执行了

java-jar HelloWorldMaven-1.0-SNAPSHOT.jar

本小节介绍了HelloWorld项目,侧重点是Maven而并非java代码本身,介绍了POM、Maven项目结构以及如何进行编译、测试、打包等。

使用Archetype生成项目骨架

HelloWorld项目中有一些Maven约定:在项目根目录的位置放置pom.xml,在src/main/java的位置放置项目主代码,在src/test/java的位置放置测试代。之所以一步一步的展示这些步骤是为了能让可能是Maven初学者的你得到最实际的感受。我们称这些基本的目录结构和pom文件内容为项目的骨架,当第一次创建项目骨架的时候,你还会饶有兴趣的去体会这些默认约定背后的思想,第二次、第三次你也许还会满意自己的熟练度,但第四第五次做同样的事情你可能就会恼火了。为此Maven提供了Archetype以帮助我们快速勾勒出项目骨架。
还是以HelloWorld项目为例,我们使用Archetype来创建项目的骨架,离开当前的Maven项目目录。
如果是Maven3,简单的运行:

mvn archetype:generate

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

mvn org.apache.maven.plugins:maven-archetype-plugin:2.0-alpha-5:generate

很多资料会简单的运行mvn archetype:generate,但是这在Maven2中是不安全的,该命令没有指定archetype的版本,Maven会去自动下载最新版的,进而可能得到的是不稳定的SNAPSHOT版本,导致运行失败。而在Maven3中,即使没有指定版本,Maven也会下载最新的而且最稳定的版本,因此是安全的。
我们实际上是在运行maven-archetype-plugin,注意冒号分隔,其格式为:groupId:artifactId:version:goal,org.apache.maven.plugins是maven官方插件的groupId,maven-archetype-plugin是Archetype的artifactId,2.0-alpha-5是目前插件最稳定的版本,generate是要是用的插件目标。
紧接着会看到一段长长的输出,有很多可用的Archetype供选择,包括著名的Appfuse项目的Archetype、JPA项目的Archetype等。每一个Archetype面前都有一个对应的编号,同时命令行会提示一个默认的编号,对应的其Archetype为maven-archetype-quickstart,直接回车以选择Archetype,紧接着Maven会提示输入对应的groupId、artifactId和version以及package包名。
这里仅仅看到了一个最简单的Archetype,如果有很多项目拥有类似的自定义项目结构以及配置文件,则完全可以一劳永逸的开发自己的Archetype,然后在这些项目中使用自定义的Archetype来快速生成一个项目骨架。

背景案例

简单的账户注册服务

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

  • 提供一个未被使用的账号id
  • 提供一个未被使用的email地址
  • 提供一个任意的账户名称
  • 设置安全密码,并重复确认
  • 输入验证码
  • 前往邮箱查收激活链接并激活账号
  • 登录
    账号的ID和email地址都可以唯一的标识某个用户,而显示名称则用来显示在页面上,方便浏览。注册用户的时候还需要输入两次验证码以确保没有输错。系统需要检查id和email的唯一性,验证两次密码是否一致。验证码是由系统随机生成的只能由肉眼识别其内容的图片,可以有效防止机器恶意批量注册,若输入正确的验证码信息,系统会进行检查,如果验证码错误,系统会生成并返回新的验证码。一旦所有检查都没问题了,系统会生成一个激活链接,并发送到用户的邮箱中。单击激活链接后,账户就被激活了,这时账户注册成功,用户可以登录。
    对于一个账户注册服务,还需要考虑一些安全因素。例如,需要在服务器端密文的保存密码,检查密码的强弱程度,更进一步则需要考虑验证码的失效时间,激活链接的失效时间等等。
    本章的主要目的是让读者清楚的了解这个背景案例,即账号注册服务,他的需求是什么,基于这样的一个需求,我们怎样设计这样的一个小型的系统。本章的描述几乎不会设计Maven,但是后面的章节在讲述各种Maven概念和实践的时候,都会基于这一实际的背景案例。

需求阐述

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

坐标和依赖

Maven的一大功能是管理项目依赖。为了能够自动的解析任何一个java构件,Maven就必须将他们唯一标识,这就依赖管理的底层基础–坐标。本章将详细的分析Maven坐标的作用,解释其每一个元素;在此基础上,再介绍如何配置Maven,以及相关的经验和技巧,以帮助我们管理项目依赖。

何为Maven坐标

关于坐标(Coordinate),大家最熟悉的定义应该来自平面几何,在一个平面坐标系中,坐标(x,y)表示平面上与x轴距离为y,与y轴坐标为x的一点,任何一个坐标都能唯一的表示平面上的一点。
实际生活中,我们也可以将地址看成是一种坐标,省、市、区、街道等一系列信息同样可以唯一标识城市中任一居住地址或工作地址。邮局和快递公司正是基于这样一种坐标进行工作的。
对应于平面中的点和城市中的地址,Maven的世界中拥有数量非常巨大的构件,也就是平时用的jar、war等文件。在Maven为这些构建引入坐标概念之前,我们无法使用任何一种方式来唯一的标识这些构建。因此,当需要使用SpringFramework的时候,大家会去Spring官网去寻找,当使用log4j的时候,大家又会去apache网站去寻找。因为各种网站风格迥异,大量的时间花费在了搜索、浏览网页的等工作上面。没有统一的规范,统一的法则,该工作就无法自动化。重复的搜索、浏览网页和下载类似的jar文件,这本身就应该交给机器来做。而机器工作必须基于预定的规则,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会在需要的时候去那里下载。
当我们在定义自己的项目的时候,也需要为其定义适当的坐标,这是Maven强制要求的,在这个基础上,其他项目才能引用该项目生成的构件。

坐标详解

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

<groupId>org.sonatype.nexus</groupId>
<artifact>nexus-indexer</artifact>
<version>2.0.0</version>
<packging>jar</packging>

这是nexus-indexer的坐标定义,nexus-indexer是一个对Maven仓库编纂索引并提供索引功能的类库,他是Nexus项目的一个子模块。下面介绍一下各个元素:

  • 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.2.3.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的时候,默认值为jar。
  • classifier:该元素用来定义构建输出的一些附属构件。附属构件与主构件对应,如上例的主构件是nexus-indexer-2.0.0.jar,该项目可能还会通过一些插件生成如nexus-indexer-2.0.0-javadoc.jar、nexus-indexer-2.0.0-source.jar这样一些附属构件,其包含了java文档和源代码。这时候,javadoc和source就是这两个附属构件的classifier。这样,附属构件也就拥有了自己的唯一的坐标。还有一个关于classifier的典型例子就是TestNG,TestNG的主要构件是基于Java1.4平台的,而他又提供了一个classifier为jak5的附属构件。注意,不能直接定义项目的classifier,因为附属构件不是项目直接生成的,而是由附加的插件帮助生成的。
    上述五个元素中,groupId、artifactId和version是必须定义的,packaging是可选的(默认为jar),而classifer是不能直接定义的。
    同时,项目构建的文件名与坐标是相对应的,一般规则为artifactId-version【-classifier】.packaging。
    此外,Maven仓库的布局也是基于Maven坐标。

依赖的配置

一个依赖的声明可以包含以下的元素:

</project>
...
<dependencies>
	...
	<dependency>
		<groupId>...</groupId>
		<artifactId>...</artifactId>
		<version>...</version>
		<type>...</type>
		<scope>...</scope>
		<optional>...</optional>
		<exclusions>
			<exclusion>...</exclultion>	
		</exclutions>
		...
	</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:测试范围依赖。只对测试范围的classpath有效,在编译主代码和运行项目时将无法使用此依赖。典型的例子就是JUnit。
  • provided:已提供依赖范围。使用此依赖范围的Maven依赖,对于编译和测试classpath有效,在运行期间无效。典型的例子就是servlet-api,编译和测试项目的时候需要使用带该依赖, 但在项目运行的时候由于容器已经提供了,就不需要Maven重复的引入一遍。
  • runtime:运行时依赖范围。对编译和运行时有效,在编译期间无效。典型的例子就是JDBC驱动实现,项目代码的编译只需要JDBC提供JDBC接口,只有在执行测试或者运行项目的时候才需要实现上述接口的具体JDBC驱动。
  • system:系统依赖范围。该依赖与三种classpath的关系,和provided依赖范围完全一致。但是,使用system范围的依赖时必须通过systemPath元素显示的指定依赖文件的路径。由于此依赖不是通过Maven仓库解析的,而且往往与本机系统绑定,可能造成构建的不可移植性,因此应该谨慎使用。
  • import:导入依赖范围。该依赖范围不会对三种classpath产生实际的影响。
    上述的除import以外的依赖范围与三种classpath的关系如下:
依赖范围编译测试运行例子
compileYYYspring-core
testYJUnit
providedYYservlet-api
runtimeYYJDBC
systemYY本地的Maven仓库之外的类库文件

传递性依赖

何为传递性依赖

考虑一个基于Spring Framework的项目,如果不使用Maven,那么在项目中就需要手动下载相关的依赖。由于SpringFramework又会依赖于其他的开源类库,因此实际中往往会下载一个很大的如spring-framework-2.5.6-with-dependencies.zip的包,这里包含了所有的SpringFramework的jar包,以及它所依赖的所有jar包。这么做往往就引入了很多不必要的依赖。另一种做法是只下载spring-framework-2.5.6.zip这样一个包,这里不包含其他的相关依赖,到实际使用的时候,再根据报错信息,或者查找相关文档,加入需要的其他依赖。很显然,这也是一件非常麻烦的事情。
Maven的传递性依赖机制可以和好的解决这个问题。比如项目有一个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依赖。于是commons-logging就是一个传递性依赖。有了传递性依赖的性质,在使用spring Framework的时候就不用去考虑它依赖了什么,也不用担心引入多余的依赖。Maven会自动解析各个直接依赖的POM,将哪些必要的间接依赖,以传递性的形式引入到当前的项目中。

传递性依赖和依赖范围

依赖范围不仅可以控制依赖与三种classpath的关系,还对依赖传递性产生影响。假设A依赖于B,B依赖于C,那么A对于B是第一直接依赖,B对于C是第二直接依赖,A对于C是传递性依赖。第一直接依赖范围和第二直接依赖范围影响了传递性依赖的范围,关系如下图所示:

compiletestprovidedruntime
compilecompileruntime
testtesttest
providedprovidedprovidedprovided
runtimeruntimeruntime

观察一下表格,可以发现这样的一个规律:当第二直接依赖compile时,传递性依赖范围与第一直接依赖范围一致;当第二直接依赖范围是test时,依赖不会进行传递;当第二直接依赖范围是provided时,值传递第一直接依赖范围为provided的依赖,且传递性依赖范围与第一直接依赖范围一致都为provide的;当第二直接依赖的范围是runtime时,传递性依赖的范围有第一直接依赖范围一直,只有当第一传递性依赖范围为compile时不一致,此时传递性依赖的范围为runtime。

依赖调解

Maven引入传递性依赖的机制,一方面大大简化和方便了依赖声明,另一方面,大部分情况下我们只关心项目的最直接依赖是什么,而不用考虑这些依赖会引入什么传递性依赖。但有的时候,当传递性依赖造成问题的时候,我们就需要清楚的知道传递性依赖是从那条路径引入的。
例如,项目A有这样的依赖关系:A->B->C->X(1.0),A->D->X(2.0),X是A的传递性依赖,但是两条路径上有两个版本的X,那么那个版本的X会被使用呢?两个版本都解析显然是不对的,因为那会造成依赖重复,因此必须选择一个。Maven依赖调解(Dependency Mediation)的第一原则是:路劲最近者优先。还有一条原则是:第一声明者优先。

最佳实践

Maven依赖设计的知识点比较多,在理解了主要功能和原理之后,最需要的当然就是前人的经验总结,称之为最佳实践。

排除依赖

传递性依赖会给项目隐式的引入很多依赖,这极大的简化了项目依赖的管理,但是有些时候这种特性也会带来问题。例如,当前项目有一个第三方依赖,而这个第三方依赖由于某些原因依赖了另一个库的SNAPSHOT版本,那么这个SNAPSHOT就会成为当前项目的传递性依赖,他的不稳定可能会影响到项目。这时就应该排除这个SNAPSHOT,引入一个稳定的版本。还有一种可能是你想替换某个依赖,这时也可以排除依赖,排除依赖在dependency元素中插入一个execusions元素即可。

依赖优化

Maven会自动解析项目的直接依赖和间接依赖,并且根据规则正确的判断每个依赖的范围,对于一些依赖冲突,也能进行调节,以确保任何一个构件只有唯一的版本在依赖中存在。在这些工作之后,最后得到的那些依赖被称之为已解析依赖(Resolved Dependency)。可以运行如下的命令查看已解析依赖:

mvn dependency:list

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

mvn dependency:tree

除此之外,Maven还有一个分析依赖的命令:

mvn dependency:analyze

这个命令可以看到声明单位使用或者使用但为声明的依赖,依次排查可以增强程序的健壮性。

仓库

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

何为Maven仓库

Maven世界里,任何一个依赖、插件或项目构建的输出,都可以称之为构件。例如:log4j1.2.15.jar是一个构件,插件maven-compile-plugin-2.0.2.jar是一个构件,自己的Maven项目打包输出的jar包也是一个构件,任何一个构件都有一组坐标唯一标识。
在一台工作站上,可能会有几十个Maven项目,所有项目都使用maven-compile-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源码,并结合具体实例来理解Maven的布局方式:

private static final char PATH_SEPARATOR = '/';
private static final char GROUP_SEPARATOR = '.';
private static final char ARTIFACT_SEPARATOR = '-';

public String pathOf(Artifact artifact){
	ArticactHandler artifactHandler = artifact.getArtifactHandler();
	StringBuilder path = ner StringBuilder(128);
	path.append(formatAsDirectory(artifact.getGroupId)())).append(PATH_SEPARATOR);
	path.append(artifact.getArtifactId()).append(PATH_SEPARATOR);
	path.append(artifact.getBaseVersion()).append(ARTIFACT_SEPAROTOR).append(artifact.getVersion());
	if(artifact.hasClassifier()){
		path.append(ARTIFACT_SEPARATOR).append(artifact.getClassifier());
	}
	if(artifactHandler.getException()!=null&&artifactHandler.getException().length>0){
		path.append(GROUP_SEPAROTOR).append(artifactHandler.getException());
	}
	return path.toString();
}
private String formatAsDirectory(String directory){
	return directory.repleace(GROUP_SEPARATOR,PATH_SEPARATOR);
}	

该pathOf()方法生成的目的是根据构件信息生成其在仓库中的路径。这里根据一个实际的例子来分析路径的生成,考虑这样一个构件:

  • groupId: org.testng
  • artifactId: testng
  • version: 5.8
  • classifier: jdk15
  • packaging: jar

其对应的路径按如下的步骤生成:

  1. 基于构件groupId准备路径,formatAsDirectory()将groupId中的句点分隔转化成路径分隔符。该例中,groupId org.testng就会转化为ort/testng,之后在加上一个路径分隔符斜杠,就成了ort/testng/。
  2. 基于artifactId准备路径,也就是在前面的基础上加上artifactId以及一个路径分隔符。该例中的artifactId为testng,那么通过这步的转化之后就变成了org/testng/testng。
  3. 使用版本信息。在前面的基础上加上version和路径分隔符。该例中是5.8,那么路径就变成了org/testng/testng/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-jdk15。
  6. 检查构件的exception,若exception存在,则加上句点分隔符和exception。从代码中可以看到,exception是从artifactHandler而非artifact获取,artifactHandler是由项目的packaging决定的。因此,可以说,packaging决定了构件的扩展名,该例的packaging是jar,因此,最终路径确定为org/testng/testng/5.8/testng-5.8-jdk15.jar。
    到这里,感谢Maven开源社区,正是由于Maven的所有源代码都是开放的,我们才能仔细的深入的知道内部工作的所有细节。
    Maven仓库是基于简单文件系存储的,我们也理解了其存储方式,因此,当遇到一个与仓库相关的问题时,可以很方便的查找相关文件,方便定位问题。例如,当Maven无法获取项目声明的依赖时,可以查看对应的文件在仓库中是否存在,如果不存在,查看是否有其他的版本可用等等。

仓库的分类

对于Maven来说,仓库只分为两类:本地仓库和远程仓库。当Maven根据坐标寻找仓库的时候,他首先会看本地仓库,如果本地仓库存在次构件,则直接使用;如果本地仓库不存在此构件,或者需要查看是否有新的构件版本,Maven就回去远程仓库查找,发现需要的构件之后,下载到本地在使用。如果本地和远程仓库都有没有需要的构件,Maven就会报错。
在这个最基本分类的基础上,还有必要介绍一些特殊的远程仓库。中央仓库是Maven核心自带的远程仓库,他包含了绝大部分开源的构件。在默认情况下,当本地仓库没有Maven需要的构件的时候,它就尝试从中央仓库下载。
私服是另一种特殊的远程仓库,为了节省带宽和时间,应该在局域网内设置一个私有的仓库服务器,用其代理所有的外部的远程仓库。此内部项目还能部署到私服上供其他项目使用。
除了中央仓库和私服,还有很多其他公开的远程仓库,常见的有Java.net Maven库(http://download.java.net/maven/2/)和JBoss Maven库(http://responsitory.jboss.com/maven2/)等。
在这里插入图片描述

本地仓库

一般来说,在Maven项目目录下,没有诸如lib/这样用来存放依赖文件的目录。当Maven在执行编译或测试时,如果需要使用依赖文件,它总是基于坐标使用仓库的依赖文件。
默认情况下,无论是Windows还是Linux上,每个用户在自己的用户目录下都有一个路径名为.m2/responsitory/的仓库目录。有时候,因为某些原因(可能是C盘空间不够),用户想自定义本地仓库的位置。这时,可以编辑文件~/.m2/settings.xml,设置localResponsitory元素的值为想要的仓库地址。例如:

<settings>
	<localResponsitory>D:\java\responsitory\</localResponsitory>
</settings>

这样,该用户的本地仓库地址就被设置成了D:\java\responsitory。
需要注意的是,默认情况下,~/.m2/setting.xml文件是不存在的,用户需要从maven的安装目录复制$M2_HOME/conf/settings.xml文件再进行编辑。
一个构件只有在本地仓库中之后,才能由其他Maven项目使用,那么该构件如何才能进入到本地仓库呢?最常见的是Maven依赖从远程仓库中下载到本地仓库中。还有一种常见的情况是,将本地项目的构建安装到Maven仓库中。例如,两个项目A和B,两者都无法从远程仓库获得,而同时A由依赖B,为了构建A,B就必须得先构建并安装到本地仓库中。

远程仓库

安装好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,可以看到如下的配置:

<responsitories>
	<responsitory>
		<id>central</id>
		<name>Maven Responsitory Switchboard</name>
		<url>http://repo1.maven.org/maven2</url>
		<layout>default</layout>
		<snapshots>
			<enabled>false</enabled>
		</snapshots>
	</responsitory>
</responsitories>

包含这段配置的文件是所有Maven项目都会继承的超级POM。这段配置使用id central对中央仓库进行了唯一标识,其名称为Maven Responsitory Switchboard,他使用default默认布局。需要注意的是snapshots元素,其子元素enabled的值为false,表示不从该中央仓库中下载快照版本的构建。
中央仓库包含了这个世界上绝大多数流行的java构件,以及源码、作者信息、SCM、信息、许可证信息等,每个月这里都会接受全世界Java程序员大概1亿次的访问,它对全世界Java程序员的贡献可见一斑。由于中央仓库包含了超过2000个开源的项目的构件,因此,一般来说,一个简单的Maven项目所需要的依赖构件都能从中央仓库下载得到。这也解释了Maven为什么能做到“开箱即用”。

私服

私服是一种特殊的远程仓库,他是架设在局域网内的仓库服务,私服代理广域网上的中央仓库,供局域网内的Maven用户使用。当Maven需要下载构件的时候,他从私服请求,如果私服上不存在该构件,则从外部的远程仓库下载,缓存到私服之后,再为Maven的下载请求提供服务。此外,一些无法从外部仓库下载到的构件也能从本地上传到私服上供大家使用。如下图:
在这里插入图片描述
即使在一台直接连入Internet的个人机器上使用Maven,也应该在本地建立私服。因为私服可以帮助你:

  • 节省自己的外网带宽。建立私服同样可以减少组织自己的开支,大量的对于外部仓库的重复请求会消耗很大的带宽,利用私服代理外部仓库之后,对外的重复下载便得以消除,即降低外网带宽的压力。
  • 加速Maven构建。不停的链接请求外部仓库是十分耗时的,但是Maven的一些内部机制(如快照更新检查)要求Maven在执行构建的时候不停的检查远程仓库数据。因此,当项目配置了很多外部远程仓库的时候,构建速度会被大大降低。使用私服可以很好的解决这一问题,当Maven只需要检查局域网内私服的数据时,构建速度便能得到很大程度上的提高。
  • 部署第三方构建。当某个构件无法从任何一个外部远程仓库获得,怎么办?这样的例子有很多,如组织内部生成的私有构件肯定无法从外部仓库获得、Oracle的JDBC驱动由于版权原因不能发布到公共库中。建立私服之后,便可以将这些构建部署到这个内部的仓库中,供以Maven项目使用。
  • 提高稳定性,增强控制。Maven构建高度依赖于远程仓库,因此,当Internet不稳定的时候,Maven也会变得不稳定,甚至无法构建。使用私服之后,即使暂时没有Internet链接,由于私服中缓存了大量的构件,Maven也仍可以继续的运行。此外,一些私服软件(如nexus)还提供了很多额外的功能,如权限管理、RELEASE/SNAPSHOT分区等,管理员可以对仓库进行一些更高级的控制。
  • 降低中央仓库的负荷。运行并维护一个中央仓库并不是一件容易的事,服务数百万的请求,存储数T的数据,需要想当大的财力。使用私服可以避免很多对中央仓库的重复下载,现象一下,一个有数百位开发人员的公司,在不使用私服的情况下一个构件往往会被重复下载数百次;建立私服后,这几百次下载只会发生在内网范围内,私服对于中央仓库的下载只有一次。

远程仓库的配置

很多情况下,默认的中央仓库无法满足项目的需求,可能项目需要的构件存在于另一个远程仓库中,如JBoss Maven仓库。这时,可以在POM中配置该仓库,代码如下:

<project>
	...
	<respositories>
		<respository>
			<id>jboss</id>
			<name>JBoss Responsitory</name>
			<url>http://responsitory.jboss.com/maven2/</url>
			<releases>
				<enabled>false</enabled>
			</releases>
			<snapshots>
				<layout>default</layout>
			</snapshots>
		</respository>
	</resipositories>
	...
</project>

在repositories元素下,可以使用repository子元素声明一个或者多个远程仓库。该例中声明了一个id为jboss,名称为JBoss Repository的仓库。任何一个仓库声明的id必须是唯一的,尤其需要注意的是,Maven自带的仓库使用的id是central,如果其他仓库也是用了该id,就会覆盖中央仓库的配置。该配置中url值指向了仓库的地址,一般来说,改地址都是基于http协议,Maven用户可以在浏览器中打开仓库地址浏览构件。
该例配置中的release和snapshots元素比较重要,他们用来控制Maven对于发布版本构件和快照版构件的下载。这里需要注意的是enabled子元素,该例中的值为true,表示开启JBoss仓库的发布版本下载支持,而snapshots的enabled值为false,表示关闭JBoss仓库的快照版本的下载支持。因此,根据该配置,Maven只会从JBoss仓库下载发布版本的构件,而不会下载快照办版本的构件。
对于releases和snapshots来说,除了enabled,他们还包含另外两个子元素updatePolicy和checksumPolicy:

<spanshots>
	<enabled>true</enabled>
	<updatePolicy>daily</updatePolicy>
	<checksumPolicy>ignore</checksumPolicy>
</spanshots>

元素updatePolicy用来配置Maven从远程仓库检查更新的频率,默认值是daily,表示Maven每天检查一次。其他可用的值包括:naver-从不检查更新;always-每次构建都检查更新;interval:X-每隔X分钟检查更新一次(X为任意数)。
元素checksumPolicy用来配置Maven检查检验和文件的策略。每当构件被部署到Maven仓库中时,会同时部署对应的校验和文件。下载构件的时候,Maven会验证校验和文件,如果校验和验证失败,怎么办?当checksumPolicy的默认值为warn时,Maven会在执行构建时输出警告信息,其他值可以包括:fail-Maven遇到校验失败就让构建失败;ignore-使Maven完全忽略校验信息和错误。

远程仓库的认证

大部分远程仓库无序认证就可以访问,但有时候出于安全方面考虑,我们需要提供认证信息才能访问一些远程仓库。例如,组织内部有一个Maven仓库服务器,该服务器为每个项目都提供一个独立的Maven仓库,为防止非法的仓库访问,管理员为每个仓库提供了一组用户名及密码。这时,为了能让Maven访问仓库内容,就需要配置认证信息。
配置认证信息和配置仓库信息不同,仓库信息可以直接配置在pom文件中,但是认证信息必须配置在settings.xml文件中。这是因为POM往往是被提交到代码仓库中供所有成员访问的,而settings.xml一般只放在本机。因此,在settings.xml中配置认证信息更加的安全。
假设需要设置一个id为my-proj的仓库配置认证信息,编辑settings.xml文件代码清单如下:

<settings>
	...
	<servers>
		<server>
			<id>my-proj</id>
			<username>repo-usr</username>
			<password>repo-pwd</password>
		</server>
	</servers>
	...
</settings>

Maven使用settings.xml文件中并不是显而易见的servers元素以及server子元素配置仓库认证信息。上面的代码中该仓库的认证用户名为repo-usr,认证密码为repo-pwd。这里的关键是id元素,settings.xml汇总server元素的id元素必须与pom中需要认证的repository元素的id完全一致。换句话说,正式这个id将认证信息与仓库配置联系在了一起。

部署至远程仓库

私服的一大作用是部署第三方构件,包括组织内部生成的构件以及一些无法从外部仓库直接获取的构件。无论是日常开发中生成的构件,还是正式版本发布的构件,都需要部署到仓库中,供其他团队成员使用。
Maven除了能对项目进行编译、测试、打包之外,还能将项目生成的构件部署到仓库中。首先,需要编辑项目的pom.xml文件。配置distributionManagement元素代码清单如下:

<project>
	...
	<distributionManagement>
		<repository>
			<id>proj-releases</id>
			<name>Proj Releases Repository</name>
			<url>http://192.168.1.100/content/repositories/proj-release</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表示该仓库的地址。
往远程仓库部署构件的时候,往往需要认证。配置认证的范式上节已讲过,简而言之就是要在setting.xml中配置server等内容。
配置正确后,在命令行输入:

mvn clean deploy

Maven就会将项目构建输出的构件部署到对应的远程仓库,如果项目当前的版本时快照版本,则部署到快照版本仓库地址,否则就部署到发布版本仓库地址。

快照版本

在Maven世界中,任何一个项目或者构件都必须有自己的版本。版本的值可能是1.0.0、1.3-alpha-4、2.0、2.1-SNAPSHOT等。其中,1.0.0/1。2-alpha-4等是稳定版本,而2.1-SNAPSHOT等是不稳定的快照版本。
Maven为什么要区分发布版和快照版本呢?试想一下这样的情景:小张在开发模块A的2.1版本,该版本还为正式发布,与模块A一同开发的还有模块B,它由小张的同事季MM开发,B的功能依赖于A。在开发过程中,小张需要经常将自己最新的构建输出,交给季MM,供她开发和调试,问题是,如何进行呢?

  1. 方案一:让季MM自己签出模块A的源代码进行构建。这种方式能够保证季MM得到模块A最新构建,不过他不得不去构建模块A。多了一些版本控制和Maven操作不算,当构建A失败的时候,她还是一头雾水,最后不得不找小张解决。显然,这种方法是最低效的。
  2. 方案二:重复部署模块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-20091212-121340-121,则复制其时间戳格式的文件至非时间戳格式,如SNAPSHOT,并使用该非时间戳格式的文件。
    当依赖的版本不明晰时,如RELEASE、LATEST和SNAPSHOT,Maven就需要基于更新远程仓库的更新策略来检查更新。用户也可以使用参数-U来自动的更新。

当Maven检查完更新策略,并决定检查更新依赖的时候,就需要检查仓库元数据maven-metadata.xml。
回顾一下前面提到的RELEASE和LATEST版本,他们分别对应了仓库中存在的该构件的最新发布版本和最新版本(包含快照),而这两个“最新”是基于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>
		<lastUpdate>20091214221557</lastUpdate>
	</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了,且MAVNE不会明确告诉用户这样的变化。当这种变化造成构建失败的时候,发现问题变得比较困难。RELEASE因为对应的是最新的发布版本,还相对可靠,LATEST就非常不可靠了,为此,MAVEN3不再支持在插件中配置使用LATEST和RELEASE。如果不设置插件版本,其效果和RELEASE一样,MAVEN只会解析最新的发布版本构件。
不过即使这样,也还是存在潜在的问题。例如,某个依赖的1.1版本和1.2版本可能发生一些接口的变化,从而导致当前Maven构建的失败。
当依赖版本设置为快照版本的时候,Maven也需要检查更新,这时,Maven会检查仓库元数据groupId/artifactId/version/maven-metadata.xml。
最后,仓库的元数据并不是永远正确的,有时候当用户发现无法解析某些构件,或者解析到错误构件时,就有可能发生了仓库元数据的错误,这时就需要手动的或者使用工具(如nexus)进行修复。

镜像

如果仓库X可以提供仓库Y存储的所有内容,那么就可以认为X是Y的一个镜像。换句话说,任何一个从仓库Y获得的构件,都能够从他的镜像中获取。举个例子,http://maven.net.cn/centent/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/centent/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.125.112/maven2/</url>
			<mirrorOf>*</mirrorOf>
		</mirror>
	</mirrors>
</settings>

该例中mirrorOf的值为星号,表示配置的是所有Maven仓库的镜像,任何对于远程仓库的请求都会被转至http://192.168.125.112/maven2/。如果该镜像需要认证,则需要配置一个id为internal-repository的server即可。
为了满足一些复杂的需求,Maven还支持一些高级的镜像配置:

  • < mirrorOf>*< /mirrorOf>:匹配所有的远程仓库
  • < mirrorOf>external:*< /mirrorOf>:匹配所有的远程仓库,使用localhost的除外,使用file://协议的除外。也就是说,匹配所有不在本机上的远程仓库。
  • < mirrorOf>repo1,repo2< /mirrorOf>:匹配repo1,repo2两个仓库,多个仓库之间用逗号隔开。
  • < mirrorOf>*,!repo1< /mirrorOf>:匹配所有远程仓库,repo1除外,使用感叹号将仓库从匹配中移除。

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

仓库搜索服务

使用Maven进行日常开发的时候,一个常见的问题就是如何寻找需要的依赖,我们可能只知道使用类库的项目名称,但添加Maven依赖要求提供确切的Maven坐标。这时,就可以使用仓库搜索服务来根据关键字得到Maven坐标。

Sonatype Nexus

地址:http://repository.sonatype.org/
Nexus是当前最流行的Maven仓库管理软件,这里要介绍的是Sonatype架设的一个公共Nexus仓库实例。
Nexus提供了关键字、类名搜索、坐标搜索、校验和搜索等功能。搜索后,页面清晰的列出了结果构件的坐标及所属仓库。用户可以直接下载相应的构件,还可以复制已经根据坐标自动生成的XML依赖声明。

Jarvana

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

MVNbrowser

地址:http://mvnbrowser.com
MVNbrowser只提供关键字搜索的功能,除了提供基于坐标的依赖声明代码片段等基本功能之外,MVNbrowser的一大特色就是,能够告诉用户该构件依赖于哪些其他构件以及该构件被那些其他构件所依赖。

MVNrepository

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

选择合适的仓库搜索服务

上述的四个仓库搜索服务都代理了主流的Maven公共仓库,如central、JBoss、java.net等。这些服务都提供了完备的检索、浏览、下载等功能,区别在于页面风格和额外功能。例如,Nexus提供了其他三种服务所没有的基于校验和搜索的功能。用户可以根据喜好和特殊需要选择合适自己的搜索服务,当然,也可以综合使用所有这些服务。

生命周期和插件

除了坐标、依赖和仓库以外,Maven另外两个核心概念就是生命周期和插件。在有关Maven的日常使用中,命令行的输入往往就对应了生命周期,如mvn package就表示执行默认的生命周期阶段package。Maven的生命中周期是抽象的,其实际行为都是由插件来完成,如package阶段可能就会由maven-jar-plugin完成。生命周期和插件两者协同完成,密不可分。

何为生命周期

在Maven出现之前,项目构建的生命周期就已经存在,项目开发人员每天都在对项目进行清理、编译、测试及部署。虽然大家都在不停的做构建工作,但公司和公司间、项目和项目间,往往使用不同的方式做类似的工作。有的项目以手工的方式在执行项目测试,有的项目以自动化脚本执行编译测试。可以想象的是,虽然各种手工方式十分类似,但不可能完全一样;同样的,对于自动化脚本,大家也是各写各的,能满足自身需求即可,换个项目就需要重头再来。
Maven的生命周期就是为了对所有构件过程进行抽象和统一。Maven从大量项目和构件工具中学习和反思,然后总结了一套高度完善的、以扩展的生命周期。这个生命周期包含了项目的清理、初始化、编译、测试、打包、集成测试、验证、部署和站点生成等几乎所有构建步骤。也就是说,几乎所有项目的构建,都能映射到这样的一个生命周期上。
Maven的生命周期是抽象的,这意味着生命周期本身做任何实际的工作。在Maven的设计中,实际的任务(如编译源码)都交由插件来完成。这种思想与设计模式中的模板方法(Template Method)非常的相似。模板方法在父类中定义算法和整体结构,子类可以通过实现或者重写父类的方法来控制实际的行为,这样既保证了算法有足够的可扩展性,又能严格控制算法的整体结构。如下的模板方法抽象类能很好的体现Maven生命周期的概念:

package com.juvenxu.mvnbook.template.method;
public abstract class AbstractBuild{
	public void build(){
		initialize();
		compile();
		test();
		packagee();
		integrationTest();
		deploy();
	}
	protected abstract void initalize();
	protected abstract void compile();
	protected abstract void test();
	protected abstract void packagee();
	protected abstract void integrationTest();
	protected abstract void deploy();
}

这段代码非常简单,build()方法定义了整个构件过程,依次初始化、编译、测试、打包(这里和java关键字冲突,所以用了packagee)、集成测试和部署,但是这个类中没有具体实现初始化、编译、测试等行为,他们都交由子类去实现。
虽然上述代码了Maven实际代码相去甚远,Maven的生命周期包含更多的步骤和更复杂的逻辑,但他们的基本理念是相同的。生命周期抽象了各个构建的步骤,定义了他们的次序,但没有提供具体实现。那么谁来实现这些步骤呢?不能为了让用户为了编译而编写一堆代码,为了测试又编写一堆代码那不就成大家在重复发明轮子了吗?Maven当然考虑到了这一点,所以它设计了插件机制。每个构建步骤都可以绑定一个或多个插件行为,而且Maven为大多数构建步骤编写并绑定了默认插件。例如,针对编译的插件有maven-compiler-plugin,针对测试的插件有maven-surefire-plugin等。虽然大多数时间里,用户几乎不会察觉到插件的存在,但实际上编译是由maven-compiler-plugin插件完成的。而测试是由maven-surefire-plugin插件完成的。当用户有特殊需求的时候,也可以配置插件定制构件行为,甚至自己编写插件。
Maven生命周期和插件机制一方面保证了所有的Maven项目有一致的构建标准,另一方面又通过默认插件简化和稳定了实际项目的构建。此外,该机制还提供了足够的扩展空间,用户可以通过配置现有的插件或者自行编写插件来自定义插件行为。

生命周期详解

上述只是介绍了Maven生命周期背后的思想,要想熟练的使用Maven,还必须详细了解其生命周期的具体定义和使用方式。

三套生命周期

初学者往往以为Maven生命周期是一个整体,其实不然,Maven拥有三套相互独立的生命周期,他们分别为clean、default和site。clean生命周期的目的是清理项目,default生命周期的目的是构建项目,而site生命周期的目的是建立项目站点。
每个生命周期包含一些阶段(phase),这些阶段是有顺序的,并且后面的阶段依赖于前面的阶段,用户和Maven最直接的交互方式就是调用这些生命阶段。以clean生命周期为例,它包含的生命阶段有pre-clean、clean和post-clean。当用户调用pre-clean的时候,只有pre-clean阶段得以执行;当用户调用clean的时候,pre-clean和clean会顺序执行;当用户调用post-clean的时候,pre-clean和clean、post-clean会顺序执行。
较之于生命周期阶段的前后依赖关系,三套生命周期本身是相互独立的,用户可以仅调用clean生命周期的某个阶段,或者仅仅调用default生命周期的某个阶段,而不会对其他生命周期产生任何影响。例如,当用户调用clean声明周期的clean阶段的时候,不会触发default生命周期的任何阶段,反之亦然,当用户调用default生命周期的compile阶段的时候,也不会触发clean生命周期的任何阶段。

clean生命周期

clean生命周期的目的是清理项目,他包含三个阶段。

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

default生命周期

default生命周期定义了真正构建时所需要执行的所有步骤,他是所有生命周期中最核心的部分,其包含的阶段如下,这里只对重要的部分进行解释:

  • validate
  • initialize
  • generate-sources
  • process-sources:处理项目资源文件。一般来说,是对src/main/resources目录的内容进行变量替换等工作后,复制到项目输出的classpath目录中。
  • generate-resources
  • process-resources
  • compile:编译项目的主源码。一般来是,是编译src/main/java目录下的Java文件至项目的输出的主classpath目录中。
  • process-classes
  • generate-test-sources
  • process-test-sources:处理项目测试资源文件。一般来说,是对src/test/resources目录的内容进行变量替换等工作后,复制项目输出的测试classpath目录中。
  • generate-test-resources
  • process-test-resources
  • test-compile:编译项目的测试代码。一般来说,是编译src/test/java目录下的Java文件至项目输出的测试classpath目录中。
  • process-test-classes
  • test:使用单元测试框架进行测试,测试代码不会被打包或者部署。
  • prepare-package
  • package:接收编译好的代码,打包成可发布的格式,如Jar。
  • pre-integration-test
  • integration-test
  • post-integration-test
  • verify
  • install:将包安装到本地Maven仓库,提供其他Maven项目使用。
  • deploy:将最终的包复制到远程仓库,供其他开发人员和Maven使用。

对于上述未加解释的阶段,读者也应该能根据名字大概猜到其用用途,若想进一步的了解这些阶段的详细信息,可以参阅官方的解释:http://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html。

site生命周期

site生命周期的目的是建立和发布项目站点,Maven能够基于POM所包含的信息,自动生成一个友好的站点,方便团队交流和发布项目信息。该生命周期包含如下阶段:

  • pre-site:执行一些在生成项目站点之前的需要完成的工作。
  • site:生成项目站点文档。
  • post-site:执行一些在生成项目站点之后需要完成的工作
  • site-depoly:将生成的项目站点发布到服务器上。

命令行与生命周期

从命令行执行Maven任何的主要方式是调用Maven的生命周期阶段。需要注意的是,各个生命周期是相互独立的,而一个生命周期的阶段是有前后依赖关系的。下面以一些常见的Maven命令为例,解释其执行的生命周期阶段:

  • $mvn clean:该命令调用clean生命周期的clean阶段。实际执行的阶段为clean生命周期的pre-clean和clean阶段。
  • $mvn test:该命令调用default生命周期的test阶段。执行该阶段的default生命周期为validate、initialize等,直到test的所有阶段。这也解释了为什么在执行测试的时候,项目的代码能够自动的编译。
  • $mvn clean install:该命令调用clean生命周期的clean阶段和default生命周期的install阶段。实际执行的阶段为clean生命周期的pre-clean、clean阶段,以及default生命周期从validate至install的所有阶段。该命令结合了两个生命周期,在执行真正的项目构建之前清理项目是一个很好的实践。
  • $mvn clean deploy site-deploy:该命令调用clean生命周期的clean阶段, default生命周期的deploy阶段,以及site生命周期的site-deploy阶段。实际执行的阶段为clean声明周期的pre-clean、clean阶段,default生命周期的所有阶段,以及site生命周期的所有阶段。该命令结合了Maven所有三个生命周期,且deploy为default生命周期的最后一个阶段,site-deploy为site生命周期的最后一个阶段

由于Maven中主要的生命周期阶段并不多,而常用的Maven命令实际都是基于这些阶段简单组合而成的,因此只要对Maven生命周期有一个基本的理解,就可以正确而熟练的使用Maven命令。

插件目标

Maven的生命周期与插件相互绑定,用以完成实际的构建任务。具体而言,是生命周期的阶段,与插件的目标相互绑定,以完成某个具体的构建任务。例如项目编译这一任务,他对应了default生命周期的compile这一阶段,而maven-compile-plugin这一插件的compile目标能够完成该任务。因此,将他们绑定,就能够实现项目编译的目的。

内置绑定

为了能让用户几乎不用任何配置就能构建Maven项目,Maven在核心为一些主要的生命周期绑定了很多的插件目标,当用户通过命令行调用生命周期阶段的时候,对应的插件目标就会执行相应的任务。
clean生命周期仅有pre-clean、clean和post-clean三个阶段,其中的clean与maven-clean-plugin:clean绑定。maven-clean-plugin仅有clean这一目标,其作用就是删除项目的输出目录。clean生命周期阶段与插件目标的绑定关系如下表

生命周期阶段插件目标
pre-cleanmaven-clean-plugin:clean
clean
post-clean
sit生命周期有pro-site、site、post-site和site-deploy四个阶段,其中,site和maven-site-plugin:site相互绑定,site-deploy和maven-site-plugin:deploy相互绑定。maven-site-pluging有很多目标,其中,site目标用来生成项目站点,deploy目标用来将项目站点部署到远程服务器上。site生命周期阶段与插件目标的绑定关系如下表:
生命周期阶段插件目标
pre-sitemaven-site-plugin:site
site
post-site
site-deploymaven-site-plugin:deploy
相对于clean和site的生命周期来说,default生命周期与插件目标的绑定关系就显得相对复杂一些。这是因为对与任何一个项目来说,例如jar项目和war项目,他们的项目清理和站点生成任务都是一样的,不过构建过程会有区别。例如jar项目需要打成jar包,而war项目需要打成war包。 由于项目的打包类型会影响到构件的具体过程,因此,default生命周期的阶段与插件目标的绑定关系由项目打包类型所决定,打包类型是通过POM中的packaging元素定义的。最常见、最重要的打包类型就是jar,它也是默认的打包类型。基于该打包类型,其default生命周期的内置插件绑定关系如下表:
生命周期阶段插件目标执行任务
process-resourcesmaven-resources-plugin:resources复制主资源文件至主输出目录
compilemaven-compiler-plugin:compile编译主代码至输出目录
process-test-resourcesmaven-resources-plugin:testResources复制测试资源文件至测试输出目录
test-compilemaven-compiler-plugin:testCompile编译测试代码至测试输出目录
testmaven-surefire-plugin:test执行测试用例
packagemaven-jar-plugin:jar创建项目jar包
installmaven-install-plugin:install将项目输出构件安装到本地目录
deploymaven-deploy-plugin:deploy将项目输出构件部署到远程仓库

注意,上表中只是列出了拥有插件绑定关系的阶段,default生命周期还有很多其他的阶段,默认他们没有绑定任何的插件,因为他们没有任何实际行为。
除了默认打包jar类型之外,常见的打包类型还有war、pom、maven-plugin、ear等。他的default生命周期与插件目标的绑定关系可以参阅Maven官方文档http://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html#Built-in_Lifecycle_Bindings

自定义绑定

除了内置绑定之外,用户还能够自己选择将某个插件目标绑定到生命周期的某个阶段上,这种自定义绑定方式能让Maven项目在构建的过程中执行更多更富特色的任务。
一个常见的例子是创建项目源码的jar包,内置的插件绑定关系中没有涉及这一任务,因此需要用户自行配置。maven-source-plugin可以帮助我们完成该任务,他的jar-no-fork目标能够将项目的主代码打成jar文件,可以将其绑定到default生命周期的verify阶段上,在执行完集成测试后和安装构件之前创建源码jar包。具体配置如下:

<build>
	<plugins>
		<plugin>
			<gourpId>org.apache.maven.plugins</groupId>
			<artifactId>maven-source-plugin</artifactId>
			<version>2.1.1</version>
			<executions>
				<id>attach-sources</id>
				<phase>verify<phase>
				<goals>
					<goal>jar-no-fork<goal>
				</goals>
			</executions>
		</plugin>
	</plugins>
</build>

在POM的build元素下的plugins子元素中声明插件的使用,该例中用到的是maven-source-plugin,其groupId为org.apache.maven.plugins,这也是Maven官方插件的groupId,紧接着是artifactId为maven-source-plugin,version是2.1.。对于自定义绑定的插件,用户总是应该声明一个非快照的版本,这样可以避免由于版本变化造成的构建不稳定性。
上述配置中,除了基本的插件坐标声明外,还有插件执行配置,executions下每个execution子元素可以用来配置执行一个任务。该例中配置了一个id为attach-sources的任务,通过phrase配置,将其绑定到verify生命周期阶段上,再通过goals配置指定要执行的插件目标。
有时候,即使不通过phase配置生命周期的阶段,插件目标也能够绑定到生命周期中去。例如,可以删除上述配置中的phase一行,再次执行mvn verify,仍能看到maven-source-plugin:jar-no-fork得以执行。出现这种情况的原因是:有很多插件目标在编写时已经定义了默认绑定阶段。可以使用maven-help-plugin查看插件的详细信息,了解插件目标的默认绑定阶段。运行命令如下:

$mvn hepl:describe-Dplugin = org.apache.maven.plugins:maven-source-plugin:2.1.1-Ddetail

我们知道,当插件目标被绑定到不同的生命周期阶段的时候,其执行顺序会由生命周期阶段的先后顺序决定。如果多个目标被绑定到同一个阶段,他们的执行顺序会怎么样呢?答案很简单,插件的声明顺序决定了目标的执行顺序。

插件配置

完成了插件和生命周期的绑定之后,用户还可以配置插件目标的参数,进一步调整插件所执行的任务,以满足任务需求。几乎所有Maven插件的目标都有一些可配置的参数,用户可以通过命令行和POM配置等方式来配置这些参数。

命令行插件配置

在日常的Maven使用中,我们会经常从命令行输入并执行Maven命令。在这种情况下,如果能够方便的更改某些插件的行为,无疑会十分方便。很多插件目标的参数都支持从命令行来配置,用户可以在Maven命令中使用-D参数,并伴随一个参数键=参数值的形式,来配置插件目标的参数。
例如,maven-surefire-plugin提供了一个maven.test.skip参数,当其值为true时,就会跳过执行测试。于是,在运行命令的时候,加上-D参数就能跳过测试:

$mvn install -Dmaven.test.skip=true

参数-D是java自带的,其功能是通过命令行设置一个Java系统属性,Maven简单的重用了该参数,在准备插件的时候检查系统属性,以便实现了插件的参数配置。

POM中插件全局配置

并不是所有的插件参数都适合从命令行配置,有些参数的值从项目创建到项目发布都不会改变,或者说改变很少,对于这种情况,在POM文件中一次性配置就显然比重复在命令行输入要方便。
用户可以在声明插件的时候,对此插件进行一个全局的配置。也就是说,所有基于该插件目标的任务,都会使用这些配置。例如,我们通常会需要配置mavne-compiler-plugin告诉他编译java1.5版本的源文件,生成JVM1.5兼容的字节码文件。见代码清单如下:

<build>
	<plugins>
		<plugin>
			<groupId>org.apache.maven.plugins</groupId>
			<artifact>mavne-compiler-plugin</artifact>
			<version>2.1</version>
			<configuration>
				<source>1.5</source>
				<target>1.5</target>
			</configuration>
		</plugin>
	</plugins>
</build>

这样,不管绑定到compile阶段的maven-compiler-plugin:compile任务,还是绑定到test-compiler阶段的maven-compiler-plugin:testCompiler任务,就都能使用该配置,基于java1.5进行编译。

POM中插件任务配置

除了为插件配置全局参数,用户还可以为某个插件任务配置特定的参数。以maven-antrun-plugin为例,它有一个目标run,可以用来在Maven中调用Ant任务。用户将maven-antrun-plugin:run绑定到多个生命周期阶段上,在加以不同的配置,就可以让Maven在不同的生命周期阶段执行不同的任务。代码清单如下:

<build>
	<plugins>
		<plugin>
			<groupId>org.apache.maven.plugins</groupId>
			<artifactId>maven-antrun-plugin</artifactId>
			<version>1.3</version>
			<executions>
				<execution>
					<id>ant-validate</id>
					<phase>validate</phase>
					<goals>
						<goal>run</goal>
					</goals>
					<configuration>
						<tasks>
							<echo>I'm bound to validate phase</echo>
						</tasks>
					</configuration>
				</execution>
				<execution>
					<id>ant-verfiy</id>
					<phase>verfiy</phase>
					<goals>
						<goal>run</goal>
					</goals>
					<configuration>
						<tasks>
							<echo>I'm bound to verfiy phase</echo>
						</tasks>
					</configuration>
				</execution>
			</executions>
		</plugin>
	</plugins>
</build>

在上述代码中,首先,maven-antrun-plugin:run与validate阶段绑定,从而构成一个id为ant-validate的任务。插件全局配置中的configuration元素位于plugin元素下面,而这里的configuration元素则位于execution元素下,表示这是特定任务的配置,而非插件整体的配置。这个ant-validate任务配置了一个echo Ant任务,向命令行输出一段文字、表示该任务是绑定到validate阶段的。第二个任务的id为ant-verfiy,他绑定到了verfiy阶段,同样也向命令行输出一段文字,告诉该任务绑定到了verfiy阶段。

获取插件信息

仅仅理解如何配置使用插件是远远不够的。当遇到一个构建任务的时候,用户还需要知道去哪里寻找合适的插件,以帮助完成任务。找到正确的插件之后,还要详细了解该插件的配置点。由于Maven的插件非常多,而且这其中的大部分没有完善的文档,因此,使用正确的插件进行正确的配置,其实并不是一件容易的事情。

在线插件信息

基本上所有主要的Maven插件都来自Apache和Codehaus。由于Maven本身是属于Apache软件基金会的,因此他们有很多的官方的插件,每天都有成千上万个Maven用户在使用这些插件,它们具有非常好的稳定性。详细的列表可以在这个地址找到:http://maven.apache.org/plugins/index.html,单击某个插件的链接便可以得到进一步的信息。所有官方插件能在这里下载:http://repo1.maven.org/maven2/org/apache/maven/plugins/。
除了Apache官方插件之外,托管于Codehaus上的Mojo项目也提供了大量的Maven插件,详细列表可以访问:http://mojo.codehaus.org/plugins.html。需要注意的是,这些插件的文档和可靠性相对较差,在使用时,如果遇到什么问题,往往只能自己看源码。所有的Codehaus的Maven插件能在这里下载:http://repository.codehaus.org/org/codehaus/mojo/。

插件解析机制

为了方便用户使用和配置插件,Maven不需要用户提供完整的插件坐标信息,就可以解析得到正确的插件,Maven的这一特性是一把双刃剑,虽然简化了使用和配置,可一旦插件出现什么问题,用户就很难定位出问题的插件。

插件仓库

与依赖构件一样,插件构件同样基于坐标存储在Maven仓库中。在需要的时候,Maven会从本地仓库去寻找插件,如果不存在,则从远程仓库找。找到插件之后再下载到本地使用。
值得一提的是,Maven会区别对待依赖的远程仓库和插件的远程仓库。不同于repositories及其repository子元素,插件的远程仓库使用plugRepositories和pluginRepository配置。
一般来说,中央仓库所包含的插件完全能够满足我们的需要,因此也不需要配置其他的远程仓库。只有在很少数的情况下,项目所使用的插件无法在中央仓库找到,这个时候可以跟Maven依赖一样在POM或者settings.xml中加入其他的仓库配置。

插件默认的groupId

在POM中配置插件的时候,如果该插件是Maven的官方插件(即groupId为maven.apache.org.plugins),就可以省略groupId配置,代码清单如下:

<build>
	<plugins>
		<artifactId>maven-compiler-plugin</artifactId>
		<version>2.1</plugin>
		<configuration>
			<source>1.5</source>
			<target>1.5</target>
		</configuration>
	</plugins>
</build>

上述配置中省略了maven-compiler-plugin的groupId,Maven在解析该插件的时候,会自动用默认的maven.apache.org.plugin补齐。

解析插件版本

同样是为了化简插件的配置和使用,在用户没有提供插件版本的情况下, Maven会自动的解析插件版本。
首先,Maven在超级POM中为所有核心插件设定了版本,超级POM是所有的Maven项目的父POM,所有项目都继承这个超级POM的配置,因此,即使用户不加任何的配置,Maven使用核心插件的时候,他们的版本就已经确定了。这些插件包括maven-clean-plugin、maven-compiler-plugin、maven-surefire-plugin等。
如果用户使用某个插件的时候没有版本,而这个插件又不属于核心插件的范畴,Maven就会去检查所有仓库中可用的版本,然后做出选择。
Maven遍历本地仓库和所有的远程仓库,将该路径下的仓库库元数据归并后,就能计算出latest和release的值。当用户非核心插件没有声明版本时,Maven会解析所有可用仓库中的最新的版本。

解析插件前缀

前面讲到mvn命令行支持使用插件前缀来简化插件的调用,现在来解释一下Maven如何根据插件前缀解析得到插件的坐标。
插件前缀与groupId:artifactId是一一对应的。这种匹配关系存储在仓库元数据中。与之前提到的groupId/artifactId/maven-metadata.xml不同,这里的仓库元数据为groupId/maven-metadata.xml。

聚合与依赖

软件的飞速发展,各类用户对软件的要求越来越高,软件本身也变得越来越复杂。因此,设计人员往往会采用各种方式对软件区分模块,以得到更加清晰的设计以及更高的重用性。当把Maven应用到实际项目中的时候,也需要将项目分成不同的模块。Maven的聚合特性能够把项目的各个模块聚合一起构建,而Maven的继承特性则能帮助抽取各个模块相同的依赖和插件等配置,在简化POM同时,还能促进各个模块配置的一致性。

聚合

我们想要一次构建两个项目,而不是两个模块的目录下分别执行mvn命令。Maven聚合(或者称为多模块)这一特性就是为该需求服务的。

继承

在面向对象的世界中,程序员可以建立一种类的父子结构,然后在父类中声明一些字段和方法供子类继承,这样就可以做到“一出声明,多处使用”。类似的,我们需要创建POM的父子结构,然后在父类POM中声明一些配置供子类POM继承,以实现“一处声明,多处使用”的目的。

可继承的POM元素

一下是可继承的POM元素列表

  • groupId:项目组ID,项目坐标的核心元素。
  • version:项目版本,项目坐标的核心元素。
  • description:项目的描述信息。
  • organization:项目的组织信息。
  • inceptionYear:项目的创建年份。
  • url:项目的URL地址。
  • developers:项目开发者信息。
  • contributors:贡献者信息。
  • contributionManagerment:项目部署配置。
  • issueManagerment:项目缺陷跟踪系统信息。
  • ciManagerment:项目持续继承系统信息。
  • scm:项目的版本控制系统信息。
  • mailingLists:项目邮件列表信息。
  • properties:自定义的Maven属性。
  • dependencies:项目的依赖配置。
  • dependencyManagment:项目依赖管理配置。
  • repositories:项目的仓库配置。
  • build:包括项目的源码目录配置、输出目录配置、插件配置、插件管理配置等。
  • repoting:包括项目报告输出配置、插件报告输出配置等。

依赖管理

依赖是会被继承的,把依赖配置放到父模块中,多个子模块中就能移除重复的依赖,简化配置。

约定优于配置

标准的重要性已经不用过多强调,想象一下,如果不是所有的程序员都基于HTTP协议开发web应用,互联网会乱成什么样。各个版本的IE、FireFox等浏览器之间的差异已经让开发者头痛不已。而Java的成功的重要原因之一就是他能屏蔽大部分操作系统之间的差异,XML流行的原因之一是所有语言都接受他。Maven当然还不能和这些成熟的技术相比,当Maven的用户都应该清楚,Maven提倡“约定优于配置”(Conversion Over Configuration),这是Maven最核心的设计理念之一。
为什么要是用约定而不是自己更加灵活的配置呢?原因之一是,使用约定可以大量减少配合。先看一个简单的Ant配置文件,如下:

<project name="my-project" default="dist" basdir=".">
	<discription>simple example build file</discription>
	<!- 设置全局属性 -->
	<properties name="src" location="src/main/java"/>
	<properties name="build" location="target/classes"/>
	<properties name="dist" location="target"/>
	
	<target name="init">
		<!- 创建时间戳 -->
		<mkdir dir="${build}"/>
	</target>
	
	<target name="compile" depends="init" discription="compile the source">
		<!- 将java代码从目录“${src}”编译至“${build}” -->
		<javac srcdir="${src}" destdir="${build}"/>
	</target>

	<target name="dist" depends="compile" discription="generate the distribution">
		<!- 创建分发目录 -->
		<mkdir dir="${dist}/lib"/>

		<!- 将${build}目录的所有内容打包至MyProject-${DSTAMP}.jar file -->
		<jar jarfile="${dist}/lib/MyProject-${DSTAMP}.jar" basedir="${build}"/>	
	</target>

	<target name="clean" distribution="clean up">
		<!- 删除${build}和${dist}目录树 -->
		<delete dir="${build}"/>
		<delete dir="${dist}"/>
	</target>
</project>

这段代码做的事情就是清除构建目录、创建目录、编译代码、复制依赖至目标目录,最后打包。这是一个项目构建要完成的最基本的事情,不过为此还是需要很多的XML配置:源码目录是什么、编译目录是什么、分发目录是什么等等。用户还要记住各种Ant任务命令,如delete、mkdir、javac和jar等等。
做同样的事情,Maven需要配置什么呢?Maven只需要一个最简单的POM,如下:

<project>
	<modelVersion>4.0.0</modelVersion>
	<groupId>com.juvenxu.mvnbook</groupId>
	<artifactId>my-project</artifactId>
	<version>1.0</version>
</project>

这段配置简单的令人惊奇,但为了获得这样简单的配置,用户是需要一定代价的,那就是遵循Maven约定。Maven会假设用户的项目是这样的:

  • 源码目录为:src/main/java
  • 编译输出目录为:target/classes/
  • 打包方式为:jar
  • 包输出目录为:target/

遵循约定虽然失去了一定的灵活性,用户不能随意安排目录结构,但是却能减少配置。更重要的是,遵循约定能够帮助用户遵守构建标准。
如果没有约定,10个项目可能有10种不同的项目目录结构,这意味着交流学习成本的增加,当新成员加入的时候,他就不得不花时间去学习这种构建配置。而有了Maven的约定,大家都知道什么目录放什么内容,此外,与Ant的自定义项目名称不同,Maven在命令行暴露的用户接口是统一的,像mvn clean install这样的命令可以用来构建几乎任何的Maven项目。
Maven也允许自定义不尊少Maven的约定,但这回增加混乱,如果没有必要,大可不必修改。Maven的约定在超级POM中,任何一个Maven项目都隐式的继承自超级POM,这点有点像java类都继承Object类一样。
Maven3中,超级POM位于$MAVEN_HOME/lib/maven-model-builder.x.x.x.jar中,的org/apache/maven/model/pom-4.0.0.xml路径下。

反应堆

在一个多模块Maven项目中,反应堆(Reactor)是指所有的模块组成一个构件结构。对于单模块的项目,反应堆本身就是该模块本身,但是对于多模块项目来说,反应堆包含了各模块之间继承与依赖的关系,从而能够计算出合理的模块构建顺序。

反应堆的构建顺序

Maven实际的构建顺序是这样形成的:Maven先按顺序读取POM,如果该POM中没有依赖,那么就先构建该模块,否则就先构建其依赖的模块,如果该依赖还依赖其他的模块,则进一步构建依赖的依赖。
当出现模块A依赖于模块B,而B又依赖于模块A时,Maven就会报错。

裁剪反应堆

一般来说,用户会选择构建整个项目或者选择构建单个模块,但有的时候,用户会想要仅仅构建完整反应堆中的某些模块。换句话说,用户需要实时的裁剪反应堆。
Maven提供很多的命令行选项支持反应堆,输入mvn -h可以看到这些选项:

  • am, --also-make 同时构建所有模块的依赖模块
  • amd -also-make-dependents 同时构建依赖于所有模块的模块
  • -pl,–project< arg > 构件指定的模块,模块间用逗号分隔
  • rm -resume-from< arg>指定的模块回复反应堆

使用Nexus创建私服

私服不是Maven的核心概念,它仅仅是一种衍生出来的特殊的maven仓库,通过建立自己的私服,就可以降低中央仓库的负荷、节省外网带宽、加速maven构建、自己部署构建等,从而高效的使用Maven。

Nexus简介

2005年12月,Tamas Cservenak由于受不了匈牙利电信ADSL的低速度,开始着手开发Proximity——一个很简单的Web应用。他可以代理并缓存Maven构件,当Maven需要下载构件的时候,就不需要反复依赖于ADSL。到2007年,Sonatype邀请Tamas参与创建一个更酷的Maven仓库管理软件,这就是后来的Nexus。
nexus分为开源版和社区版,其中开源版基于GPLv3许可证,其特性以满足大部分Maven用户的需求。一下是Nexus开源版本的特性:

  • 较少的内存占用(最少仅占用28MB)
  • 基于ExtJS的友好界面
  • 基于Restlet的完全REST API
  • 支持代理仓库、宿主仓库和仓库组
  • 基于文件系统,不需要数据库
  • 支持仓库索引和搜索
  • 支持从界面上传Maven构件
  • 细粒度的安全控制

Nexus专业版是需要付费购买的,除了开源版本的所有特色之外,他主要包含一些企业安全控制、发布流程控制等需要的特性。官方网址:http://www.sonatype.com/products/nexus/community

安装Nexus

Nexus是典型的Java Web应用,它有两种安装包,一种是包含Jetty容器的Bundle包,另一种是不包含Web容器的war包。

下载Nexus

官网:http://nexus.sonatype.org/downloads/下载最新版本的Nexus。

Bundle方式安装Nexus

Nexus的Bundle自带Jetty容器,因此用户不需要额外的web容器就能启动Nexus。首先将Bundle文件解压,这时就会得到两个目录:

  • nexus-webapp-x.x.x/: 该目录包含了Nexus运行所需的文件,如启动脚本、依赖jar包等。
  • sonatype-work:该目录包含Nexus生成的配置文件、日志文件、仓库文件等。

其中,第一个目录是运行Nexus所必须的,而且所有相同版本Nexus实例所包含的该目录内容都是一样的。而第二个目录不是必须的,Nexus会在运行的时候动态创建该目录,不过他的内容对于各个Nexus实例是不一样的,因为不同用户在不同的机器上使用的Nexus会有不同的配置和仓库内容。当用户需要备份Nexus的时候,默认备份sonatype-work/目录,因为该目录包含了用户特定的内容,而nexus-wapapps-x.x.x目录下的内容是可以直接从安装包获得的。
在windows操作系统上,用户进入nexus-webapps-x.x.x/bin/jsw/windows-x86-32/子目录,然后运行nexus.bat脚本就能启动nexus。
这时,访问http://localhost:8081/nexus就能看到nexus界面,要停止nexus,可以在控制台按ctrl+c。
在nexus-webapps-x.x.x/bin/jsw/windows-x86-32/子目录下还有一些其他的脚本:

  • Installnexus.bat:将nexus安装成Windows服务。
  • Uninstallnexus.bat:卸载nexus服务。
  • Startnexus.bat:启动nexus服务。
  • Stopnexus.bat:停止nexus windows服务。
  • Pausenexus.bat:暂停nexus windows服务。
  • Restartnexus.bat:恢复暂停的Nexus windows服务。

借助windows服务,用户就可以让Nexus伴随着windows自动启动,非常方便。

war方式启动nexus

除了Bundle,Nexus还提供了一个可以直接运行在web服务器上的war包。该war包支持主流的服务器,如tomcat、classfish、Jetty、Resin等。

登录Nexus

Nexus拥有全面的权限控制功能,默认的Nexus的访问都是匿名的,而匿名用户仅包含一些最基本的权限,要全面的学习使用Nexus,就必须以管理员的方式登录。默认的账号密码为admin/admin123。

Nexus仓库与仓库组

作为Maven仓库服务软件,仓库自然是Nexus中最重要的概念。Nexus包含了各种类型的仓库概念,包括代理仓库、宿主仓库和仓库组等。每一种仓库都提供了丰富使用的配置参数,方便用户根据需要进行定制。

Nexus内置的仓库

在具体介绍每一种类型的仓库之前,先浏览一下Nexus的内置仓库,单机Nexus界面左边的Repositories链接,可以看到所有类型的Nexus仓库。
仓库包含四种类型:

  • group:仓库组
  • hosted:宿主
  • proxy:代理
  • virtual:虚拟

解释一下各个仓库的内容:

  • Maven Central:该仓库代理Maven中央仓库,其策略为Release,因此只会下载和缓存中央仓库的发布版本构件。
  • Releases:这是一个策略为Release的宿主类型仓库,用来部署组织内部的快照版本构件。
  • 3rd party:这是一个策略为Release的宿主类型仓库,用来部署无法从公共仓库获得的第三方发布版本的构件。
  • Apache Snapshots:这是一个策略为snapshot的代理仓库,用来代理Apache Maven仓库的快照版本构件。
  • Codehaus Snapshot:这是一个策略为snapshot的代理仓库,用来代理Codehaus Maven仓库的快照版本构件。
  • Google Code:这是一个策略为Release的代理仓库,用来代理Google Code Maven仓库的发布版本构件。
  • java.net-Maven2:这是一个策略为Release的代理仓库,用来代理java.net Maven仓库的发布版本构件。
  • Public Repositories:该仓库将上述所有策略为Release的仓库聚合并通过一致的地址提供服务。
  • Public Snapshot Repositories:该仓库将上述所有策略为Snapshot的仓库聚合并通过一致的地址提供服务。

使用Maven进行测试

随着敏捷开发模式的日益横行,软件开发人员也越来越意识到日常编程工作中单元测试的重要性。Maven的重要职责之一就是自动运行单元测试,他通过maven-surefire-plugin与主流的单元测试框架Junit3、Junit4以及TestNG集成,并能够自动生成丰富的结果报告。
通过Maven可以调用JUnit和TestNG等框架对项目进行测试,非常的方便。Maven的test命令中还可以通过参数非常灵活的调节测试的类和方法,也可以生成测试报告、执行动态测试等功能。

使用Hudson进行持续集成

持续集成的作用、过程和优势

简单的说,持续集成就是快速且高效地自动构建项目的所有源码,并为项目成员提供丰富的反馈信息。这句话有很多的关键字:

  • 快速:集成的速度要尽可能的快,开发人员不希望自己的代码提交半天之后才得到反馈。
  • 高频率:频率越高越好,例如每隔一个小时就是一个不错的选择,这样问题才能被很好的反应出来。
  • 自动:持续集成应该是自动触发执行的,不应该有手动参与。
  • 构建:包括编译、测试、审核、打包、部署等工作。
  • 所有源码:所有团队成员提交到代码库里的最新源代码。
  • 反馈:持续集成应该通过各种快捷的方式告诉团队成员最新的集成状态,当集成失败的时候,反馈报告应该尽可能的反应失败的具体细节。

一个典型的继承场景是这样的:开发人员对代码做了一些修改,在本地运行构建确认无误后,将代码提交到代码库。具有高配置硬件的持续集成服务器每隔30分钟查询代码库一次,发现更新后,签出所有最新的代码,将调用自动化构建工具(如Maven)构建项目,该过程包括编译、测试、审核、打包和部署等。然而不幸的是,另外一名开发人员在这一时间段也提交了代码更改,两处更改导致了某些代码的失败,持续集成服务器基于这些失败的测试创建了一个报告,并自动发给相关开发人员。开发人员接到报告后,立即周守调查原因,并尽快修复。
一次完整的集成往往包含以下6个步骤:

  1. 持续编译:所有正式的源代码都应该提交到代码控制系统中(如Subversion),持续集成服务器按一定的频率检查源码控制系统,如果有新的代码,就触发一次集成,旧的已编译的自己码应当全部清除,然后服务器编译所有的最新源码。
  2. 持续数据库集成:在很多项目中,源代码不仅仅指Java代码,还包括数据库SQL脚本,如果单独管理他们,很容易造成项目与其他代码不一致,并造成混乱。持续集成也应该包括数据库的集成,每次发现新的SQL脚本,就应该清理集成环境的数据库,重新创建表结构,并填入预备的数据。这样就能随时发现脚本的错误,此外,这些脚本的测试还能进一步发现其他相关问题。
  3. 持续测试:有了JUnit之类的框架,自动化测试就成了可能。编写优良的单元测试并不容易,好的单元测试必须是自动化的、可重复执行的、不依赖于环境的,并且能够自我检查的。除了单元测试,有些项目还会包含一些依赖外部环境的集成测试。所有这些测试都应该在每次集成的时时候运行,并且在发生问题的时候差生具体的报告。
  4. 持续审查:诸如Checkstyle和PMD之类的工具能够帮助我们发现代码中的坏味道(Bad Small),持续集成可以使这类工具来生成各类报告,如覆盖率报告、Checkstyle报告、PMD报告等。这些报告生成的频率可以低一些,如每天生成一次,当审查发现问题的时候,可以给开发人员反馈警告信息。
  5. 持续部署:持续集成的最后一步的反馈,通常是一封电子邮件。在重要的时候将正确的信息发送给正确的人。如果开发者一直收到与自己无关的持续集成报告,他慢慢的就会忽略这些报告。基本的规则是:将集成报告发送给这次集成相关的代码提交者,项目经理应该收到所有的失败报告。

持续集成需要额外的引入硬件设置,特别是对于集成服务器来说,性能越高,集成的速度就越快,反馈的速度也就越快。持续集成还需要开发者使用工具,如源码控制工具、自动化构建工具、自动化检测工具、持续集成软件等。这一切无疑都增加了开发者的负担,然而学习并适应这些开发工具及流程是完全值得的,因为持续集成有着很多好处:

  • 尽早暴露问题:越早的暴露问题,修复问题代码的成本就越低。持续集成高频率的编译、测试、审核、打包和部署项目代码,能够快速的发现问题并及时反馈。
  • 减少重复操作:持续集成是完全自动化的,这就避免了大量的重复的手工劳动,开发人员不再需要手动的去签出源码,一步步的编译、测试、审核、部署。
  • 简化项目发布:每日高频的集成保证了项目随时都是可部署运行的,如果没有持续集成,项目发布之前将不得不手动的集成,然后花费大量的精力修复集成问题。
  • 建立团队信心:一个优良的持续集成环境能让团队随时对项目的状态保持信息,因为项目的大部分问题区域已经由持续集成的环境覆盖了。

使用Maven构建Web项目

在现今的互联网时代,我们创建的大部分应用程序都是Web应用,在Java世界中,Web项目的标准打包方式就是war。

Web项目的目录结构

我们都知道,基于Java的Web应用,其标准的打包方式是WAR。WAR与JAR类似,只不过他可以包含更多的内容,如JSP文件、Servlet类、Java类、web.xml文件、依赖jar包、静态web资源(HTML、CSS、JS)等。一个典型的war文件会有如下的目录结构:

war/
	META-INF/
	WEB-INF/
		classes/
			ServletA.class
			ServletB.class
			config.properties
			...
		lib/
			dom4j-1.3.4.jar
			mail-1.3.5.jar
			...
		web.xml
	img/
	css/
	js/
	index.html
	sample.jsp

一个war包下至少包含两个子目录:META-INF和WEB-INF。前者包含了一些打包元数据信息,我们一般不去关心;后者包含war包的核心,WEB-INF下必须包含一个Web资源表述文件web.xml,他的子目录classes包含所有该web项目的类,而另一个目录lib则包含所有该web项目的依赖jar包,classese和lib目录在运行的时候都会被加入到classpath中。除了META-INF和WEB-INF之外,一般的war包都会包含很多的web资源,例如往往可以在war包的根目录下看到很多的HTML或者jsp文件。此外,还能看到一些文件夹如img、css和js,他们会包含对应的文件供页面使用。
同其他的Maven项目一样,Maven对web项目的布局结构也有一个通用的约定。不过首先要记住的是,用户必须为web项目显示的指定打包方式为war,代码清单如下:

<project>
	...
	<groupId>com.juvenxu.mvnbook</groupId>
	<artifactId>sample-war<artifactId>
	<version>1.0-SNAPSHOT</version>
	<packaging>war</packaging>
	...
</project>

如果不显示的指定packaging,Maven会使用默认的jar作为打包方式,从而导致无法正确的打包web项目。
web项目的类及资源文件痛jar项目一样,默认的位置都是src/main/java和src/main/resource,测试类及测试资源文件默认位置是src/test/java和src/test/resource。web项目比较特殊的一个地方在于:他还有一个web资源目录,其默认位置是:src/main/webapp。一个典型的web项目的Maven目录结构如下:

project
	pom.xml
	src/
		main/
			java/
				servletA.java
				servletB.java
				...
			resources/
				config.properties
				...
			webapp/
				WEB-INF/
					web.xml
				img/
				css/
				js/
				index.html
				sample.jsp
		test/
			java/
			resource/

在src/main/webapp下,必须包含一个子目录WEB-INF,该子目录还必须包含web.xml文件。src/main/webapp目录下的其他文件和目录包括HTML、jsp、css、js等,他们与war包中的web资源完全一致。
在使用Maven创建web项目之前,必须理解这种Maven项目目录结构和war包结构的对应关系。有一点需要注意的是,war包中有一个lib目录包含所有的jar包,但Maven项目结构中没有这样的一个目录,这是因为一起依赖都配置在POM中,Maven在用war方式打包的时候会根据POM的配置从本地仓库复制响应的jar文件。

版本管理

一个健康的项目通常有一个长期、合理的版本演变过程。例如JUnit有3.7、3.8、3.8.1、3.8.2、4.0等版本。Maven本身的版本也比较多,如最早的Maven1;目前使用最广泛的Maven2有2.0.9、2.0.10、2.1.0、2.2.0、2.2.1等各种版本;而最新的Maven3有3.0-alpha-1、3.0-alpha-2、3.0-alpha-7、3.0-beta-1等版本。除了这些对外发布的版本之外,还有一些快照版本之类的。
另外,还需要分清版本管理(Version Management)和版本控制(Version Control)之间的区别。版本管理是指项目整体版本的演变过程管理,如从1.0-SNAPSHOT到1.1-SNAPSHOT,再到1.2-SNAPSHOT等。版本控制是指借助版本控制工具(如Subversion)追踪代码的每一个变更。

何为版本管理

为了方便团队的合作,在项目的开发过程中,大家都应该使用快照版本,Maven能够智能的处理这些特殊的版本,解析项目各个模块最新的“快照”。快照版本机制促进团队内部的交流,但是当项目需要对外发布时,我们显然需要提供非常稳定的版本,使用该版本应该永远只能定位到唯一的构件,而不是像快照那样,定位的构件随时间的变化可能发生变化。对应的,我们称这类稳定的版本为发布版本。项目发布了一个版本之后,就进入了下一个阶段开发阶段,项目自然的就转到了新的快照版本中了。
版本管理关心的问题之一就是这种快照版本和发布版本之间的转换。项目经历了一段时间的1.0-SNAPSHOT的开发之后,在某个时刻发布了1.0正式版,然后进入了1.1-SNAPSHOT的开发,这个版本可能添加了一些有趣的特性,然后在某个时刻发布1.1正式版。
理想的发布版本应该对应项目某个时刻比较稳定的状态,包括源代码的状态以及构建的状态,因此这个时候项目的构建应当满足一下的要求:

  • 所有自动化测试应当全部通过:毫无疑问,失败的测试代表了需要修复的问题因此发布版本之前应该确保所有测试都能得以正确执行。
  • 项目没有配置任何快照版本的依赖:快照版本的依赖意味着不同时间的构建可能会引入不同内容的依赖,这显然不能保证多次构建能够生成相同的结果。
  • 没有配置任何相同版本的插件:快照版本的插件配置可能会在不同的时间引入不同内容的Maven插件,从而影响Maven行为,破坏构建的稳定性。
  • 项目所有的代码已经提交到版本控制系统中:项目已经发布了,可源码却不在版本控制系统中,甚至丢失了。这意味着项目丢失了某个时刻的状态,因此这种情况必须避免,版本发布的时候必须确保所有的源代码都已经提交了。

灵活的构建

一个优秀的构建系统必须足够灵活,他应该能够让项目在不同的环境下都能够成功的构件。例如,典型的项目都会有开发环境、测试环境和产品环境,这些环境的数据库配置不尽相同,那么项目构建的时候就需要能够识别所在的环境并正确的配置。还有一种常见的情况是,项目开发了大量的集成测试,这些测试运行起来非常耗时,不适合在每次构建项目的时候都运行,因此需要一种手段让我们在特定的时候才激活这些测试。Maven为了支持构建的灵活性,内置了三大特性,即属性、profile和资源过滤。

生成项目站点

Maven不仅仅是一个自动化构建工具和一个依赖管理工具,它还能够帮助聚合项目信息,促进团队间交流。POM可以包含各种信息,如项目描述、版本控制系统地址、缺陷跟踪系统地址、认证许可信息、开发者信息等。用户可以让Maven自动生成一个站点,以web的形式发布这些信息。此外,Maven社区提供了大量的插件,能够让用户生成各种各样的审查报告,包括测试覆盖率、静态代码分析、代码变更等。

编写Maven插件

Maven的任何行为都是由插件完成的,包括项目的清理、编译、测试以及打包等操作都有其对应的Maven插件。每个插件拥有一个或者多个目标,用户可以直接从命令行运行这些插件目标,或者选择将目标绑定到Maven的生命周期。
大量的插件可以从Apache和Codehaus获得,这里的近百个插件几乎能满足所有Maven项目的需求。除此之外,还有很多Maven插件分布在Googlecode、Sourceforge、Github等项目托管服务中。因此当你发现自己有特殊需求的时候,首先应该搜索一下看是否已经有现成的插件可供使用。例如,如果想要配置Maven自动为所有Java文件的头部添加许可证声明,那么通过关键字maven plugin lisense找到maven-lisense-plugin,这个托管在Goolecode上的项目完全能够满足我们的需求。
在一些及其特殊的情况下,也可以编写自己的插件。

编写插件的一般步骤

  1. 创建一个maven-plugin项目:插件本身也是一个Maven项目,特殊的地方在于他的packaging必须是maven-plugin,用户可以使用maven-archetype-plugin快速创建一个Maven插件项目。
  2. 为插件编写目标:每个插件都必须包含一个或者多个目标,Maven称之为Mojo(与POJO对应,后者指Plain Old Java Object,这里指的是Maven Old Java Object)。编写插件的时候,必须提供一个或者多个继承自AbstractMojo的类。
  3. 为目标提供配置点:大部分的Maven插件及其目标都是可配置的,因此在编写Mojo的时候需要注意提供可配置的参数。
  4. 编写代码实现目标行为:根据实际的需要实现Mojo。
  5. 错误处理及日志:当Mojo发生错误的时候,根据情况控制Maven的运行状态。在代码中编写必要的日志以便为用户提供足够的信息。
  6. 测试插件:编写自动化的测试代码测试行为,然后再根据实际运行插件及验证其行为。

案例:编写一个用于代码统计的Maven插件

下面编写一个用于代码统计的Maven插件,使用该插件,可以了解到Maven项目各个源码目录下的文件数量,以及他们加起来有多少行。
要创建一个Maven项目,首先使用maven-archetype-plugin命令:

mvn archetype:generate

然后选择:
maven-archetype-plugin(An archetype which contains a sample Maven plugin。)
输入maven坐标信息之后,一个Maven插件项目就创建好了。打开项目的pom.xml可以看到代码清单如下:

<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.juvenxu.book</groupId>
	<artifactId>maven-loc-plugin</artifactId>
	<packaging>maven-plugin</packaging>
	<version>1.0-SNAPSHOT</version>
	<name>Maven LOC Plugin</name>
	<url>http://www.juvenxu.com</usl>

	<properties>
		<maven.version>3.0</maven.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.apache.maven</groupId>
			<artifactId>maven-plugin-api</artifactId>
			<version>${maven.version}</version>
		</dependency>
	</dependencies>
</project>

Maven插件项目的POM有两个特殊的地方:

  1. 它的packaging必须是maven-plugin,这种特殊的打包类型能控制Maven为其在生命周期阶段绑定插件处理相关的目标,例如在compile阶段,Maven需要为项目构建一个特殊的插件描述文件。
  2. 从上述代码中可以看到一个artifactId为maven-plugin-api的依赖,该依赖中包含了插件开发所需要的类,例如稍后会看到的AbstractMojo。需要注意的是,代码清单中并没有使用默认的Archetype生成maven-plugin-api版本,而是升级到了3.0,这样做的目的是与Maven的版本保持一致。

插件项目创建好了之后,下一步是为插件编写目标。使用Archetype生成的插件项目包含一个名为MyMojo的java文件,我们将其删除,然后创建一个CountMojo。代码如下:

/**
* @goal count
*/
public class CountMojo extends AbstractMojo{
	private static final String[] INCLUDES_DEFAULT={"java","xml","properties"};

	private File basedir;
	
	private File sourceDirectory;

	private File testSourceDirectory;
	
	private List<Resource> resources;

	private List<Resource> testResources;

	private String[] includes;

	public void execute()throws MojoExecutionException{
		if(includes == null || includes.length == 0){
			includes = INCLUDES_DEFAULT;
		}
		try{
			countDir(sourceDirectory);
			countDir(testSourceDirectory);
			for(Resource resource:resource){
				countDir(new File(resource.getDirectory());
			}
			for(Resource resource:testResource){
				countDir(new File(resource.getDirecotry());
			}
		}catch(IOException e){
			throw new MojoExecutionException("Unable to count lines of code.*,e);
		}
	}
}

首先每个插件类目标,或者说Mojo,都必须继承自AbstractMojo并实现execute()方法,只有这样Maven才能识别该插件目标,并执行execute()方法中的行为。其次,由于历史原因,上述的CountMojo类使用了java1.4风格的标注(将标注写在注释中),这里关注的时候@goal,任何一个Mojo都必须使用该标注写明自己的目标名称,有了目标定义之后,我们才能在项目中配置该插件目标,或者在命令行执行命令调用,例如:

mvn com.juvenxu.mvnbook:maven-loc-plugin:0.0.1-SNAPSHOT:count

创建一个Mojo所必要的工作就是这三项,继承自AbstractMojo、实现execute()方法,提供@goal标注。
下一步是为插件提供配置点。我们希望该插件能够统计所有的Java、XML,以及properties文件,但是允许用户配置包含哪些类型的文件。
下一步是为插件提供配置点。我们希望该插件默认统计所有java、XML以及properties文件,但是允许用户配置包含哪些类型的文件。下面的代码中的includes字段就是用来为用户提供该配置点的,他的类型为String数组,并且使用了@parameter参数表示用户可以在使用该插件的时候在POM文件中配置该字段。

<plugin>
	<groupId>com.juvenxu.mvnbook</groupId>
	<artifactId>maven-loc-plugin</artifact>
	<version>0.0.1-SNAPSHOT</version>
	<configuration>
		<includes>
			<include>java</include>
			<include>sql</include>
		</includes>
	</configuration>
</plugin>

代码清单中配置了CountMojo统计Java和SQL文件。
CountMojo类中还包含了basedir、sourceDirectory、testSourceDirectory等字段,他们都使用了@parameter标注,但同时关键字expression表示从系统属性读取这几个字段的值。${properties.basedir},${project.build.sourceDirectory},${project.build.testSourceDirectory}等表达式分别表示项目的基础目录、主代码目录和测试代码目录。@readonly标注表示不允许用户进行配置,因为对于一个项目来说,这几个目录位置都是固定的。
了解了这些简单的配置之后,下一步就是实现插件的具体行为了。从之前代码清单中能够看到一些信息:如果用户没有配置include则使用默认的统计包含配置,然后再分别统计项目主代码目录、测试代码目录、主资源目录以及测试资源目录。这里设计一个countDir()方法,其具体实现如下:

private void countDir(File dir)throws IOException{
	if(dir.exists()){
		return;
	}
	List<File> collected = new ArrayList<File>();
	collectFiles(collected.dir);
	int line = 0;
	for(File sourceFile:collected){
		lines += countLine(sourceFile);
	}
	String path = dir.getAbsolutePath().substring(basedir.getAbsolutePath().length());
	getLog().info(path + ":" + lines + "lines of code in" + "code size() + "files");
}

private void collectFiles(List<File> collectd,File file){
	if(file.isFile){
		for(String include:includes){
			if(file.getName().endWith("." + include){
				collectd.add(file);
				break;
			}
		}
	}else{
		for(File sub:file.listFiles()){
			collecteFiles(collected,sub);
		}
	}
}

private int countLine(File file)throws IOException{
	BufferedReader reader = new BuffereReader(new FileReader(file));
	int line = 0;
	try{
		while(reader.ready()){
			reader.readLine();
			line ++;
		}
	}
	finally{
		reader.close();
	}
	return line;
}

这里简单介绍下上述的三个方法:collectFIles()方法用来递归地收集下一个目录下所有应当被统计的文件,countLine()方法用来统计单个文件的行数,而countDir()则借助上述的两个方法统计某一目录下共有多少文件被统计,以及这些文件包含了多少行代码。
使用mvn clean install命令将该插件安装到本地仓库之后,就能够使用它统计Maven项目的代码行数了。如下所示:

mvn com.juvenxu.mvnbook:maven-loc-plugin:0.0.1-SNAPSHOT:count

如果嫌命令太长,可以将该插件的groupId添加到setting.xml中。如下所示:

<settings>
	<pluginGroups>
		<pluginGroup>com.juvenxu.mvnbook</pluginGroup>
	</pluginGroups>
</settings>

再执行命令的时候就可以简化为:

mvn loc:count

Mojo标注

每个Mojo都必须使用@Goal标注来注明其目标名称,否则Maven将无法识别该目标。Mojo的标注不仅限于@Goal,一下是一些可以控制Mojo行为的标注:

  • @goal
    这是唯一必须声明的标注,当用户使用命令行调用插件,或者在POM中配置插件的时候,都需要使用该目标名称。
  • @phase
    默认将该目标绑定值Default生命周期的某个阶段,这样在配置使用该插件目标的时候就不需要声明phase。例如,maven-surefire-plugin的test目标就带有@phase test标注。
  • @requiresDependencyResolution
    表示在运行该Mojo之前必须解析所有指定范围的依赖。例如,maven-surefire-plugin的test目标带有@requiresDependencyResolution test标注,表示在执行测试之前,所有测试范围的依赖必须得到解析。这里的依赖范围有compile、test和runtime,默认值为runtime。
  • @requiresProject <true/false>
    表示该目标是否必须在一个Maven项目中运行。默认值为true。大部分插件目标必须依赖一个项目才能执行,但有一个例外。例如maven-help-plugin的system目标,他用来显示系统属性和环境变量信息,不需要实际项目,因此使用了@requiresProject标注。另外,maven-archetype-plugin的generate也是一个例子。
  • @requiresDirectInvocation <true/false>
    当值为true的时候,该目标就只能通过命令直接调用,如果试图在POM中将其绑定到生命周期阶段,Maven就会报错,默认值为false。如果你希望编写的插件只能在命令行中运行,就是用该标注。
  • @requiresOnline <true/false>
    表示是否要求Maven必须是在线状态,默认值是false。
  • @requiresRepot <true/false>
    表示是否要求项目报告已经生成,默认值是false。
  • @aggregator
    当Mojo在多模块项目上运行时,使用该标注表示目标会在顶层模块运行。例如maven-javadoc-plugin的aggregator-jar使用了@aggregator标注,他不会为多模块项目的每个模块生成javadoc,而是在顶层项目生成一个已经聚集的javadoc文档。
  • @execute goal = “<goal>"
    该目标运行之前先让Maven运行另外一个目标,如果是本插件的目标,则直接使用目标名称,否则使用“prefix:goal”的形式,即注明目标前缀。例如maven-pmd-plugin是一个使用PMD来分析项目源码的工具,他包含pmd和check等目标,其中pmd用来生成报告,而check用来生成验证报告。由于check是依赖于pmd生成的内容,因此可以看到他使用了标注@execute goal=”pmd”。
  • @execute phase = “<phase>”
    在运行该目标之前,让Maven先运行一个并行的生命周期,到指定的阶段为止。例如maven-dependency-plugin的analyze使用了标注@execute phase = “test-compile”,因此当用户在执行dependency:analyze的时候,Maven会首先执行default生命周期所有至test-compile的阶段。
  • @execute lifecycle = “<lifecycle>” phase = “<phase>”
    在运行该目标之前让Maven先运行一个自定义的生命周期,到指定的阶段为止。例如maven-surefire-report-plugin这个用来生成一个测试报告的插件,它有一个report目标,标注了@execute phase = “test” lifecycle = “surefire”,表示运行这个自定义的surefire生命周期至test阶段。

Archetype

Archetype可以理解成Maven项目的模板,例如maven-archetype-quickstart就是一个最简单的Maven项目模板,只需要提供最基本的元素(如groupId、artifactId、version等),他就能生成项目的基本结构及POM文件。很多著名的开源项目(如AppFuse和Apache Wicket)都提供了Archetype方便用户快速的创建项目。如果你所在组织的项目都遵循一些通用的配置及结构,则也可以为其创建一个自己的Archetype并进行维护。使用Archetype不仅能让用户快速地创建项目;还可以鼓励大家遵循一些项目结构及其配置约定。

Maven Archetype Plugin

Archetype并不是Maven的核心特性,他也是通过插件来实现的,这一插件就是maven-archetype-plugin(http://maven.apache.org/archetype/maven-archetype-plugin/)。尽管它只是一个插件,但由于其使用范围非常广泛,主要的IDE(如Eclipse、IDEA、NetBeans)在集成Maven插件的时候就集成了archetype特性,以方便用户快捷的创建Maven项目。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值