赞
踩
Maven被翻译成知识的积累,也可以翻译成“专家”、“内行”,他是Apache组织中的一个优秀的开源项目,Maven主要服务于基于java平台的项目构建、依赖管理和项目信息管理。
程序员工作中会有大量的构建(build)工作,众多的IDE中大都是一个锤子的图标。我们会从仓库中拉取代码,在本地通过编译运行测试代码的各种问题;在以前开发的时候,不同项目调用相同的jar包,需要将jar包分别手动复制到对应的工程下,通过工程的build path关联路径,从而使得该jar包能够被各个项目所使用。
这种结构使得很多公共的jar包会被复制很多份,分别放到不同的项目中,这种方式不仅增加了构建工作、浪费计算机资源,而且在jar包变动的时候也难以管理,也可能手动导入许多没用的jar包等等一系列的问题。
Maven有仓库的概念,将依赖统一放到仓库中管理通过坐标定位每一个jar包的位置,项目中只需要设置坐标就能自动的跟进所需要的jar包。这种方式节约了计算机资源、易于管理,大大提高了程序员的构建效率。
不仅如此,Maven可以自动化构建过程,从清理、编译、测试到生成报告,再到打包和部署。我们不需要一遍遍的输入命令,进行各种繁杂的操作,我们要做的就是Maven配置好了之后,输入简单的命令(例如mvn clean install等,大多IDE上还有对应的界面操作),Maven就会帮我们完成哪些复杂的操作。
Maven最大化的消除了构建的重复,抽象了构建生命周期,并且为绝大部分的构建任务提供了已实现的插件,通过这些插件,Maven能够实现更多的事情。同时,Maven还标准化了构建的过程,极大的避免了不必要的学习成本等。
Java不仅仅是一门编程语言,还是一个平台,通过JRuby和Jython,我们可以在java平台上运行Ruby和Python程序。Maven也不仅仅是一个构建工具,还是一个依赖管理工具和项目信息管理工具。他提供了中央仓库,能帮助我们自动下载构件。
Maven为全世界java开发者免费提供了一个中央仓库,在其中几乎可以找到任何开源的类库,国内也通了一些镜像可供我们提升下载速度。
Maven不是多么高级的技术,他是用java写的,所以使用Maven就必须要有JDK。Windows上查看JDK安装和配置:
使用java -version命令或者使用echo %JAVA_HOME%
官方下载地址
可以下载bin或者附带源码的src,官网上还提供了MD5校验和(checksum)文件、asc数字签名文件等,可以用来验证Maven分发包的正确性和安全性。
与tomcat类似,windows上安装Maven只需要解压、配置环境变量即可使用,环境变量的配置最终能定位bin目录即可,这里不做过多的描述。
Maven是跨平台的,它可以在任何一种主流操作系统上运行。
Maven解压后的目录结构如下:
用户目录下可以发现一个.m2文件夹,默认情况下,该文件夹下放置了Maven本地仓库.m2/repository。所有的Maven构件都被存储到该仓库中,可以方便重用。Maven根据一套规则来确定任何一个构件在仓库中的具体位置。
一些公司出于安全考虑,要求使用安全认证的代理访问因特网。这种情况下,就需要为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结尾的域名访问都不需要代理。
运行mvn实际上是执行了java命令,既然是运行java命令,那么java命令可用的参数当然在运行mvn时也可用,这时候,MAVEN_OPTS环境变量就能派上用场了。通常需要设置MAVEN_OPTS的值为-Xms128m -Xmx512m,以为java默认的最大可用内存往往不能够满足MAVEN运行的需求,比如在项目比较时,使用MAVEN生成站点需要占用大量内存,如果没有配置,很容易得到java.lang.OutOfMemeoryError。
MAVEN解压路径下conf目录的setting.xml是全局配置,~/.m2/setting.xml配置的是用户范围内的配置。
就像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);
}
}
一个典型的测试单元包含三个步骤
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项目结构以及如何进行编译、测试、打包等。
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来快速生成一个项目骨架。
注册互联网账户是日常生活中最简单不过的一件事情,作为一个用户,注册账户时往往需要做一下事情:
了解账户注册服务之后,下面从软件工程的角度来分析一下改服务的需求。
Maven的一大功能是管理项目依赖。为了能够自动的解析任何一个java构件,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项目的一个子模块。下面介绍一下各个元素:
一个依赖的声明可以包含以下的元素:
</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,以声明一个或多个项目依赖。每个依赖可以包含的元素有:
上节提到,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 | Y | Y | Y | spring-core |
test | Y | JUnit | ||
provided | Y | Y | servlet-api | |
runtime | Y | Y | JDBC | |
system | Y | Y | 本地的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是传递性依赖。第一直接依赖范围和第二直接依赖范围影响了传递性依赖的范围,关系如下图所示:
compile | test | provided | runtime | |
---|---|---|---|---|
compile | compile | runtime | ||
test | test | test | ||
provided | provided | provided | provided | |
runtime | runtime | runtime |
观察一下表格,可以发现这样的一个规律:当第二直接依赖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世界里,任何一个依赖、插件或项目构建的输出,都可以称之为构件。例如: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()方法生成的目的是根据构件信息生成其在仓库中的路径。这里根据一个实际的例子来分析路径的生成,考虑这样一个构件:
其对应的路径按如下的步骤生成:
对于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,也应该在本地建立私服。因为私服可以帮助你:
很多情况下,默认的中央仓库无法满足项目的需求,可能项目需要的构件存在于另一个远程仓库中,如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,供她开发和调试,问题是,如何进行呢?
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会自动找到最新的快照。这背后的依赖解析机制可以概括如下:
当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还支持一些高级的镜像配置:
需要注意的是,由于镜像仓库完全屏蔽了被镜像的仓库,当镜像仓库不稳定或者停服的时候,Maven仍将无法访问被镜像的仓库,因而无法下载需要的构建。
使用Maven进行日常开发的时候,一个常见的问题就是如何寻找需要的依赖,我们可能只知道使用类库的项目名称,但添加Maven依赖要求提供确切的Maven坐标。这时,就可以使用仓库搜索服务来根据关键字得到Maven坐标。
地址:http://repository.sonatype.org/
Nexus是当前最流行的Maven仓库管理软件,这里要介绍的是Sonatype架设的一个公共Nexus仓库实例。
Nexus提供了关键字、类名搜索、坐标搜索、校验和搜索等功能。搜索后,页面清晰的列出了结果构件的坐标及所属仓库。用户可以直接下载相应的构件,还可以复制已经根据坐标自动生成的XML依赖声明。
地址:http://www.jarvana.com/jarvana
Jarvana提供了基于关键字、类名的搜索,构件下载,依赖声明片段等功能也是一应俱全。值得一提的是,Jarvana还支持浏览构件的内容。此外,Jarvana还提供了便捷的Java文档浏览的功能。
地址:http://mvnbrowser.com
MVNbrowser只提供关键字搜索的功能,除了提供基于坐标的依赖声明代码片段等基本功能之外,MVNbrowser的一大特色就是,能够告诉用户该构件依赖于哪些其他构件以及该构件被那些其他构件所依赖。
地址: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生命周期的目的是清理项目,他包含三个阶段。
default生命周期定义了真正构建时所需要执行的所有步骤,他是所有生命周期中最核心的部分,其包含的阶段如下,这里只对重要的部分进行解释:
对于上述未加解释的阶段,读者也应该能根据名字大概猜到其用用途,若想进一步的了解这些阶段的详细信息,可以参阅官方的解释:http://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html。
site生命周期的目的是建立和发布项目站点,Maven能够基于POM所包含的信息,自动生成一个友好的站点,方便团队交流和发布项目信息。该生命周期包含如下阶段:
从命令行执行Maven任何的主要方式是调用Maven的生命周期阶段。需要注意的是,各个生命周期是相互独立的,而一个生命周期的阶段是有前后依赖关系的。下面以一些常见的Maven命令为例,解释其执行的生命周期阶段:
由于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-clean | maven-clean-plugin:clean |
clean | |
post-clean |
生命周期阶段 | 插件目标 |
---|---|
pre-site | maven-site-plugin:site |
site | |
post-site | |
site-deploy | maven-site-plugin:deploy |
生命周期阶段 | 插件目标 | 执行任务 |
---|---|---|
process-resources | maven-resources-plugin:resources | 复制主资源文件至主输出目录 |
compile | maven-compiler-plugin:compile | 编译主代码至输出目录 |
process-test-resources | maven-resources-plugin:testResources | 复制测试资源文件至测试输出目录 |
test-compile | maven-compiler-plugin:testCompile | 编译测试代码至测试输出目录 |
test | maven-surefire-plugin:test | 执行测试用例 |
package | maven-jar-plugin:jar | 创建项目jar包 |
install | maven-install-plugin:install | 将项目输出构件安装到本地目录 |
deploy | maven-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文件中一次性配置就显然比重复在命令行输入要方便。
用户可以在声明插件的时候,对此插件进行一个全局的配置。也就是说,所有基于该插件目标的任务,都会使用这些配置。例如,我们通常会需要配置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进行编译。
除了为插件配置全局参数,用户还可以为某个插件任务配置特定的参数。以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中加入其他的仓库配置。
在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元素列表
依赖是会被继承的,把依赖配置放到父模块中,多个子模块中就能移除重复的依赖,简化配置。
标准的重要性已经不用过多强调,想象一下,如果不是所有的程序员都基于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会假设用户的项目是这样的:
遵循约定虽然失去了一定的灵活性,用户不能随意安排目录结构,但是却能减少配置。更重要的是,遵循约定能够帮助用户遵守构建标准。
如果没有约定,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可以看到这些选项:
私服不是Maven的核心概念,它仅仅是一种衍生出来的特殊的maven仓库,通过建立自己的私服,就可以降低中央仓库的负荷、节省外网带宽、加速maven构建、自己部署构建等,从而高效的使用Maven。
2005年12月,Tamas Cservenak由于受不了匈牙利电信ADSL的低速度,开始着手开发Proximity——一个很简单的Web应用。他可以代理并缓存Maven构件,当Maven需要下载构件的时候,就不需要反复依赖于ADSL。到2007年,Sonatype邀请Tamas参与创建一个更酷的Maven仓库管理软件,这就是后来的Nexus。
nexus分为开源版和社区版,其中开源版基于GPLv3许可证,其特性以满足大部分Maven用户的需求。一下是Nexus开源版本的特性:
Nexus专业版是需要付费购买的,除了开源版本的所有特色之外,他主要包含一些企业安全控制、发布流程控制等需要的特性。官方网址:http://www.sonatype.com/products/nexus/community。
Nexus是典型的Java Web应用,它有两种安装包,一种是包含Jetty容器的Bundle包,另一种是不包含Web容器的war包。
官网:http://nexus.sonatype.org/downloads/下载最新版本的Nexus。
Nexus的Bundle自带Jetty容器,因此用户不需要额外的web容器就能启动Nexus。首先将Bundle文件解压,这时就会得到两个目录:
其中,第一个目录是运行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/子目录下还有一些其他的脚本:
借助windows服务,用户就可以让Nexus伴随着windows自动启动,非常方便。
除了Bundle,Nexus还提供了一个可以直接运行在web服务器上的war包。该war包支持主流的服务器,如tomcat、classfish、Jetty、Resin等。
Nexus拥有全面的权限控制功能,默认的Nexus的访问都是匿名的,而匿名用户仅包含一些最基本的权限,要全面的学习使用Nexus,就必须以管理员的方式登录。默认的账号密码为admin/admin123。
作为Maven仓库服务软件,仓库自然是Nexus中最重要的概念。Nexus包含了各种类型的仓库概念,包括代理仓库、宿主仓库和仓库组等。每一种仓库都提供了丰富使用的配置参数,方便用户根据需要进行定制。
在具体介绍每一种类型的仓库之前,先浏览一下Nexus的内置仓库,单机Nexus界面左边的Repositories链接,可以看到所有类型的Nexus仓库。
仓库包含四种类型:
解释一下各个仓库的内容:
随着敏捷开发模式的日益横行,软件开发人员也越来越意识到日常编程工作中单元测试的重要性。Maven的重要职责之一就是自动运行单元测试,他通过maven-surefire-plugin与主流的单元测试框架Junit3、Junit4以及TestNG集成,并能够自动生成丰富的结果报告。
通过Maven可以调用JUnit和TestNG等框架对项目进行测试,非常的方便。Maven的test命令中还可以通过参数非常灵活的调节测试的类和方法,也可以生成测试报告、执行动态测试等功能。
简单的说,持续集成就是快速且高效地自动构建项目的所有源码,并为项目成员提供丰富的反馈信息。这句话有很多的关键字:
一个典型的继承场景是这样的:开发人员对代码做了一些修改,在本地运行构建确认无误后,将代码提交到代码库。具有高配置硬件的持续集成服务器每隔30分钟查询代码库一次,发现更新后,签出所有最新的代码,将调用自动化构建工具(如Maven)构建项目,该过程包括编译、测试、审核、打包和部署等。然而不幸的是,另外一名开发人员在这一时间段也提交了代码更改,两处更改导致了某些代码的失败,持续集成服务器基于这些失败的测试创建了一个报告,并自动发给相关开发人员。开发人员接到报告后,立即周守调查原因,并尽快修复。
一次完整的集成往往包含以下6个步骤:
持续集成需要额外的引入硬件设置,特别是对于集成服务器来说,性能越高,集成的速度就越快,反馈的速度也就越快。持续集成还需要开发者使用工具,如源码控制工具、自动化构建工具、自动化检测工具、持续集成软件等。这一切无疑都增加了开发者的负担,然而学习并适应这些开发工具及流程是完全值得的,因为持续集成有着很多好处:
在现今的互联网时代,我们创建的大部分应用程序都是Web应用,在Java世界中,Web项目的标准打包方式就是war。
我们都知道,基于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为了支持构建的灵活性,内置了三大特性,即属性、profile和资源过滤。
Maven不仅仅是一个自动化构建工具和一个依赖管理工具,它还能够帮助聚合项目信息,促进团队间交流。POM可以包含各种信息,如项目描述、版本控制系统地址、缺陷跟踪系统地址、认证许可信息、开发者信息等。用户可以让Maven自动生成一个站点,以web的形式发布这些信息。此外,Maven社区提供了大量的插件,能够让用户生成各种各样的审查报告,包括测试覆盖率、静态代码分析、代码变更等。
Maven的任何行为都是由插件完成的,包括项目的清理、编译、测试以及打包等操作都有其对应的Maven插件。每个插件拥有一个或者多个目标,用户可以直接从命令行运行这些插件目标,或者选择将目标绑定到Maven的生命周期。
大量的插件可以从Apache和Codehaus获得,这里的近百个插件几乎能满足所有Maven项目的需求。除此之外,还有很多Maven插件分布在Googlecode、Sourceforge、Github等项目托管服务中。因此当你发现自己有特殊需求的时候,首先应该搜索一下看是否已经有现成的插件可供使用。例如,如果想要配置Maven自动为所有Java文件的头部添加许可证声明,那么通过关键字maven plugin lisense找到maven-lisense-plugin,这个托管在Goolecode上的项目完全能够满足我们的需求。
在一些及其特殊的情况下,也可以编写自己的插件。
下面编写一个用于代码统计的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有两个特殊的地方:
插件项目创建好了之后,下一步是为插件编写目标。使用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都必须使用@Goal标注来注明其目标名称,否则Maven将无法识别该目标。Mojo的标注不仅限于@Goal,一下是一些可以控制Mojo行为的标注:
Archetype可以理解成Maven项目的模板,例如maven-archetype-quickstart就是一个最简单的Maven项目模板,只需要提供最基本的元素(如groupId、artifactId、version等),他就能生成项目的基本结构及POM文件。很多著名的开源项目(如AppFuse和Apache Wicket)都提供了Archetype方便用户快速的创建项目。如果你所在组织的项目都遵循一些通用的配置及结构,则也可以为其创建一个自己的Archetype并进行维护。使用Archetype不仅能让用户快速地创建项目;还可以鼓励大家遵循一些项目结构及其配置约定。
Archetype并不是Maven的核心特性,他也是通过插件来实现的,这一插件就是maven-archetype-plugin(http://maven.apache.org/archetype/maven-archetype-plugin/)。尽管它只是一个插件,但由于其使用范围非常广泛,主要的IDE(如Eclipse、IDEA、NetBeans)在集成Maven插件的时候就集成了archetype特性,以方便用户快捷的创建Maven项目。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。