[关闭]
@lemonguge 2017-09-13T14:50:02.000000Z 字数 11128 阅读 1087

Maven入门

Maven


Maven 主要服务于基于 Java 平台的项目构建、依赖管理和项目信息管理

Maven 这个词可以翻译为 “知识的积累”,也可以翻译为 “专家” 或 “内行”。我们会发现,除了编写源代码,我们每天有相当一部分时间花在了编译、运行单元测试、生成文档、打包和部署等烦琐且不起眼的工作上,这就是构建。

Maven 是优秀的构建工具,能够帮我们自动化构建过程,从清理、编译、测试到生成报告,再到打包和部署。我们要做的是使用 Maven 配置好项目,然后输入简单的命令,Maven 会帮我们处理那些烦琐的任务。

Maven 对于项目目录结构、测试用例命名方式等内容都有既定的规则,只要遵循了这些成熟的规则,用户在项目间切换的时候就免去了额外的学习成本,可以说是约定优于配置(Convention Over Configuration)。


安装和配置

在 Windows 上进行安装 Maven,首先要确认你已经正确安装了 JDK。下载好 Maven 的安装包,将安装包解压到指定的目录中,接着需要设置环境变量。在系统变量中新建一个变量,变量名为 MAVEN_HOME,变量值为 Maven 的安装目录。单击 “确定” 按钮,接着在系统变量中找到一个名为 Path 的变量,在变量值的末尾加上 %MAVEN_HOME%\bin;。注意:多个值之间需要有分号隔开,然后单击 “确定” 按钮。至此,环境变量设置完成。

值得注意的是 Path 环境变量。当我们在 cmd 中输入命令时,Windows 首先会在当前目录中寻找可执行文件或脚本,如果没有找到,Windows 会接着遍历环境变量 Path 中定义的路径。由于将 %MAVEN_HOME%\bin 添加到了 Path 中,而这里 %MAVEN_HOME% 实际上是引用了前面定义的另一个变量,其值是 Maven 的安装目录。因此,Windows 会在执行命令时搜索 Maven 的安装目录下的 bin 目录,而 mvn 执行脚本的位置就是这里。

  1. C:\Users\lemonguge>echo %MAVEN_HOME%
  2. D:\Program Files\Maven-3.3.3
  3. C:\Users\lemonguge>mvn -v
  4. Apache Maven 3.3.3 (7994120775791599e205a5524ec3e0dfe41d4a06; 2015-04-22T19:57:37+08:00)
  5. Maven home: D:\Program Files\Maven-3.3.3\bin\..
  6. Java version: 1.8.0_20, vendor: Oracle Corporation
  7. Java home: D:\Program Files\Java\jdk1.8.0_20\jre
  8. Default locale: zh_CN, platform encoding: GBK
  9. OS name: "windows 8.1", version: "6.3", arch: "amd64", family: "dos"

在 Windows 上升级 Maven 非常简便,只需要下载新的 Maven 安装文件,解压至本地目录,然后更新 MAVEN_HOME 环境变量即可。同理,如果需要使用某一个旧版本的 Maven,也只需要编辑 MAVEN_HOME 环境变量指向旧版本的安装目录。


安装目录分析

