Maven 基本流程

  |  

摘要: 本文首先介绍了 Maven 的基本信息和安装,然后通过 HelloWorld 介绍基本流程。

【对算法,数学,计算机感兴趣的同学,欢迎关注我哈,阅读更多原创文章】
我的网站:潮汐朝夕的生活实验室
我的公众号:算法题刷刷
我的知乎:潮汐朝夕
我的github:FennelDumplings
我的leetcode:FennelDumplings


Maven 基本信息

Maven 不是 Java 领域唯一的构建管理的解决方案。其他构建解决方案还有 IDE、Make 和 Ant 等。

测试驱动开发(TDD):TDD 强调测试先行,所有产品都应该由测试用例覆盖。而测试是 Maven 生命周期的最重要的组成部分之一,并且 Maven 有现成的成熟插件支持业界流行的测试框架,如 JUnit 和 TestNG。

快速构建:快速构建强调我们能够随时快速地从源码构建出最终的产品。这正是 Maven 所擅长的,只需要一些配置,之后用一条简单的命令就能让 Maven 帮你清理、编译、测试、打包、部署,然后得到最终的产品。

持续集成(CI):CI 强调项目以很短的周期(如 15 分钟)集成最新的代码。实际上,CI 的前提是源码管理系统和构建系统。目前业界流行的 CI 服务器如 Hudson 和 CruiseControl 都能很好地和 Maven 进行集成。也就是说,使用 Maven 后,持续集成会变得更加方便。

富有信息的工作区:这条实践强调开发者能够快速方便地了解到项目的最新状态。当然,Maven 并不会帮你把测试覆盖率报告贴到墙上,也不会在你的工作台上放个鸭子告诉你构建失败了。不过使用 Maven 发布的项目报告站点,并配置你需要的项目报告,如测试覆盖率报告,都能帮你把信息推送到开发者眼前。

在传统的瀑布模型开发中,项目依次要经历需求开发、分析、设计、编码、测试和集成发布阶段。从设计和编码阶段开始,就可以使用 Maven 来建立项目的构建系统。在设计阶段,也完全可以针对设计开发测试用例,然后再编写代码来满足这些测试用例。然而,有了自动化构建系统,我们可以节省很多手动的测试时间。


安装

1
sudo apt install maven

验证成功

1
mvn --version

结果如下,可以看到安装的是 Maven 3。

1
2
3
4
5
Apache Maven 3.6.0
Maven home: /usr/share/maven
Java version: 11.0.15, vendor: Private Build, runtime: /usr/lib/jvm/java-11-openjdk-amd64
Default locale: en_US, platform encoding: UTF-8
OS name: "linux", version: "4.15.0-177-generic", arch: "amd64", family: "unix"

HelloWorld 项目

到目前为止,已经大概了解并安装好了 Maven,现在,我们开始创建一个最简单的 Hello World 项目。通过这个项目我们需要关注以下内容

  • 编写 POM
  • 编写主代码
  • 编写测试代码
  • 打包
  • 安装
  • 运行

(1) pom.xml

就像 Make 的 Makefile、Ant 的 build.xml 一样,Maven 项目的核心是 pom.xml。

POM(Project Object Model,项目对象模型)定义了项目的基本信息,用于描述项目如何构建,声明项目依赖,等等。

首先创建一个名为 hello-world 的文件夹,打开该文件夹,新建一个名为 pom.xml 的文件。

1
2
3
mkdir hello-world
cd hello-world
touch pom.xml

然后输入内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<?xml version="1.0"encoding="UTF-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.0http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.czx.helloworld</groupId>
<artifactId>hello-world</artifactId>
<version>1.0-SNAPSHOT</version>
<name>Maven Hello World Project</name>

<build>
<plugins> <!-- plugins 要写在 build 标签下,不然无法识别 -->
<plugin>
<artifactId> maven-compiler-plugin</artifactId>
<version>3.1</version> <!-- maven-compiler-plugin 版本写一个中央仓库有的版本,保存后它会自动下载 -->
<configuration>
<source>11</source> <!-- 我的 jdk 是 11 的 -->
<target>11</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.6</source>
<target>1.6</target>
</configuration>
</plugin>
</plugins>
</build>

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

第一行是 XML 头,指定了该 xml 文档的版本和编码方式。

紧接着是 project 元素,project 是所有 pom.xml 的根元素,它还声明了一些 POM 相关的命名空间及 xsd 元素,虽然这些属性不是必须的,但使用这些属性能够让第三方工具(如IDE中的XML编辑器)帮助我们快速编辑 POM。

根元素下的第一个子元素 modelVersion 指定了当前POM模型的版本,对于 Maven 3 来说,它只能是 4.0.0。

