【Maven教程】(三)基础使用篇:入门使用指南——POM编写、业务代码、测试代码、打包与运行、使用Archetype生成项目骨架~-LMLPHP


1️⃣ 编写 POM

到目前为止,已经大概了解并安装好了Maven环境, 现在,我们开始创建一个最简单的 Hello World 项目。如果你是初次接触 Maven, 建议按照本文的内容 一步步地编写代码并执行, 其中可能你会碰到一些概念暂时难以理解,但不用着急,记下这些疑难点,我在后续文章中会逐一进行解答。

就像 MakeMakefileAntbuild.xml 一样 ,Maven 项目的核心是 pom.xmlPOM (Project Object Model, 项目对象模型) 定义了项目的基本信息,用于描述项目如何构建, 声明项目依赖,等等。现在先为 Hello World 项目编写一个最简单的 pom.xml

首先创建一个名为 hello-world 的文件夹,打开该文件夹,新建一个名为 pom.xml 的文件,输入其内容,如下所示。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven,apache.org/POM/4.0.0"
	xmins: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.xiaoshan.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 模型的版本,对于 Maven 2 及 Maven 3 来说,它只能是4.0.0。

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

groupld定义了项目属于哪个组,这个组往往和项目所在的组织或公司存在关联。譬如在 googlecode 上建立了一个名为 myapp 的项目,那么 groupld 就应该是 com. googlecode.myapp, 如果你的公司是 mycom, 有一个项目为 myapp, 那 groupld 就应该是 com.mycom.myappartifactld 定义了当前Maven 项目在组中唯一 的ID, 我们为这个 Hello World 项目定义 artifactldhello-world。 而在前面的 groupIdcom. googlecode.myapp 的例子中,你可能会为不同的子项目(模块)分配 artifactId, 如 myapp-utilmyapp-domainmyapp-web 等。
顾名思义, version 指定了 Hello World 项目当前的版本 — — 1.0-SNAPSHOT。SNAPSHOT 意为快照,说明该项目还处于开发中,是不稳定的版本。随着项目的发展, version 会不断更新,如升级为1.01.1-SNAPSHOT1.12.0 等。后面文章会详细介绍 SNAPSHOT 以及如何使用Maven 管理项目版本的升级发布。

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

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

2️⃣ 编写业务代码

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

package com.xiaoshan.mvnbook.helloworld;

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

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

关于该Java代码有两点需要注意。首先,在绝大多数情况下,应该把项目主代码放到 src/main/java/ 目录下 ( 遵循 Maven的约定), 而无须额外的配置,Maven 会自动搜寻该目录找到项目主代码。其次,该Java 类的包名是 com.xiaoshan. mvnbook. helloworld, 这与之前在 POM 中定义的 groupIdartifactId 相吻合。 一般来说,项目中Java 类的包都应该基于项目的 groupIdartifactId, 这样更加清晰,更加符合逻辑,也方便搜索构件或者Java 类 。

代码编写完毕后,使用Maven 进行编译,在项目根目录下运行命令 mvn clean compile。会得到如下输出:

[INFO] Scanning for projects..
[INFO]
-----------------------------------------------------------------------
[INFO] Building Maven Hello World Project
[INFO] task-segment:[clean, compile]
[INFO]
-----------------------------------------------------------------------
[INFO] [clean:clean {execution:default-clean}]
[INF0] Deleting directory D:\code\hello-world\target
[INF0] [resources:resources {execution:default-resources}]
[INFO] skip non existing resourcepirectory D:\code\hello-world\src\main\resources
[INFO] [compiler:compile {execution:default-compile}]
[INFO] Compiling 1 source file to D:\code\hello-world\target\classes
[INFO]
-----------------------------------------------------------------------
[INFO] BUILD SUCCESSFUL
[INFO]
-----------------------------------------------------------------------
[INFO]    Total time:1 second
[INFO]    Finished at: Fri oct 0902:08:09 CST 2009
[INFO]     Final Memory:9M/16M
-----------------------------------------------------------------------

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

