@lemonguge
2017-09-13T14:50:02.000000Z
字数 11128
阅读 1087
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 执行脚本的位置就是这里。
C:\Users\lemonguge>echo %MAVEN_HOME%D:\Program Files\Maven-3.3.3C:\Users\lemonguge>mvn -vApache Maven 3.3.3 (7994120775791599e205a5524ec3e0dfe41d4a06; 2015-04-22T19:57:37+08:00)Maven home: D:\Program Files\Maven-3.3.3\bin\..Java version: 1.8.0_20, vendor: Oracle CorporationJava home: D:\Program Files\Java\jdk1.8.0_20\jreDefault locale: zh_CN, platform encoding: GBKOS name: "windows 8.1", version: "6.3", arch: "amd64", family: "dos"
在 Windows 上升级 Maven 非常简便,只需要下载新的 Maven 安装文件,解压至本地目录,然后更新 MAVEN_HOME 环境变量即可。同理,如果需要使用某一个旧版本的 Maven,也只需要编辑 MAVEN_HOME 环境变量指向旧版本的安装目录。
前面讲到设置 MAVEN_HOME 环境变量指向 Maven 的安装目录,下面看一下该目录的结构和内容:
mvn 运行的脚本,这些脚本用来配置 Java 命令,准备好 classpath 和相关的 Java 系统属性,然后执行 Java 命令。其中 mvn 是基于 UNIX 平台的 shell 脚本,mvn.bat 是基于 Windows 平台的 bat 脚本。在命令行输入任何一条 mvn 命令时,实际上就是在调用这些脚本。打开用户目录,当前目录 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,主要是为了避免无意识地影响到系统中的其他用户
基于安全因素考虑,需要通过安全认证的代理访问因特网。这种情况下,就需要为 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)
<settings><proxies><proxy><id>my-proxy</id><active>true</active><protocol>http</protocol><host>218.14.227.197</host><port>3128</port><!--<username>proxyuser</username><password>proxypass</password><nonProxyHosts>repository.mycom.com|*.google.com</nonProxyHosts>--></proxy></proxies></settings>
proxies 下可以有多个 proxy 元素,如果声明了多个 proxy 元素,则默认情况下第一个被激活的 proxy 会生效。这里声明了一个 id 为 my-proxy 的代理,active 的值为 true 表示激活该代理,protocol 表示使用的代理协议,这里是 http。当然,最重要的是指定正确的主机名(host 元素)和端口(port 元素)。上述 XML 配置中注释掉了 username、password、nonProxyHost 几个元素。当代理服务需要认证时,就需要配置 username 和 password。nonProxyHost 元素用来指定哪些主机名不需要代理,可以使用 “|” 符号来分隔多个主机名。此外,该配置也支持通配符,如 *.google.com 表示所有以 google.com 结尾的域名访问都不要通过代理。
Maven项目的核心是 pom.xml,POM(Project Object Model,项目对象模型)
pox.xml 定义了项目的基本信息,用于描述项目如何构建,声明项目依赖,等等。现在看一个最简单的 pom.xml:
<?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/maven-v4_0_0.xsd"><modelVersion>4.0.0</modelVersion><groupId>cn.homjie.maven</groupId><artifactId>maven-core</artifactId><version>1.0-SNAPSHOT</version><name>Maven Hello World Project</name></project>
project 元素,project 是所有 pom.xml 的根元素,它还声明了一些 POM 相关的命名空间及 xsd 元素,虽然这些属性不是必须的,但使用这些属性能够让第三方工具(如 IDE 中的 XML 编辑器)帮助我们快速编辑 POM modelVersion 元素指定了当前 POM 模型的版本,对于 Maven 2及 Maven 3来说,它只能是 4.0.0groupId 元素定义了项目属于哪个组,这个组往往和项目所在的组织或公司存在关联,如果你的公司 mycom,有一个项目为 myapp,那么 groupId 就应该是 com.mycom.myapp,可以认为 groupId 是项目artifactId 元素定义了当前 Maven 项目在组中唯一的 ID(构建ID),可以认为 artifactId 是项目中的模块,如 myapp-util、myapp-core、myapp-rest等version 元素指定了项目当前的版本,在上面的示例中为 1.0-SNAPSHOT,其中 SNAPSHOT 意为快照,说明该项目还处于开发中,是不稳定的版本。随着项目的发展,version 会不断更新,如升级为 1.0、1.1-SNAPSHOT、1.1、2.0 等name 元素声明了一个对于用户更为友好的项目名称,虽然这不是必须的,但还是推荐为每个 POM 声明 name,以方便信息交流这段代码中最重要的是包含 groupId、artifactId 和 version 的三行。这三个元素定义了一个项目基本的坐标,在 Maven 的世界,任何的 jar、pom 或者 war 都是以基于这些基本的坐标进行区分的。
pom.xml 所在的目录应为项目的根目录,假设该目录为 ${project.basedir},那么 Maven 有以下假设:
${project.basedir}/src/main/java,存放项目的 .java 文件${project.basedir}/src/main/resources,存放项目资源文件,如 spring,hibernate 配置文件${project.basedir}/src/main/webapp,如果是 web 项目,作为 web 应用文件目录${project.basedir}/src/test/jave,存放所有测试 .java 文件,如 JUnit 测试类${project.basedir}/src/test/resources,测试资源文件${project.basedir}/target,项目输出位置${project.basedir}/target/classes,编译输出目录${project.basedir}/target/test-classes,编译输出目录${project.basedir}/target/site,项目 site 输出目录**/*Test.java、**/Test*.java 和 **/*TestCase.java,Maven 只会自动运行符合该命名规则的测试类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 的一段配置:
<project><build><!-- 输出主构件的名称 --><finalName>${project.artifactId}-${project.version}</finalName><!-- 输出目录 --><directory>${project.basedir}/target</directory><!-- 主源码目录 --><sourceDirectory>${project.basedir}/src/main/java</sourceDirectory><!-- 脚本源码目录 --><scriptSourceDirectory>${project.basedir}/src/main/scripts</scriptSourceDirectory><!-- 测试源码目录 --><testSourceDirectory>${project.basedir}/src/test/java</testSourceDirectory><!-- 主源码输出目录 --><outputDirectory>${project.build.directory}/classes</outputDirectory><!-- 测试源码输出目录 --><testOutputDirectory>${project.build.directory}/test-classes</testOutputDirectory><resources><!-- 主资源目录 --><resource><directory>${project.basedir}/src/main/resources</directory></resource></resources><testResources><!-- 测试资源目录 --><testResource><directory>${project.basedir}/src/test/resources</directory></testResource></testResources></build></project>
正如上文所述,Maven 的一大功能是管理项目依赖。为了能自动化地解析任何一个 Java 构件,Maven 就必须将它们唯一标识,这就依赖管理的底层基础——坐标。
世界上任何一个构件都可以使用 Maven 坐标唯一标识
Maven 坐标的元素包括 groupId、artifactId、version、packaging、classifier,只要我们提供正确的坐标元素,Maven 就能找到对应的构件。Maven 内置了一个中央仓库的地址(https://repo.maven.apache.org/maven2),该地址在超级 POM 文件中配置好了。
groupId:定义当前 Maven 项目隶属的实际项目,如 org.sonatype.nexus,org.sonatype 表示 Sonatype 公司建立的一个非盈利性组织,nexus 表示 Nexus 这一实际项目artifactId:定义一个Maven项目的模块,推荐的做法是使用实际项目名称作为artifactId的前缀,如 nexus-indexer,使用了实际项目名 nexus 作为前缀,这样做的好处是方便寻找实际构件version:定义 Maven 项目当前所处的版本,如 nexus-indexer 的版本是2.0.0packaging:该元素定义 Maven 项目的打包方式,当不定义 packaging 的时候,Maven 会使用默认值 jar,如上例中 packaging 为 jar,最终的构件名为 nexus-indexer-2.0.0.jarclassifier:帮助定义构建输出的一些附属构件,如主构件是 nexus-indexer-2.0.0.jar,该项目可能还会通过使用一些插件生成 如nexus-indexer-2.0.0-javadoc.jar、nexus-indexer-2.0.0-sources.jar 这样一些附属构件,其包含了 Java 文档和源代码。这时候 javadoc 和 sources 就是这两个附属构件的 classifier,附属构件也就拥有了自己唯一的坐标。不能直接定义项目的 classifier,因为附属构件不是项目直接默认生成的,而是由附加的插件帮助生成groupId、artifactId、version 是必须定义的,packaging 是可选的(默认为 jar),而 classifier 是不能直接定义的。项目构件的文件名是与坐标相对应的,一般的规则为 artifactId-version[-classifier].packaging,[-classifier]表示可选。值得注意的是,packaging 并非一定与构件扩展名对应,比如 packaging 为 maven-plugin 的构件扩展名为 jar。
首先看下一个完整的依赖声明,包含如下元素:
<project><dependencies><dependency><groupId></groupId><artifactId></artifactId><version></version><type></type><scope></scope><!-- scope为system,必须通过 systemPath元素显式地指定依赖文件的路径 --><systemPath></systemPath><optional></optional><exclusions><exclusion><groupId></groupId><artifactId></artifactId></exclusion></exclusions></dependency><!-- many dependency declare --></dependencies></project>
根元素 project 下的 dependencies 可以包含一个或者多个 dependency 元素,以声明一个或者多个项目依赖。每个依赖可以包含的元素有:
groupId、artifactId 和 version:依赖的基本坐标,对于任何一个依赖来说,基本坐标是最重要的,Maven 根据坐标才能找到需要的依赖。type:依赖的类型,对应于项目坐标定义的 packaging。大部分情况下,该元素不必声明,其默认值为 jar。scope:依赖的范围optional:标记依赖是否可选exclusions:用来排除传递性依赖Maven 解析后的依赖中,不可能出现
groupId和artifactId相同,但是version不同的两个依赖
Maven 有三种 classpath,Maven 在编译项目主代码的时候需要使用一套 classpath,编译和执行测试的时候会使用另外一套 classpath,实际运行 Maven 项目的时候,又会使用一套 classpath。
依赖范围就是用来控制依赖与这三种 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:系统依赖范围。该依赖与三种 classpath 的关系,和 provided 依赖范围完全一致。但是,使用 system 范围的依赖时必须通过 systemPath 元素显式地指定依赖文件的路径。由于此类依赖不是通过 Maven 仓库解析的,而且往往与本机系统绑定,可能造成构建的不可移植,因此应该谨慎使用。systemPath 元素可以引用环境变量,如 <systemPath>${java.home}/lib/rt.jar</systemPath>。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 |
通过上面的表格,可以得出一下结论:
compile 的时候,传递性依赖的范围与第一直接依赖的范围一致test 的时候,依赖不会得以传递provided 的时候,只传递第一直接依赖范围也为 provided 的依赖,且传递性依赖的范围同样为 providedruntime 的时候,传递性依赖的范围与第一直接依赖的范围一致,但 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 的时候只需要 groupId 和 artifactId,而不需要 version 元素,这是因为只需要 groupId 和 artifactId 就能唯一定位依赖图中的某个依赖。
在项目中,会有很多关于 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 依赖的版本值用这一属性引用表示。