1. 概述
Maven是一个Java开发者比较熟悉的项目构建工具,旨在简化项目构建和自动化测试过程。
2. Maven的框架模型
Maven本质上是一个插件框架,其运行命令一般为:
mvn plugin-name : goal -Dparameter=abc
例如在创建一个新Maven项目时,我们使用:mvn archetype:generate,或是部署一个项目文件:mvn deloy:deploy-file
这里的archetype和deploy都是maven的插件,generate和deploy-file意思是使用插件的某个方法。参数传递同Java的入口函数(Main)相同。
Maven对项目的结构描述放在pom.xml中,其中字段的含义在这就不一一赘述了。
Maven在构建项目时有三种执行流:default, clean, site
1. default
Validate //验证项目目录结构等是否完整
Compile //编译
Test //单元测试
Package //打包
Integration-test //集成测试
Verify //测试包是否有效
Install //将包安装到本地repository中,默认为.m2文件夹
Deploy //将包部署到私有仓库中(nexus或artifactory)
当用户执行某一命令时,其之前的命令都会被执行一遍。例如用户输入mvn package,则Maven会依次运行Validate, Compile, Test, Package
2. clean: 清理所有构建生成的文件(目标class,jar包war包,资源文件等等)。用法mvn : clean。它的执行流程是Pre-clean, Clean, Post-clean
3. site: 生成详细的文档。用法mvn site
3. 模块化管理
Maven的模块化管理非常简单,首先定义父项目为Sample,其中有三个子项目A,B,C,其中C是Web项目,也是最终的输出项目
3.1 父模块
Sample的pom文件如下
<pre name="code" class="html"><project ....>
<modelVersion>4.0.0</modelVersion>
<!-- 0.0.4 -->
<groupId>com.example.maven</groupId>
<artifactId>Sample</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>pom</packaging>
<properties>
<spring.version>3.6</spring.version>
<base.url>http://x.x.x.x:port</base.url>
</properties>
<distributionManagement>
<repository>
<id>maven.release</id>
<name>maven-releases</name>
<url>${base.url}/artifactory/libs-release-local</url>
</repository>
<snapshotRepository>
<id>maven.snapshots</id>
<name>maven-snapshots</name>
<url>${base.url}/artifactory/libs-snapshot-local</url>
</snapshotRepository>
</distributionManagement>
<modules>
<module>A</module>
<module>B</module>
<module>C</module>
</modules>
<dependencyManagement>
<dependencies>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${spring.version}</version>
</dependencies>
</dependencyManagement>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.5.1</version>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
父项目中的packaging字段需为pom类型,即其只负责管理项目。
modules字段中定义了子项目
distributionManagement管理了打包上传的服务器仓库地址(这里我使用的是artifactory私有仓库)
注意:这里使用了<dependencyManagement> 和 <pluginManagement> ,而不是直接使用<dependencies>和<plugins>。这是应为如果直接使用dependencies,则所有的子项目都会继承依赖父项目的包,即使子项目并没有用到这个包。而把dependencies放入dependencyManagement中后,子项目继承父项目中对依赖的定义,而不会直接继承依赖包。即父项目只是在管理包的版本,而不管谁去用它。例如在A项目中,它必须显示声明
<dependencies>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
</dependencies>
才会去使用spring-beans-3.6这个包。pluginManagement同理。
许晓斌在InfoQ的文章里还介绍了一种更加简练的包依赖管理。试想我们有7,8个子项目,依赖50-100个包,我们无法清晰的对这些包进行划分,即哪个子项目用了哪些包。可用下面的方法,新建一个空项目A-Dependency,项目的pom如下
<project>
..
<artifactId>A-Dependency</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<dependencyManagement>
<dependencies>
...
</dependencies>
</dependencyManagement>
</project>
该项目专门用来管理A所需要的,或是一类包,然后在Sample的pom中作如下引用
<dependencyManagement>
<dependencies>
<dependency>
<artifactid>A-Dependency</artifactId>
<version>1.0-SNAPSHOT</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
这里的scope使用了import
Maven Dependency有6个scope
Compile //默认,编译和打包都会引用到该包
Provided //编译时需要使用,但运行时环境会提供无需打包,例如tomcat, jboss等容器的包(servlet-api等等)
Runtime //编译时无需使用但运行时需要,所以会一起打包,例如oracle jdbc driver(通过反射调用)
Test // 只在单元测试时使用,无需打包
System // 引用本地的Jar包
Import
3.2 子模块
因为C模块的输出为War包,我们直接看C的pom文件
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example.maven</groupId>
<artifactId>Sample</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>C</artifactId>
<packaging>war</packaging>
...
<build>
<finalName>C-WEB</finalName>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>false</filtering>
<excludes>
<exclude>**/*.*</exclude>
</excludes>
</resource>
</resources>
<plugins>
<!-- war plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<configuration>
<archive>
<addMavenDescriptor>false</addMavenDescriptor>
</archive>
<!-- Default: ${project.build.directory} -->
<outputDirectory>${package.outputDirectory}</outputDirectory>
<webResources>
<resource>
<directory>src/main/resources/${package.environment}</directory>
<targetPath>WEB-INF/classes</targetPath>
<filtering>true</filtering>
</resource>
<resource>
<directory>src/main/resources/config</directory>
<targetPath>WEB-INF/classes</targetPath>
<filtering>true</filtering>
</resource>
</webResources>
</configuration>
</plugin>
</plugins>
</build>
</project>
其中parent中定义了继承自哪一个父模块
4. maven-war Plugin 使用不通配置文件
首先,C项目的src/main/resource下有三个文件夹
config //存放所有不会改变的资源文件,例如log4j.properties
local //存放本地环境的资源文件
test //存放测试环境的资源文件
再看build节点
resources节点实际上是使用了maven-resource插件,由于是maven的必装插件,所以无需使用plugin声明,在该节点中,我们使用了exclude方法过滤了所有的src/main/resouces文件夹下的资源文件。即resource下的资源文件不会输出到WEB-INF/classes下。
而在War-Plugin中,我们将需要的资源文件拷贝到相应的文件夹中进行打包。其中${package.environment}代表一个参数变量,用户可以有两种方法传入,一是使用properties节点,二是在敲命令行时传入。例如mvn package -Dpackage.environment=test,此时就会把src/main/resource/test文件夹下的资源文件放入WEB-INF/classes中
outputDirectory定义了war包的输出地址
注意:maven-resouce和maven-war-plugin执行的节点不一样,resource是在maven编译时执行,war-plugin是在package时执行,我们所做的操作实际上是拷贝资源文件。
5. jetty-maven-plugin Web应用集成测试
在集成测试Web应用时,通常有这么几步:启动Web容器,部署待测试Web应用,以Web客户端的角色运行测试用例,停止Web容器
首先需要启动Web容器,部署应用和关闭容器,在build节点中添加如下
<plugin>
<groupId>org.mortbay.jetty</groupId>
<artifactId>jetty-maven-plugin</artifactId>
<version>6</version>
<configuration>
<stopPort>9090</stopPort>
<stopKey>stop-jetty-inthatport</stopKey>
</configuration>
<executions>
<execution>
<id>start-jetty</id>
<phase>pre-integration-test</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<daemon>true</daemon>
</configuration>
</execution>
<execution>
<id>stop-jetty</id>
<phase>post-integration-test</phase>
<goals>
<goal>stop</goal>
</goals>
</execution>
</executions>
</plugin>
其中execution中的goal表明要使用哪个方法(目标),phase表示在哪个阶段启动这个方法。其他具体配置详见
http://www.eclipse.org/jetty/documentation/current/jetty-maven-plugin.html
为了测试Web应用是否正确部署,我们需要用Junit些一些测试用例,并使用maven-surefire-plugin(test阶段默认使用的plugin)验证测试
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.7.2</version>
<executions>
<execution>
<id>run-integration-test</id>
<phase>integration-test</phase>
<goals>
<goal>test</goal>
</goals>
<configuration>
<includes>
<include>**/*IntegrateT.java</include>
</includes>
</configuration>
</execution>
</executions>
</plugin>
默认的情况下surefire会取*Test.java做单元测试,所以给我们的集成测试取名*IntegrateT.java
在IntegrateTestJava中可以使用类似下面的方法去验证Web应用是否正确启动
HttpClient httpclient = new DefaultHttpClient();
HttpGet httpGet = new HttpGet( "http://localhost:8080/C-Web/index.html" );
HttpResponse response = httpclient.execute( httpGet );
Assert.assertEquals(200, response.getStatusLine().getStatusCode() );
6. 正确的集成命令
再此,直接引用许http://www.infoq.com/cn/articles/xxb-maven-4-ci的原文:
在持续集成服务器上使用怎样的 mvn 命令集成项目,这个问题乍一看答案很显然,不就是 mvn clean install 么?事实上比较好的集成命令会稍微复杂些,下面是一些总结:
不要忘了clean: clean能够保证上一次构建的输出不会影响到本次构建。
使用deploy而不是install: 构建的SNAPSHOT输出应当被自动部署到私有Maven仓库供他人使用,这一点在前面已经详细论述。
使用-U参数: 该参数能强制让Maven检查所有SNAPSHOT依赖更新,确保集成基于最新的状态,如果没有该参数,Maven默认以天为单位检查更新,而持续集成的频率应该比这高很多。
使用-e参数:如果构建出现异常,该参数能让Maven打印完整的stack trace,以方便分析错误原因。
使用-Dmaven.repo.local参数:如果持续集成服务器有很多任务,每个任务都会使用本地仓库,下载依赖至本地仓库,为了避免这种多线程使用本地仓库可能会引起的冲突,可以使用-Dmaven.repo.local=/home/juven/ci/foo-repo/这样的参数为每个任务分配本地仓库。
使用-B参数:该参数表示让Maven使用批处理模式构建项目,能够避免一些需要人工参与交互而造成的挂起状态。
综上,持续集成服务器上的集成命令应该为 mvn clean deploy -B -e -U -Dmaven.repo.local=xxx 。此外,定期清理持续集成服务器的本地Maven仓库也是个很好的习惯,这样可以避免浪费磁盘资源,几乎所有的持续集成服务器软件都支持本地的脚本任务,你可以写一行简单的shell或bat脚本,然后配置以天为单位自动清理仓库。需要注意的是,这么做的前提是你有私有Maven仓库,否则每次都从Internet下载所有依赖会是一场噩梦。
7. 持续集成
本地(一般是Windows平台)的构建,单元测试,集成测试成功了。但是目标平台(例如linux)可能会发生未知的错误。所以建议使用自动化持续集成工具。例如Hudson,
在目标服务器上安装Hudson, 让其去取VCS(Version Control System)的最新代码,调用Maven进行构建。Hudson和git, svn, cvs都有很好的集成,再次不赘述了。
总之,Maven很强大,是Java开发,敏捷团队的必备工具