上文提到的 clean:clean, resources: resources 和 compiler:compile 对应了一些 Maven 插件及插件目标,比如 clean:clean 是 clean 插件的 clean 目标 ,compiler:compile 是 compiler 插件的 compile 目标。后文会详细讲述 Maven 插件及其编写方法。

至此 ,Maven 在没有任何额外的配置的情况下就执行了项目的清理和编译任务。接下来,编写一些单元测试代码并让 Maven 执行自动化测试。

3️⃣ 编写测试代码

为了使项目结构保持清晰,业务代码与测试代码应该分别位于独立的目录中。上文讲到过 Maven 项目中默认的主代码目录是 src/main/java, 对应地,Maven 项目中默认的测试代码目录是 src/test/java。因此,在编写测试用例之前,应当先创建该目录。

在Java 中,由 Kent Beck 和 Erich Gamma建立的JUnit 是事实上的单元测试标准。要使用JUnit, 首先需要为 Hello World 项目添加一个JUnit 依赖,修改项目的 POM 如下所示:

<?xml version="1.0" encoding="UTP-8"?>
<project xmlns="http://maven.apache,org/POM/4.0.0" 
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/maven-v4_0_0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>com.xiaoshan.mvnbook</groupld>
	<artifactId>helloworld</artifactId>
	<version>1.0-SNAPSHOT</version>
	<name>Maven Hello World Project</name>
	<dependencies>
		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<version>4.7</version>
			<scope>test</scope>
		</dependency>
	</dependencies>
</project>                            