前面讲到设置 MAVEN_HOME 环境变量指向 Maven 的安装目录,下面看一下该目录的结构和内容:


  1. bin 目录中包含了 mvn 运行的脚本,这些脚本用来配置 Java 命令,准备好 classpath 和相关的 Java 系统属性,然后执行 Java 命令。其中 mvn 是基于 UNIX 平台的 shell 脚本,mvn.bat 是基于 Windows 平台的 bat 脚本。在命令行输入任何一条 mvn 命令时,实际上就是在调用这些脚本。
  2. boot 目录只包含一个文件,该文件为 plexus-classworlds-x.x.x.jar,plexus-classworlds 是一个类加载器框架,相对于默认的 java 类加载器,它提供了更丰富的语法以方便配置,Maven 使用该框架加载自己的类库。
  3. conf 目录包含了一个非常重要的文件 settings.xml。直接修改该文件,就能在机器上全局地定制 Maven 的行为。一般情况下,我们更偏向于复制该文件至 ~/.m2/ 目录下(~表示用户目录),然后修改该文件,在用户范围定制 Maven 的行为。
  4. lib 目录包含了所有 Maven 运行时需要的 Java 类库,值得一提的是,用户可以在这个目录中找到 Maven 内置的超级 POM(maven-model-builder-3.0.4.jar 中的 org/apache/maven/model/pom-4.0.0.xml 文件)。
  5. LICENSE 记录了 Maven 使用的软件许可证 Apache License Version 2.0。
  6. NOTICE 记录了 Maven 包含的第三方软件
  7. README.txt则包含了 Maven 的简要介绍,包括安装需求及如何安装的简要指令等。

~/.m2

打开用户目录,当前目录 C:\Users\lemonguge\,在用户目录下可以发现 .m2 文件夹。默认情况下,该文件夹下放置了 Maven 本地仓库 .m2/repository。所有的 Maven 构件都被存储到该仓库中,以方便重用。默认情况下,~/.m2 目录下除了 repository 仓库之外就没有其他目录和文件了,不过大多数 Maven 用户需要复制 M2_HOME/conf/settings.xml 文件到 ~/.m2/settings.xml。

Maven 用户可以选择配置 $MAVEN_HOME/conf/settings.xml 或者 ~/.m2/settings.xml。前者是全局范围的,整台机器上的所有用户都会直接受到该配置的影响,而后者是用户范围的,只有当前用户才会受到该配置的影响。

推荐使用用户范围的 settings.xml,主要是为了避免无意识地影响到系统中的其他用户


HTTP代理

基于安全因素考虑,需要通过安全认证的代理访问因特网。这种情况下,就需要为 Maven 配置 HTTP 代理,才能让它正常访问外部仓库,以下载所需要的资源。
1. 确认自己无法直接访问公共的 Maven 中央仓库,直接运行命令 ping repo.maven.apache.org 可以检查网络
2. 检查一下代理服务器是否畅通,如现在有一个IP地址为 218.14.227.197,端口为 3128 的代理服务,我们可以运行 telnet 218.14.227.197 3128 来检测该地址的该端口是否畅通
3. 编辑 ~/.m2/settings.xml 文件(如果没有该文件,则复制 $M2_HOME/conf/settings.xml)

  1. <settings>
  2. <proxies>
  3. <proxy>
  4. <id>my-proxy</id>
  5. <active>true</active>
  6. <protocol>http</protocol>
  7. <host>218.14.227.197</host>
  8. <port>3128</port>
  9. <!--
  10. <username>proxyuser</username>
  11. <password>proxypass</password>
  12. <nonProxyHosts>repository.mycom.com|*.google.com</nonProxyHosts>
  13. -->
  14. </proxy>
  15. </proxies>
  16. </settings>

proxies 下可以有多个 proxy 元素,如果声明了多个 proxy 元素,则默认情况下第一个被激活的 proxy 会生效。这里声明了一个 idmy-proxy 的代理,active 的值为 true 表示激活该代理,protocol 表示使用的代理协议,这里是 http。当然,最重要的是指定正确的主机名(host 元素)和端口(port 元素)。上述 XML 配置中注释掉了 usernamepasswordnonProxyHost 几个元素。当代理服务需要认证时,就需要配置 usernamepasswordnonProxyHost 元素用来指定哪些主机名不需要代理,可以使用 “|” 符号来分隔多个主机名。此外,该配置也支持通配符,如 *.google.com 表示所有以 google.com 结尾的域名访问都不要通过代理。


POM初识

Maven项目的核心是 pom.xml,POM(Project Object Model,项目对象模型)