这段代码中最重要的是包含 groupId、artifactId 和 version 的三行。这三个元素定义了一个项目基本的坐标,在 Maven 中,任何的 jar、pom 或者 war 都是以基于这些基本的坐标进行区分的。

  • groupId 定义了项目属于哪个组,这个组往往和项目所在的组织或公司存在关联。譬如在 googlecode 上建立了一个名为 myapp 的项目,那么 groupId 就应该是 com.googlecode.myapp,如果你的公司是 mycom,有一个项目为 myapp,那么 groupId 就应该是 com.mycom.myapp。

  • artifactId 定义了当前 Maven 项目在组中唯一的 ID,我们为这个 Hello World 项目定义 artifactId 为 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 等。

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

下面有一个 plugins,要放在 build 标签下。这里主要是设置 Java 版本为 11,与 jdk 版本一致。以及设置 Maven 的核心插件之一的 compiler 件支持 Java 6。再比如有时候用 spring 也会写这个配置。

1
2
3
4
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>

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

(2) 编写主代码

项目的主代码会被打包到最终的构件中(如 jar),而测试代码只在运行测试时用到,不会被打包。

默认情况下,Maven 假设项目主代码位于 src/main/java 目录,我们遵循 Maven 的约定,创建该目录,然后在该目录下创建文件 com/czx/helloworld/HelloWorld.java

1
2
3
4
mkdir -p src/main/java/
cd src/main/java/
mkdir -p com/czx/helloworld/
touch com/czx/helloworld/HelloWorld.java

然后写代码内容:

1
2
3
4
5
6
7
8
9
10
11
package com.czx.helloworld;

public class HelloWorld {
public static void main (String[] args) {
System.out.println(new HelloWorld().sayHello());
}

public String sayHello () {
return "Hello Maven";
}
}

有两点需要注意:

(1) 绝大多数情况下,应该把项目主代码放到 src/main/java/ 目录下(遵循 Maven 的约定),而无须额外的配置,Maven 会自动搜寻该目录找到项目主代码。

(2) 该 Java 类的包名是 com.czx.helloworld,这与之前在 POM 中定义的 groupId 和 artifactId 相吻合。一般来说,项目中 Java 类的包都应该基于项目的 groupId 和 artifactId,这样更加清晰,更加符合逻辑,也方便搜索构件或者 Java 类。

在项目根目录下执行以下命令进行编译:

1
mvn clean compile

clean 告诉 Maven 清理输出目录 target/,compile 告诉 Maven 编译项目主代码,从输出中看到 Maven 首先执行了 clean:clean 任务,删除 target/ 目录。

默认情况下,Maven 构建的所有输出都在 target/ 目录中;接着执行 resources:resources 任务(未定义项目资源,暂且略过);最后执行 compiler:compile 任务,将项目主代码编译至 target/classes 目录(编译好的类为 target/classes/com/czx/helloworld/HelloWorld.class)。

(3) 写测试代码

Maven 项目中默认的测试代码目录是 src/test/java 。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?xml version="1.0"encoding="UTF-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.0http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.czx.helloworld</groupId>
<artifactId>hello-world</artifactId>
<version>1.0-SNAPSHOT</version>
<name>Maven Hello World Project</name>

<build>
<plugins> <!-- plugins 要写在 build 标签下,不然无法识别 -->
<plugin>
<artifactId> maven-compiler-plugin</artifactId>
<version>3.1</version> <!-- maven-compiler-plugin 版本写一个中央仓库有的版本,保存后它会自动下载 -->
<configuration>
<source>11</source> <!-- 我的 jdk 是 11 的 -->
<target>11</target>
</configuration>
</plugin>
</plugins>
</build>

<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 就能够访问中央仓库( http://repo1.maven.org/maven2/ ),自动下载 junit-4.7.jar。

配置了测试依赖,接着就可以编写测试类。现在要测试该类的 sayHello() 方法,检查其返回值是否为 “Hello Maven”。

在 src/test/java 目录下创建文件。

1
2
3
mkdir -p src/test/java
cd src/test/java
touch HelloWorldTest.java

然后写测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.czx.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.sayHello();
assertEquals("Hello Maven", result);
}
}

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

  1. 准备测试类及数据;
  2. 执行要测试的行为;
  3. 检查结果。

在 JUnit 4 中,需要执行的测试方法都应该以 @Test 进行标注。

在项目根目录下执行以下命令进行测试:

1
mvn clean test

target/surefire-reports 目录下记录测试结果。如果测试没通过,可以在该目录下排查。

注意:命令行输入的是 mvn clean test,而 Maven 实际执行的可不止这两个任务,还有 clean:clean、resources:resources、compiler:compile、resources:testResources 以及 compiler:testCompile。

在 Maven 执行测试(test)之前,它会先自动执行项目主资源处理、主代码编译、测试资源处理、测试代码编译等工作,这是 Maven 生命周期的一个特性。

(4) 打包

将项目进行编译、测试之后,下一个重要步骤就是打包(package)。POM 中没有指定打包类型,则使用默认打包类型 jar。

执行以下命令进行打包:

1
mvn clean package

至此我们得到项目输出 target/hello-world-1.0-SNAPSHOT.jar,此后如果有需要的话,就可以复制这个 jar 文件到其他项目的 Classpath 中从而使用 HelloWorld 类。

(5) 安装

