1. ~/.m2 文件
默认情况下,该文件夹下放置了 Maven 本地 仓库.m2/repository。所有的 Maven 构件(artifact)都被存储到该仓库中,以方便重用。
默认情况下,~/.m2 目录下除了 repository 仓库之外就没有其他目录和文件了,不过大多 数 Maven 用户需要复制 M2_HOME/conf/settings.xml 文件到~/.m2/settings.xml。
2. 配置用户范围settings.xml
Maven 用户可以选择配置$M2_HOME/conf/settings.xml 或者~/.m2/settings.xml。前者是全 局范围的,整台机器上的所有用户都会直接受到该配置的影响,而后者是用户范围的,只 有当前用户才会受到该配置的影响。
我们推荐使用用户范围的 settings.xml,主要原因是为了避免无意识地影响到系统中的其 他用户。当然,如果你有切实的需求,需要统一系统中所有用户的 settings.xml 配置,当 然应该使用全局范围的 settings.xml。
除了影响范围这一因素,配置用户范围 settings.xml 文件还便于 Maven 升级。直接修改 conf 目录下的 settings.xml 会导致 Maven 升级不便,每次升级到新版本的 Maven,都需要 复制 settings.xml 文件,如果使用~/.m2 目录下的 settings.xml,就不会影响到 Maven 安装 文件,升级时就不需要触动 settings.xml 文件。
3. pom
POM (Project Object Model,项目对象模型)定义了项目的基本信息,用于描述项目如何构 建,声明项目依赖,等等。
4. maven默认搜索代码的目录
项目主代码和测试代码不同,项目的主代码会被打包到最终的构件中(比如 jar),而测 试代码只在运行测试时用到,不会被打包。默认情况下,Maven 假设项目主代码位于 src/main/java 目录
我们应该把项目主代码 放到 src/main/java/目录下(遵循 Maven 的约定),而无须额外的配置,Maven 会自动搜 寻该目录找到项目主代码。
代码编写完毕后,我们使用 Maven 进行编译,在项目根目录下运行命令 mvn clean compile ,我们会得到如下输出:
[INFO] ------------------------------------------------------------------------
[INFO] Building Maven Hello World Project
[INFO] task-segment: [clean, compile]
[INFO] ------------------------------------------------------------------------
[INFO] [clean:clean {execution: default-clean}]
[INFO] Deleting directory D:codehello-world arget
[INFO] [resources:resources {execution: default-resources}]
[INFO] skip non existing resourceDirectory D: codehello-worldsrcmain esources [INFO] [compiler:compile {execution: default-compile}]
[INFO] Compiling 1 source file to D: codehello-world argetclasses
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESSFUL
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 1 second
[INFO] Finished at: Fri Oct 09 02:08:09 CST 2009
[INFO] Final Memory: 9M/16M
[INFO] ------------------------------------------------------------------------
clean 告诉 Maven 清理输出目录 target/,compile 告诉 Maven 编译项目主代码,从输出中 我们看到 Maven 首先执行了 clean:clean 任务,删除 target/目录,默认情况下 Maven 构建 的所有输出都在 target/目录中;接着执行 resources:resources 任务(未定义项目资源,暂 且略过);最后执行 compiler:compile 任务,将项目主代码编译至 target/classes 目录(编译 好的类为 com/juvenxu/mvnbook/helloworld/HelloWorld.Class)。
上文提到的 clean:clean、resources:resources,以及 compiler:compile 对应了一些 Maven 插 件及插件目标,比如 clean:clean 是 clean 插件的 clean 目标,compiler:compile 是 compiler 插件的 compile 目标,后文会详细讲述 Maven 插件及其编写方法。
5.
为了使项目结构保持清晰,主代码与测试代码应该分别位于独立的目录中。3.2 节讲过 Maven 项目中默认的主代码目录是 src/main/java,对应地,Maven 项目中默认的测试代码 目录是 src/test/java。
测试用例编写完毕之后就可以调用 Maven 执行测试,运行 mvn clean test :
[INFO] ------------------------------------------------------------------------
[INFO] Building Maven Hello World Project
[INFO] task-segment: [clean, test]
[INFO] ------------------------------------------------------------------------
[INFO] [clean:clean {execution: default-clean}]
[INFO] Deleting directory D:git-juvenmvnbookcodehello-world arget [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: codehello-world argetclasses
[INFO] [resources:testResources {execution: default-testResources}]
...
Downloading: http://repo1.maven.org/maven2/junit/junit/4.7/junit-4.7.jar 226K downloaded (junit-4.7.jar)
[INFO] [compiler:testCompile {execution: default-testCompile}]
[INFO] Compiling 1 source file to D: codehello-world arget est-classes [INFO] ------------------------------------------------------------------------
[ERROR] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Compilation failure
D:codehello-
worldsrc estjavacomjuvenxumvnbookhelloworldHelloWorldTest.java:[8,5] -source 1.3
中不支持注释
(请使用 -source 5 或更高版本以启用注释)
@Test
[INFO] ------------------------------------------------------------------------ [INFO] For more information, run Maven with the -e switch
不幸的是构建失败了,不过我们先耐心分析一下这段输出(为了本书的简洁,一些不重要 的信息我用省略号略去了)。命令行输入的是 mvn clean test,而 Maven 实际执行的可不 止 这 两 个 任 务 , 还 有 clean:clean 、 resources:resources 、 compiler:compile 、 resources:testResources 以及 compiler:testCompile。暂时我们需要了解的是,在 Maven 执 行测试(test)之前,它会先自动执行项目主资源处理,主代码编译,测试资源处理,测 试代码编译等工作,这是 Maven 生命周期的一个特性
构建在执行 compiler:testCompile 任务的时候失败了,Maven 输出提示我们需要使用- source 5 或更高版本以启动注释,也就是前面提到的 JUnit 4 的@Test 注解。这是 Maven 初学者常常会遇到的一个问题。由于历史原因,Maven 的核心插件之一 compiler 插件默 认只支持编译 Java 1.3,因此我们需要配置该插件使其支持 Java 5,见代码清单 2-5:
代码清单 2-5:配置 maven-compiler-plugin 支持 Java 5
...
<build> <plugins>
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration>
<source>1.5</source>
<target>1.5</target> </configuration>
</plugin> </plugins>
</build>
...
</project>
该 POM 省略了除插件配置以外的其他部分,我们暂且不去关心插件配置的细节,只需要 知道 compiler 插件支持 Java 5 的编译。
我们看到 compiler:testCompile 任务执行成功了,测试代码通过编译之后在 target/test- classes 下生成了二进制文件,紧接着 surefire:test 任务运行测试,surefire 是 Maven 世界 中负责执行测试的插件,这里它运行测试用例 HelloWorldTest,并且输出测试报告,显示 一共运行了多少测试,失败了多少,出错了多少,跳过了多少。显然,我们的测试通过了 ——BUILD SUCCESSFUL。
将项目进行编译、测试之后,下一个重要步骤就是打包(package)。Hello World 的 POM 中没有指定打包类型,使用默认打包类型 jar,我们可以简单地执行命令 mvn clean package 进行打包,可以看到如下输出:
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO] [jar:jar {execution: default-jar}]
[INFO] Building jar: D:codehello-world argethello-world-1.0-SNAPSHOT.jar [INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESSFUL
...
类似地,Maven 会在打包之前执行编译、测试等操作。这里我们看到 jar:jar 任务负责打 包,实际上就是 jar 插件的 jar 目标将项目主代码打包成一个名为 hello-world-1.0- SNAPSHOT.jar 的文件,该文件也位于 target/输出目录中,它是根据 artifact-version.jar 规 则进行命名的,如有需要,我们还可以使用 finalName 来自定义该文件的名称,这里暂且 不展开,本书后面会详细解释。
至此,我们得到了项目的输出,如果有需要的话,就可以复制这个 jar 文件到其他项目的 Classpath 中从而使用 HelloWorld 类。但是,如何才能让其他的 Maven 项目直接引用这个 jar 呢?我们还需要一个安装的步骤,执行 mvn clean install:
[INFO] [jar:jar {execution: default-jar}]
[INFO] Building jar: D: codehello-world argethello-world-1.0-SNAPSHOT.jar
[INFO] [install:install {execution: default-install}]
[INFO] Installing D:codehello-world argethello-world-1.0-SNAPSHOT.jar to
C:Usersjuven.m2 epositorycomjuvenxumvnbookhello-world1.0-SNAPSHOThello- world-1.0-SNAPSHOT.jar
[INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESSFUL
...
在打包之后,我们又执行了安装任务 install:install,从输出我们看到该任务将项目输出的 jar 安装到了 Maven 本地仓库中,我们可以打开相应的文件夹看到 Hello World 项目的 pom 和 jar。之前讲述 JUnit 的 POM 及 jar 的下载的时候,我们说只有构件被下载到本地仓库 后,才能由所有 Maven 项目使用,这里是同样的道理,只有将 Hello World 的构件安装到 本地仓库之后,其他 Maven 项目才能使用它。
我们已经将体验了 Maven 最主要的命令:mvn clean compile、mvn clean test、mvn clean package、mvn clean install。执行 test 之前是会先执行 compile 的,执行 package 之前是会 先执行 test 的,而类似地,install 之前会执行 package。我们可以在任何一个 Maven 项目 中执行这些命令,而且我们已经清楚它们是用来做什么的。
7. 运行jar包
默认打包生成的 jar 是不能够直接运行的,因为带有 main 方法的类信息不会添加 到 manifest 中(我们可以打开 jar 文件中的 META-INF/MANIFEST.MF 文件,将无法看到 Main-Class 一行)。为了生成可执行的 jar 文件,我们需要借助 maven-shade-plugin,配置该 插件如下:
<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.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/目录,我们可以看
到 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,可以看到它包含这样一行信息:
现在,我们在项目根目录中执行该 jar 文件:
D: codehello-world>java -jar targethello-world-1.0-SNAPSHOT.jar Hello Maven
控制台输出为 Hello Maven,这正是我们所期望的。
8. packaging
packaging:该元素定义 Maven 项目的打包方式。首先,打包方式通常与所生成构件的 文件扩展名对应,如上例中 packaging 为 jar,最终的文件名为 nexus-indexer- 2.0.0.jar,而使用 war 打包方式的 Maven 项目,最终生成的构件会有一个.war 文件, 不过这不是绝对的。其次,打包方式会影响到构建的生命周期,比如 jar 打包和 war 打包会使用不同的命令。最后,当我们不定义 packaging 的时候,Maven 会使用默认 值 jar。
依赖
9. 项目构件的文件名
项目构件的文件名是与坐标相对应的,一般的规则为 artifactId-version[- classifier].packaging,[-classifier]表示可选,比如上例 nexus-indexer 的主构件为 nexus- indexer-2.0.0.jar,附属构件有 nexus-indexer-2.0.0-javadoc.jar。这里还要强调的一点是, packaging 并非一定与构件扩展名对应,比如 packaging 为 maven-plugin 的构件扩展名为 jar。
10. 依赖范围
首先需要知道,Maven 在编译项目主代码的时候需要使用一套 classpath。在上例中,编译 项目主代码的时候需要用到 spring-core,该文件以依赖的方式被引入到 classpath 中。其 次,Maven 在编译和执行测试的时候会使用另外一套 classpath,上例中的 JUnit 就是一个 很好的例子,该文件也以依赖的方式引入到测试使用的 classpath 中,不同的是这里的依 赖范围是 test。最后,实际运行 Maven 项目的时候,又会使用一套 classpath,上例中的 spring-core 需要在该 classpath 中,而 JUnit 则不需要。
依赖范围就是用来控制依赖与这 3 种 classpath(编译 classpath、测试 classpath、运行 classpath)的关系,Maven 有以下几种依赖范围:
Compile:编译依赖范围。如果没有指定,就会默认使用该依赖范围。使用此依赖范 围的 Maven 依赖,对于编译、测试、运行三种 classpath 都有效。典型的例子是 spring-core,在编译、测试和运行的时候都需要使用该依赖。
Test:测试依赖范围。使用此依赖范围的 Maven 依赖,只对于测试 classpath 有效,在 编译主代码或者运行项目的使用时将无法使用此类依赖。典型的例子是 JUnit,它只有 在编译测试代码及运行测试的时候才需要。
Provided:已提供依赖范围。使用此依赖范围的 Maven 依赖,对于编译和测试 classpath 有效,但在运行时无效。典型的例子是 servlet-api,编译和测试项目的时候 需要该依赖,但在运行项目的时候,由于容器已经提供,就不需要 Maven 重复的引入 一遍。
Runtime:运行时依赖范围。使用此依赖范围的 Maven 依赖,对于测试和运行 classpath 有效,但在编译主代码时无效。典型的例子是 JDBC 驱动实现,项目主代码 的编译只需要 JDK 提供的 JDBC 接口,只有在执行测试或者运行项目的时候才需要实 现上述接口的具体 JDBC 驱动。
System:系统依赖范围。该依赖与 3 种 classpath 的关系,和 Provided 依赖范围完全一 致。但是,使用 System 范围的依赖时必须通过 systemPath 元素显式地指定依赖文件 的路径。由于此类依赖不是通过 Maven 仓库解析的,而且往往与本机系统绑定,可能 造成构建的不可移植,因此应该谨慎使用。systemPath 元素可以引用环境变量
Import(Maven 2.0.9 及以上):导入依赖范围。该依赖范围不会对 3 种 classpath 产生 实际的影响
上述除 import 以外的各种依赖范围与 3 种 classpath 的关系如表 3-1 所示:
依赖范围 (Scope) | 对于编译 classpath 有效 | 对于测试 classpath 有效 | 对于运行时 classpath 有效 |例子
compile Y Y Y spring-c
test - Y - JUnit
provided Y Y - servlet-api
runtime - Y Y JDBC 驱动实现
system Y Y - 本地的,Maven 仓库之外的 类库文件
11. 传递性依赖和依赖范围
第一直接依赖的范围和第二直接依赖 的范围决定了传递性依赖的范围,如表 3-2 所示,最左边一行表示第一直接依赖范围,最 上面一行表示第二直接依赖范围,中间的交叉单元格则表示传递性依赖范围:
表 3-2:依赖范围影响传递性依赖
仔细观察一下表格,可以发现这样的规律:当第二直接依赖的范围是 compile 的时候,传 递性依赖的范围与第一直接依赖的范围一致;当第二直接依赖的范围是 test 的时候,依赖 不会得以传递;当第二直接依赖的范围是 provided 的时候,只传递第一直接依赖范围也 为 provided 的依赖,且传递性依赖的范围同样为 provided;当第二直接依赖的范围是 runtime 的时候,传递性依赖的范围与第一直接依赖的范围一致,但 compile 例外,此时 传递性依赖的范围为 runtime。
12. 依赖调解
Maven 引入的传递性依赖机制,一方面大大简化和方便了依赖声明,大部分情况下我们只 需要关心项目的直接依赖是什么,而不用考虑这些直接依赖会引入什么传递性依赖。但有 时候,当传递性依赖造成问题的时候,我们就需要清楚地知道该传递性依赖是从哪条依赖 路径引入的。
例如,项目 A 有这样的依赖关系:A->B->C->X(1.0)、A->D->X(2.0),X 是 A 的传递性 依赖,但是两条依赖路径上有两个版本的 X,那么哪个 X 会被 Maven 解析使用呢?两个版 本都被解析显然是不对的,因为那会造成依赖重复,因此必须选择一个。
Maven 依赖调解 的第一原则是:路径最近者优先。该例中 X(1.0)的路径长度为 3,而 X(2.0)的路径长 度为 2,因此 X(2.0)会被解析使用。
依赖调解第一原则不能解决所有问题,比如这样的依赖关系:A->B->Y(1.0)、A->C->Y (2.0),Y(1.0)和 Y(2.0)的依赖路径长度是一样的,都为 2。那么到底谁会被解析使 用呢?
在 Maven 2.0.8 及之前的版本中,这是不确定的,但是从 Maven 2.0.9 开始,为了 尽可能避免构建的不确定性,Maven 定义了依赖调解的第二原则:第一声明者优先。
在依赖路径长度相等的前提下,在 POM 中依赖声明的顺序决定了谁会被解析使用,顺序最靠 前的那个依赖优胜。该例中,如果 B 的依赖声明在 C 之前,那么 Y(1.0)就会被解析使 用。
13. 排除依赖
传递性依赖会给项目隐式的引入很多依赖,这极大地简化了项目依赖的管理,但是有些时 候这种特性也会带来问题。例如,当前项目有一个第三方依赖,而这个第三方依赖由于某 些原因依赖了另外一个类库的 SNAPSHOT 版本,那么这个 SNAPSHOT 就会成为当前项目的 传递性依赖,而 SNAPSHOT 的不稳定性会直接影响到当前的项目,这时就需要排除掉该 SNAPSHOT,并且在当前项目中声明该类库的某个正式发布的版本。还有一些情况,你可 能也想要替换某个传递性依赖,比如 Sun JTA API,Hibernate 依赖于这个 JAR,但是由于版 权的因素,该类库不在中央仓库中,而Apache Geronimo项目有一个对应的实现,这时你 就可以排除 Sun JAT API,再声明 Geronimo 的 JTA API 实现,见代码清单 3-9。
代码清单 3-9:排除传递性依赖
<dependencies>
<dependency>
<groupId>com.juvenxu.mvnbook</groupId>
<artifactId>project-b</artifactId>
<version>1.0.0</version>
<exclusions>
<exclusion> <groupId>com.juvenxu.mvnbook</groupId> <artifactId>project-c</artifactId>
</exclusion> </exclusions>
</dependency> <dependency>
<groupId>com.juvenxu.mvnbook</groupId> <artifactId>project-c</artifactId> <version>1.1.0</version>
</dependency> </dependencies>
</project>
上述代码中,项目 A 依赖于项目 B,但是由于一些原因,不想引入传递性依赖 C,而是自 己显式地声明对于项目 C 1.1.0 版本的依赖。代码中使用 exclusions 元素声明排除依赖, exclusions 可以包含一个或者多个 exclusion 子元素,因此可以排除一个或者多个传递性依 赖。需要注意的是,声明 exclusion 的时候只需要 groupId 和 artifactId,而不需要 version 元素,这是因为只需要 groupId 和 artifactId 就能唯一定位依赖图中的某个依赖,换句话说,Maven 解析后的依赖中,不可能出现 groupId 和 artifactId 相同,但是 version 不同的两个依赖
14. 归类依赖
在 3.3.1 节中,有很多关于 Spring Framework 的依赖,它们分别是 org.springframework:spring-core:2.5.6 、 org.springframework:spring-beans:2.5.6 、 org.springframework:spring-context:2.5.6 和 org.springframework:spring-context- support:2.5.6,它们是来自同一项目的不同模块,因此,所有这些依赖的版本都是相同 的,而且可以预见,如果将来需要升级 Spring Framework,这些依赖的版本会一起升级。
对于这些 Spring Framework 来说,也应该在一个唯一的地方定义 版本,并且在 dependency 声明中引用这一版本,这样,在升级 Spring Framework 的时候 就只需要修改一处 <project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.juven.mvnbook.account</groupId>
<artifactId>account-email</artifactId>
<name>Account Email</name>
<version>1.0.0-SNAPSHOT</version>
<properties>
<springframework.version>2.5.6</springframework.version> </properties>
<dependencies>
<dependency> <groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${springframework.version}</version> </dependency>
<dependency> <groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${springframework.version}</version> </dependency>
<dependency> <groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${springframework.version}</version> </dependency>
<dependency> <groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>${springframework.version}</version> </dependency>
这里简单用到了 Maven 属性(本书 14.1 节详细介绍 Maven 属性),首先使用 properties 元素定义 Maven 属性,该例中定义了一个 springframework.version 子元素,其值为 2.5.6,有了这个属性定义之后,Maven 运行的时候会将 POM 中的所有的 ${springframework.version}替换成实际值 2.5.6,也就是说,可以使用美元符号和大括弧环 绕的方式引用 Maven 属性。然后,将所有 Spring Framework 依赖的版本值用这一属性引用表示
15. 优化依赖
在软件开发过程中,程序员会通过重构等方式不断地优化自己的代码,使其变得更简洁、 更灵活。同理,程序员也应该能够对 Maven 项目的依赖了然于胸,并对其进行优化,如 去除多余的依赖,显式地声明某些必要的依赖。
通过阅读本章前面的内容,读者应该能够了解到:Maven 会自动解析所有项目的直接依赖 和传递性依赖,并且根据规则正确判断每个依赖的范围,对于一些依赖冲突,也能进行调 节,以确保任何一个构件只有唯一的版本在依赖中存在。
在这些工作之后,最后得到的那 些依赖被称为已解析依赖(Resolved Dependency)。可以运行如下的命令查看当前项目的 已解析依赖:
mvn dependency:list
我们可以运行如下命令查看当前项目的依赖 树:
mvn dependency:tree
使用 dependency:list 和 dependency:tree 可以帮助我们详细了解项目中所有依赖的具体信 息,在此基础上,还有 dependency:analyze 一个工具可以帮助分析当前项目的依赖。
运行dependency:analyze结果中重要的是两个部分。
首先是 Used undeclared dependencies,意指项目中使用到 的,但是没有显式声明的依赖,这里是 spring-context。这种依赖意味着潜在的风险,当 前项目直接在使用它们,例如有很多相关的 JAVA import 声明,而这种依赖是通过直接依 赖传递进来的,当升级直接依赖的时候,相关传递性依赖的版本也可能发生变化,这种变 化不易察觉,但是有可能导致当前项目出错,例如由于接口的改变,当前项目中的相关代 码无法编译。这种隐藏的、潜在的威胁一旦出现,就往往需要耗费大量的时间来查明真 相。因此,显式声明任何项目中直接用到的依赖。
还有一个重要的部分是 Unused declared dependencies,意指项目中未使用的,但显 式声明的依赖,这里有 spring-core 和 spring-beans。需要注意的是,对于这样一类依赖, 我们不应该简单地直接删除其声明,而是应该仔细分析。由于 dependency:analyze 只会分 析编译主代码和测试代码需要用到的依赖,一些执行测试和运行时需要的依赖它就发现不 了,很显然,该例中的 spring-core 和 spring-beans 是运行 Spring Framework 项目必要的类 库,因此不应该删除依赖声明。当然,有时候确实能通过该信息找到一些没用的依赖,但 一定要小心测试。
仓库
16 .
依赖范围 (Scope) |
对于编译 classpath 有效 |
对于测试 classpath 有效 |
对于运行时 classpath 有效 |
|