Maven基础使用篇
- 1️⃣ 编写 POM
- 2️⃣ 编写业务代码
- 3️⃣ 编写测试代码
- 4️⃣ 打包和运行
- 5️⃣ 使用 Archetype生成项目骨架
1️⃣ 编写 POM
到目前为止,已经大概了解并安装好了Maven环境, 现在,我们开始创建一个最简单的 Hello World
项目。如果你是初次接触 Maven, 建议按照本文的内容 一步步地编写代码并执行, 其中可能你会碰到一些概念暂时难以理解,但不用着急,记下这些疑难点,我在后续文章中会逐一进行解答。
就像 Make
的 Makefile
、Ant
的 build.xml
一样 ,Maven 项目的核心是 pom.xml
。POM (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。
这段代码中最重要的是包含 groupld
、artifactld
和 version
的三行。这三个元素定义了一个项目基本的坐标,在Maven 的世界,任何的 jar
、pom
或者 war
都是以基于这些基本的坐标进行区分的。
groupld
定义了项目属于哪个组,这个组往往和项目所在的组织或公司存在关联。譬如在 googlecode 上建立了一个名为 myapp 的项目,那么 groupld
就应该是 com. googlecode.myapp
, 如果你的公司是 mycom, 有一个项目为 myapp, 那 groupld
就应该是 com.mycom.myapp
。artifactld
定义了当前Maven 项目在组中唯一 的ID, 我们为这个 Hello World
项目定义 artifactld
为 hello-world
。 而在前面的 groupId
为 com. googlecode.myapp
的例子中,你可能会为不同的子项目(模块)分配 artifactId
, 如 myapp-util
、myapp-domain
、myapp-web
等。
顾名思义, version
指定了 Hello World
项目当前的版本 — — 1.0-SNAPSHOT
。SNAPSHOT 意为快照,说明该项目还处于开发中,是不稳定的版本。随着项目的发展, version
会不断更新,如升级为1.0
、1.1-SNAPSHOT
、1.1
、2.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 中定义的 groupId
和 artifactId
相吻合。 一般来说,项目中Java 类的包都应该基于项目的 groupId
和 artifactId
, 这样更加清晰,更加符合逻辑,也方便搜索构件或者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
元素以声明项目的依赖。这里添加了一个依赖 —— groupId
是 junit
, artifactId
是 junit
, version
是 4.7。前面提到 groupId
、artifactId
和 version
是任何一个 Maven 项目最基本的坐标,JUnit 也不例外,有了这段声明,Maven 就能够自动下载 junit-4.7.jar
。也许你会问,Maven 从哪里下载这个 jar 呢? 在 Maven 之前,可以去JUnit 的官方网站下载分发包,有了Maven, 它会自动访问中央仓库 (http://repol.maven.org/maven2/
), 下载需要的文件。大家也可以自己访问该仓库,打开路径 junit/junil/4.7/
, 就能 看到 junit-4.7.pom
和 junil-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:clean
、resources:resources
、compiler:compile
、resources:testResources
以及compiler:testCompile
。 暂时需了解的是, 在Maven 执行测试 (test) 之前 , 它会先自动执行项目主资源处理、主代码编译、测试资源处理、测试代码编译等工作,这是 Maven 生命周期的一个特性。后续章节会详细解释 Maven 的生命周期。
从输出中还看到:Maven从中央仓库下载了junit-4.7.pom
和 junit-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 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</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>
下面。我们配置了mainClass
为com.xiaoshan.mvnbook.helloworld.HelloWorld
, 项目在打包时会将该信息放到 MANIFEST
中。现在执行mvn clean install
, 待构建完成之后打开 target/
目录,可以看到 hello-world-1.0-SNAPSHOT.jar
和 original-hello-world-1.0-SNAPSHOT.jar
, 前者是带有 Main-Class
信息的可运行jar, 后者是原始的jar, 打开 hello-world-1.0-SNAPSHOT.jar
的 META-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会提示输入要创建项目的groupId
、artifactId
、version
以及包名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。