要让其他的 Maven 项目直接引用这个 jar,还需要一个安装的步骤,执行:

1
mvn clean install

在打包之后,又执行了安装任务 install:install。从输出可以看到该任务将项目输出的 jar 安装到了 Maven 本地仓库中,可以打开相应的文件夹看到 Hello World 项目的 pom 和 jar。只有将 Hello World 的构件安装到本地仓库之后,其他 Maven 项目才能使用它。

(6) 运行

HelloWorld 类有一个 main 方法的。默认打包生成的 jar 是不能够直接运行的,因为带有 main 方法的类信息不会添加到 manifest 中(打开 jar 文件中的 META-INF/MANIFEST.MF 文件,将无法看到 Main-Class 一行)。

为了生成可执行的 jar 文件,需要借助 maven-shade-plugin,配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<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.czx.helloworld.HelloWorld</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>

plugin 元素在 POM 中的相对位置在 <project><build><plugins> 下面。

我们配置了 mainClass 为 com.czx.helloworld.HelloWorld,项目在打包时会将该信息放到 MANIFEST 中。

现在执行 mvn clean install,待构建完成之后打开 target/ 目录,可以看到两个 jar 文件。

1
2
hello-world-1.0-SNAPSHOT.jar
original-hello-world-1.0-SNAPSHOT.jar

前者是带有 Main-Class 信息的可运行 jar,后者是原始的 jar。

进入项目根目录,以下命令都可以运行带有 Main-Class 信息的 jar 文件。

1
java -jar target/hello-world-1.0-SNAPSHOT.jar
1
java -classpath target/hello-world-1.0-SNAPSHOT.jar com.czx.helloworld.HelloWorld

使用Archetype生成项目骨架

前面的 Hello World 项目中有一些 Maven 的约定:在项目的根目录中放置 pom.xml,在 src/main/java 目录中放置项目的主代码,在 src/test/java 中放置项目的测试代码。

些基本的目录结构和 pom.xml 文件内容称为项目的骨架。Maven 提供了 Archetype 以帮助我们快速勾勒出项目骨架。

还是以 Hello World 为例,用 maven archetype 来创建该项目的骨架,离开当前的 Maven 项目目录,运行以下命令:

1
mvn archetype:generate

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

1
2
3
4
Define value for property 'groupId':com.czx.helloworld
Define value for property 'artifactId':hello_world
Define value for property 'version' 1.0-SNAPSHOT: :
Define value for property 'package' com.czx.helloworld: :

然后就会让你确认以下信息:

1
2
3
4
5
6
Confirm properties configuration:
groupId: com.czx.helloworld
artifactId: hello_world
version: 1.0-SNAPSHOT
package: com.czx.helloworld
Y: :

在当前目录下,Archetype 插件会创建一个名为 hello_world(我们定义的artifactId)的子目录,从中可以看到项目的基本结构:

基本的 pom.xml 已经被创建,里面包含了必要的信息以及一个 junit 依赖,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
<?xml version="1.0" encoding="UTF-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/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.czx.helloworld</groupId>
<artifactId>hello_world</artifactId>
<version>1.0-SNAPSHOT</version>

<name>hello_world</name>
<!-- FIXME change it to the project's website -->
<url>http://www.example.com</url>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.7</maven.compiler.source>
<maven.compiler.target>1.7</maven.compiler.target>
</properties>

<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
<plugins>
<!-- clean lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#clean_Lifecycle -->
<plugin>
<artifactId>maven-clean-plugin</artifactId>
<version>3.1.0</version>
</plugin>
<!-- default lifecycle, jar packaging: see https://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_jar_packaging -->
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>3.0.2</version>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.1</version>
</plugin>
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<version>3.0.2</version>
</plugin>
<plugin>
<artifactId>maven-install-plugin</artifactId>
<version>2.5.2</version>
</plugin>
<plugin>
<artifactId>maven-deploy-plugin</artifactId>
<version>2.8.2</version>
</plugin>
<!-- site lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#site_Lifecycle -->
<plugin>
<artifactId>maven-site-plugin</artifactId>
<version>3.7.1</version>
</plugin>
<plugin>
<artifactId>maven-project-info-reports-plugin</artifactId>
<version>3.0.0</version>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>

主代码目录 src/main/java 已经被创建,在该目录下还有一个 Java 类 com.czx.helloword.App,这里使用到了刚才定义的包名,而这个类也仅仅只有一个简单的输出 Hello World!的 main 方法,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.czx.helloworld;

/**
* Hello world!
*
*/
public class App
{
public static void main( String[] args )
{
System.out.println( "Hello World!" );
}
}

测试代码目录 src/test/java 也被创建好了,并且包含了一个测试用例 com.czx.helloworld.AppTest,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.czx.helloworld;

import static org.junit.Assert.assertTrue;

import org.junit.Test;

/**
* Unit test for simple App.
*/
public class AppTest
{
/**
* Rigorous Test :-)
*/
@Test
public void shouldAnswerWithTrue()
{
assertTrue( true );
}
}

Share