pox.xml 定义了项目的基本信息,用于描述项目如何构建,声明项目依赖,等等。现在看一个最简单的 pom.xml:

  1. <?xml version="1.0"encoding="UTF-8"?>
  2. <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  3. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
  4. <modelVersion>4.0.0</modelVersion>
  5. <groupId>cn.homjie.maven</groupId>
  6. <artifactId>maven-core</artifactId>
  7. <version>1.0-SNAPSHOT</version>
  8. <name>Maven Hello World Project</name>
  9. </project>

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


默认约定

pom.xml 所在的目录应为项目的根目录,假设该目录为 ${project.basedir},那么 Maven 有以下假设:

  1. ${project.basedir}/src/main/java,存放项目的 .java 文件
  2. ${project.basedir}/src/main/resources,存放项目资源文件,如 spring,hibernate 配置文件
  3. ${project.basedir}/src/main/webapp,如果是 web 项目,作为 web 应用文件目录
  4. ${project.basedir}/src/test/jave,存放所有测试 .java 文件,如 JUnit 测试类
  5. ${project.basedir}/src/test/resources,测试资源文件
  6. ${project.basedir}/target,项目输出位置
  7. ${project.basedir}/target/classes,编译输出目录
  8. ${project.basedir}/target/test-classes,编译输出目录
  9. ${project.basedir}/target/site,项目 site 输出目录
  10. **/*Test.java**/Test*.java**/*TestCase.java,Maven 只会自动运行符合该命名规则的测试类
  11. jar,Maven 默认打包格式

运行一条 mvn clean package 命令,Maven 会帮你清除 target 目录,重新建一个空的,编译src/main/java 类至 target/classes,复制 src/main/resources 的文件至 target/classes;编译 src/test/java 至 target/test-classes,复制 src/test/resources 的文件至 target/test-classes;然后运行所有测试,测试通过后,使用 jar 命令打包,存储于 target 目录。

遵循约定会带来很多好处,显而易见,配置大量减少了,减少了交流学习的时间。如果不想遵守,Maven 也允许自定义,首先,问自己三遍,你真的需要这样做么?如果仅仅是因为喜好,那么别耍个性,个性意味着牺牲通用性,意味着增加无谓的复杂度。

可以通过覆盖超级 POM 的配置来实现自定义,以下是超级 POM 的一段配置:

  1. <project>
  2. <build>
  3. <!-- 输出主构件的名称 -->
  4. <finalName>${project.artifactId}-${project.version}</finalName>
  5. <!-- 输出目录 -->
  6. <directory>${project.basedir}/target</directory>
  7. <!-- 主源码目录 -->
  8. <sourceDirectory>${project.basedir}/src/main/java</sourceDirectory>
  9. <!-- 脚本源码目录 -->
  10. <scriptSourceDirectory>${project.basedir}/src/main/scripts</scriptSourceDirectory>
  11. <!-- 测试源码目录 -->
  12. <testSourceDirectory>${project.basedir}/src/test/java</testSourceDirectory>
  13. <!-- 主源码输出目录 -->
  14. <outputDirectory>${project.build.directory}/classes</outputDirectory>
  15. <!-- 测试源码输出目录 -->
  16. <testOutputDirectory>${project.build.directory}/test-classes</testOutputDirectory>
  17. <resources>
  18. <!-- 主资源目录 -->
  19. <resource>
  20. <directory>${project.basedir}/src/main/resources</directory>
  21. </resource>
  22. </resources>
  23. <testResources>
  24. <!-- 测试资源目录 -->
  25. <testResource>
  26. <directory>${project.basedir}/src/test/resources</directory>
  27. </testResource>
  28. </testResources>
  29. </build>
  30. </project>

坐标和依赖

坐标详解

正如上文所述,Maven 的一大功能是管理项目依赖。为了能自动化地解析任何一个 Java 构件,Maven 就必须将它们唯一标识,这就依赖管理的底层基础——坐标。

世界上任何一个构件都可以使用 Maven 坐标唯一标识

Maven 坐标的元素包括 groupIdartifactIdversionpackagingclassifier,只要我们提供正确的坐标元素,Maven 就能找到对应的构件。Maven 内置了一个中央仓库的地址(https://repo.maven.apache.org/maven2),该地址在超级 POM 文件中配置好了。

groupIdartifactIdversion 是必须定义的,packaging 是可选的(默认为 jar),而 classifier 是不能直接定义的。项目构件的文件名是与坐标相对应的,一般的规则为 artifactId-version[-classifier].packaging,[-classifier]表示可选。值得注意的是,packaging 并非一定与构件扩展名对应,比如 packagingmaven-plugin 的构件扩展名为 jar

依赖配置

首先看下一个完整的依赖声明,包含如下元素:

  1. <project>
  2. <dependencies>
  3. <dependency>
  4. <groupId></groupId>
  5. <artifactId></artifactId>
  6. <version></version>
  7. <type></type>
  8. <scope></scope>
  9. <!-- scope为system,必须通过 systemPath元素显式地指定依赖文件的路径 -->
  10. <systemPath></systemPath>
  11. <optional></optional>
  12. <exclusions>
  13. <exclusion>
  14. <groupId></groupId>
  15. <artifactId></artifactId>
  16. </exclusion>
  17. </exclusions>
  18. </dependency>
  19. <!-- many dependency declare -->
  20. </dependencies>
  21. </project>

根元素 project 下的 dependencies 可以包含一个或者多个 dependency 元素,以声明一个或者多个项目依赖。每个依赖可以包含的元素有:

Maven 解析后的依赖中,不可能出现 groupIdartifactId 相同,但是 version 不同的两个依赖

依赖范围

Maven 有三种 classpath,Maven 在编译项目主代码的时候需要使用一套 classpath,编译和执行测试的时候会使用另外一套 classpath,实际运行 Maven 项目的时候,又会使用一套 classpath

依赖范围就是用来控制依赖与这三种 classpath(编译 classpath、测试 classpath、运行 classpath)的关系,Maven 有以下几种依赖范围:

  1. compile:编译依赖范围。如果没有指定,就会默认使用该依赖范围。使用此依赖范围的 Maven 依赖,对于编译、测试、运行三种 classpath 都有效。典型的例子是 spring-core,在编译、测试和运行的时候都需要使用该依赖。
  2. test:测试依赖范围。使用此依赖范围的 Maven 依赖,只对于测试 classpath 有效,在编译主代码或者运行项目的使用时将无法使用此类依赖。典型的例子是 JUnit,它只有在编译测试代码及运行测试的时候才需要。
  3. provided:已提供依赖范围。使用此依赖范围的 Maven 依赖,对于编译和测试 classpath 有效,但在运行时无效。典型的例子是 servlet-api,编译和测试项目的时候需要该依赖,但在运行项目的时候,由于容器已经提供,就不需要 Maven 重复地引入一遍。
  4. runtime:运行时依赖范围。使用此依赖范围的 Maven 依赖,对于测试和运行 classpath 有效,但在编译主代码时无效。典型的例子是 JDBC 驱动实现,项目主代码的编译只需要 JDK 提供的 JDBC 接口,只有在执行测试或者运行项目的时候才需要实现上述接口的具体 JDBC 驱动。
  5. system:系统依赖范围。该依赖与三种 classpath 的关系,和 provided 依赖范围完全一致。但是,使用 system 范围的依赖时必须通过 systemPath 元素显式地指定依赖文件的路径。由于此类依赖不是通过 Maven 仓库解析的,而且往往与本机系统绑定,可能造成构建的不可移植,因此应该谨慎使用。systemPath 元素可以引用环境变量,如 <systemPath>${java.home}/lib/rt.jar</systemPath>
  6. import:导入依赖范围。该依赖范围不会对三种 classpath 产生实际的影响。

依赖范围 对于编译 classpath 有效 对于测试 classpath 有效 对于运行 classpath 有效 例子
compile Y Y Y spring-core
test - Y - JUnit
provided Y Y - servlet-api
runtime - Y Y JDBC 驱动实现
system Y Y - 本地的,Maven 仓库之外的类库文件

传递性依赖

有了传递性依赖机制,在使用依赖的时候就不用去考虑它依赖了什么,也不用担心引入多余的依赖。Maven 会解析各个直接依赖的 POM,将那些必要的间接依赖,以传递性依赖的形式引入到当前的项目中。

依赖范围不仅可以控制依赖与三种 classpath 的关系,还对传递性依赖产生影响。假设 A 依赖于 B,B 依赖于 C,我们说 A 对于 B 是第一直接依赖,B 对于 C 是第二直接依赖,A 对于 C 是传递性依赖。


第一直接依赖范围\第二直接依赖范围 compile test provided runtime
compile compile - - runtime
test test - - test
provided provided - provided provided
runtime runtime - - runtime

通过上面的表格,可以得出一下结论:

  1. 当第二直接依赖的范围是 compile 的时候,传递性依赖的范围与第一直接依赖的范围一致
  2. 当第二直接依赖的范围是 test 的时候,依赖不会得以传递
  3. 当第二直接依赖的范围是 provided 的时候,只传递第一直接依赖范围也为 provided 的依赖,且传递性依赖的范围同样为 provided
  4. 当第二直接依赖的范围是 runtime 的时候,传递性依赖的范围与第一直接依赖的范围一致,但 compile 例外,此时传递性依赖的范围为 runtime

依赖调解

当传递性依赖造成问题的时候,我们就需要清楚地知道该传递性依赖是从哪条依赖路径引入的。

项目 A 有这样的依赖关系:A->B->C->X(1.0)、A->D->X(2.0),X 是 A 的传递性依赖,但是两条依赖路径上有两个版本的X,那么哪个 X 会被 Maven 解析使用呢?两个版本都被解析显然是不对的,因为那会造成依赖重复,因此必须选择一个。Maven 依赖调解(Dependency Mediation)的第一原则是:路径最近者优先。该例中 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 定义了依赖调解的第二原则:第一声明者优先。在依赖路径长度相等的前提下,在 POM 中依赖声明的顺序决定了谁会被解析使用,顺序最靠前的那个依赖优胜。如果 B 的依赖声明在 C 之前,那么 Y(1.0)就会被解析使用。

排除依赖

想想这样一种情景,当前项目有一个第三方依赖,而这个第三方依赖由于某些原因依赖了另外一个类库的SNAPSHOT版本,那么这个SNAPSHOT就会成为当前项目的传递性依赖,而 SNAPSHOT 的不稳定性会直接影响到当前的项目。这时就需要排除掉该 SNAPSHOT,并且在当前项目中声明该类库的某个正式发布的版本。

使用 exclusions 元素声明排除依赖,exclusions 可以包含一个或者多个 exclusion 子元素,因此可以排除一个或者多个传递性依赖。需要注意的是,声明 exclusion 的时候只需要 groupIdartifactId,而不需要 version 元素,这是因为只需要 groupIdartifactId 就能唯一定位依赖图中的某个依赖。

归类依赖

在项目中,会有很多关于 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 Frame-work,这些依赖的版本会一起升级。

使用 properties 元素定义 Maven 属性,该例中定义了一个 springframework.version 子元素,其值为 2.5.6。有了这个属性定义之后,Maven 运行的时候会将 POM 中的所有的 ${springframework.version} 替换成实际值 2.5.6。也就是说,可以使用美元符号和大括弧环绕的方式引用 Maven 属性。然后,将所有 Spring Framework 依赖的版本值用这一属性引用表示。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注