代码中添加了dependencies 元素,该元素下可以包含多个 dependency 元素以声明项目的依赖。这里添加了一个依赖 —— groupIdjunit, artifactIdjunit, version 是 4.7。前面提到 groupIdartifactIdversion 是任何一个 Maven 项目最基本的坐标,JUnit 也不例外,有了这段声明,Maven 就能够自动下载 junit-4.7.jar。也许你会问,Maven 从哪里下载这个 jar 呢? 在 Maven 之前,可以去JUnit 的官方网站下载分发包,有了Maven, 它会自动访问中央仓库 (http://repol.maven.org/maven2/), 下载需要的文件。大家也可以自己访问该仓库,打开路径 junit/junil/4.7/, 就能 看到 junit-4.7.pomjunil-4.7.jar。后面会详细介绍 Maven 仓库及中央仓库。

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

配置了测试依赖,接着就可以编写测试类。回顾 一 下前面的 HelloWorld 类 , 现在要测试该类的 sayHello() 方法, 检查其返回值是否为 “Hello Maven”。 在 src/test/java 目录下创建文件,其内容如下所示:

package com.xiaoshan.mvnbook.helloworld;

import org.junit.Assert.assertEquals;
import org.junit.Test:

public class HelloWorldTest{
	@Test
	public void testSayHello(){
		He1loWorld helloWorld = new HelloWorld();
		String result = helloWorld.sayHello();
		assertEquals("Hello Maven",result);
	}
}

一个典型的单元测试包含三个步骤:①准备测试类及数据;②执行要测试的行为;③检查结果。上述样例首先初始化了一个要测试的HelloWorld实例,接着执行该实例的sayHello()方法并保存结果到result变量中,最后使用JUnit框架的Assert类检查结果是否为我们期望的“HelloMaven”。在JUnit3中,约定所有需要执行测试的方法都以test开头,这里使用了JUnit4,但仍然遵循这一约定。在JUnit4中,需要执行的测试方法都应该以@Test进行标注。

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

[INFO] Scanning for projects.
[INFO]
-------------------------------------------------------------------
[INFO] Building Maven Hello World Project
[INFO] task-segment:[clean, test]
[INFO]
-------------------------------------------------------------------
[INFO] [elean:clean	{execution:default-clean}]
[INFO] Deleting directory D:\git-juven\mvnbook\code\hello-world\target
[INFO] [resources:resources	{execution:default-resources)]
Downloading:http://repo1.maven.org/maven2/junit/junit/4.7/junit-4.7.pom
	1K downloaded (junit-4.7.pom)
[INFO] [compiler:compile {execution: default-compile}]
[INFO]       Compiling 1 source file to D:\code\hello-world\target\classes
[INF0] [resources:testResources {execution:default-testResources}]
Downloading: http://repol,maven.org/maven2/junit/junit/4.7/unit-4.7.jar
	226K downloaded (Junit-4.7.jar) 
[INFO] [compiler:testCompile {execution:default-testCompile}]
[INFO] Compiling 1 source file to D:\code\hello-world\target\test-classes
[INFO]
-------------------------------------------------------------------
[ERROR] BUILD FAILURE
[INFO]
-------------------------------------------------------------------
[INFO] Compilation failure
D:\code\hello-world\src\test\java\com\xiaoshan\mvnbook\helloworld\He11oWorldTest.java;[8,5]-source      1.3 中不支持注释(请使用-source 5 或更高版本以启用注释) @Test
[INFO]
[INFO For more information, run Maven with the switch

不幸的是构建失败了 , 先耐心分析一下这段输出。命令行输入的是 mvn clean test, 而 Maven 实际执行的可不止这两个任务 , 还 有 clean:cleanresources:resourcescompiler:compileresources:testResources 以及compiler:testCompile。 暂时需了解的是, 在Maven 执行测试 (test) 之前 , 它会先自动执行项目主资源处理、主代码编译、测试资源处理、测试代码编译等工作,这是 Maven 生命周期的一个特性。后续章节会详细解释 Maven 的生命周期。

从输出中还看到:Maven从中央仓库下载了junit-4.7.pomjunit-4.7.jar 这两个文件到本地仓库(~/. m2/repository)中,供所有Maven项目使用。

构建在执行 compiler:testCompile任务的时候失败了,Maven输出提示我们需要使用 source5或更高版本以启动注释,也就是前面提到的JUnit4的@Test注解。这是Maven初学者常常会遇到的一个问题。由于历史原因,Maven的核心插件之———compiler插件默认只支持编译Java1.3, 因此需要配置该插件使其支持Java5,如下所示:

<project>
	<build>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<configuration>
					<source>1.5</source>
					<target>1.5c/target>
				</configuration>
			</plugin>
		</plugins>
	</build>
</project>           

该 POM 省略了除插件配置以外的其他部分。我们暂且不去关心插件配置的细节,只需知道 compiler 插件支持Java5 的编译。现在再执行mvn clean test, 输出如下:

[INFO] [compiler:testCompile {execution:default-testCompile}]
[INFO]        Compiling 1 source file to D:\code\hello-world\target\test-classes
[INFO] [surefire:test {execution:default-test}]
[INFO]        Surefire report directory:D:\code\hello-world\target\surefire-reports
TESTS
Running com.xiaoshan.mvnbook.helloworld.He11oWorldTest
Tests run:1, Failures:0, Errors:0, Skipped:0,Time elapsed:0.055 sec
Results:
Tests run:1, Failures;0.Errors:0,Skipped:0
[INFO]
-------------------------------------------------------------------
[INFO] BUILD SUCCESSFUL
[INFO]
-------------------------------------------------------------------

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

4️⃣ 打包和运行

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

...
Tests run:1,Failures:0, Errors:0, Skipped:0

[INFO] [jar:jar {execution: default-jar}]
[INFO] Building jar:D:\code\hello-worid\target\hello-wor1d-1.0-SNAPSHOT.Jar
[INPO]
-------------------------------------------------------------------
[INPO] BUILD SUCCESSFUL
...

类似地, Maven 会在打包之前执行编译、测试等操作。这里看到 jar:jar 任务负责打包,实际上就是 jar 插件的 jar 目 标将项目主代码打包成一个名为 hello-world-1.0-SNAP-SHOT.jar 的文件。该文件也位于 target/ 输出目录中,它是根据 artifact-version.jar 规则进行命名的,如有需要,还可以使用 finalName 来自定义该文件的名称,这里暂且不展开,后面会详细解释。

至此,我们得到了项目的输出,如果有需要的话,就可以复制这个jar 文件到其他项目的 Classpath 中从而使用 HelloWonld 类。但是,如何才能让其他的Maven 项目直接引用这个jar呢? 还需要一个安装的步骤,执行 mvn clean install:

...
[INFO] [jar:jar {execution: default-jar}]
[INF0] Building jar:D:\code\hello-world\target\hello-world-1.0-SNAPSHOT.jar
[INFO] [install:install {execution:default-instal1}]
[INFO] Installing D:\code\hello-world\target\hello-wor1d-1.0-SNAPSHOT.jar to c:\Users\xiaoshan\.m2\repository\com\xiaoshan\mvnbook\hello-world\1.0-SNAPSHOTN\hello-world-1.0-SNAPSHOT.jar
[INFO]
-------------------------------------------------------------------
[INFO]   BUILD   SUCCESSFUL
...

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

我们已经体验了Maven最主要的命令:mvn clean compilemvn clean testmvn clean packagemvn 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</artifactId> 
	<version>1.2.1</version>
	<executions>
		<execution>
			<phase>package</phase>
			<goals>
				<goal>shade</goal>
			</goals>
			<configuration>
				<transformers>
					<transformer implementation="org.apache.maven.plugins.shade.resource. ManifestResourceTransformer">
						<mainClass>com.xiaoshan.mvnbook.helloworld.HelloWorld</mainclass>
					</transformer>	
				</transformers>
			</configuration>
		</execution>
	</executions>
</plugin>

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

Main-Class:com.xiaoshan.mvnbook.helloworld.HelloWorld

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

D:\code\hello-world>java-jartarget\hello-world-1.0-SNAPSHOT.jar
HelloMaven

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

5️⃣ 使用 Archetype生成项目骨架

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

还是以HelloWorld为例,我们使用maven 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,注意冒号的分隔,其格式为groupld:arifactld:version:goal, orgapache.maven.plugins是maven官方插件的groupId, maven-archetype-plugin是Archetype插件的artifactId, 2.0-alpha-5是目前该插件最新的稳定版,generate是要使用的插件目标。

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

Define value for groupId:: com.xiaoshan.mvnbook
Define value for artifactId::hello-world
Define value for version::1.0-SNAPSHOT
Define value for package:com.xiaoshan.mvnbook::com.xiaoshan.mvnbook.helloworld
Confirm properties configuration:
groupId: com.juvenxu.mvnbook
artifactId: hello-world
version: 1.0-SNAPSHOT
package: com.xiaoshan.mvnbook.helloworld
Y::Y

Archetype插件将根据我们提供的信息创建项目骨架。在当前目录下,Archetype插件会创建一个名为 hello-world(我们定义的artifactId)的子目录,从中可以看到项目的基本结构:基本的pom.xml已经被创建,里面包含了必要的信息以及一个junit依赖;主代码目录src/main/java已经被创建,在该目录下还有一个Java类com.xiaoshan.mvnbook.helloworld.App, 注意这里使用到了刚才定义的包名,而这个类也仅仅只有一个简单的输出Hello World!main方法;测试代码目录 src/test/java 也被创建好了,并且包含了一个测试用例com.xiaoshan.mvnbook.helloworld.AppTest

Archetype可以帮助我们迅速地构建起项目的骨架,在前面的例子中,我们完全可以在Archetype生成的骨架的基础上开发HelloWorld项目以节省大量时间。
此外,这里仅仅是看到了一个最简单的Archetype,如果有很多项目拥有类似的自定义项目结构以及配置文件,则完全可以一劳永逸地开发自己的Archetype,然后在这些项目中使用自定义的Archetype来快速生成项目骨架。后面我会详细阐述如何开发Maven Archetype。


【Maven教程】(三)基础使用篇:入门使用指南——POM编写、业务代码、测试代码、打包与运行、使用Archetype生成项目骨架~-LMLPHP
08-25 09:19