Java Maven 小结
- Maven 坐标和依赖
- Maven 生命周期和插件
一. Maven 坐标和依赖
1. Maven 坐标详解
一组 Maven 坐标是通过一些元素定义的,如:groupId,artifactId,version,packaging,classifier。
- groupId: 定义当前项目隶属的实际项目
- artifactId: 定义该实际项目中的一个 Maven 项目(模块)
- version: 该元素定义 Maven 当前项目所处的版本
- packaging: 该元素定义 Maven 项目的打包方式。打包方式通常与所生成文件构建的文件扩展名对应,不声明则默认是 JAR
- classifier: 该元素用来帮助定义,构建输出一些附属构件。附属构件与主构件对应,如上例中的主构件是 nexus-indexer-2.0.0.jar,该项目可能还会通过使用一些插件生成如 nexus-indexer-2.0.0-javadoc.jar 这样一些附属构件。这时 javadoc 就是附属构件的classifier。
Maven 如何根据坐标,生成 仓库路径
任何一个构件都有其唯一坐标,根据这个坐标可以定义在仓库的唯一存储路径。
如下构件:
<groupId>com.test.saas</groupId>
<artifactId>message-biz-misc</artifactId>
<version>1.0.0-release</version>
<classifier>jdk15</classifier>
<packaging>jar</packaging>
- 基于构件的 groupId 准备路径,将构件 groupId 句点分隔符,转换成路径分隔符。如com.test.saas 转换成:
com/test/saas/
- 基于构件 artifactId 准备路径,在前面的基础上,加上artifactId以及一个路径分隔;如:message-biz-misc 就会变成
com/test/saas/message-biz-misc/
- 基于构件 version 准备路径,在前面的基础上加上 version路径,并加上路径分隔符;如:
com/test/saas/message-biz-misc/1.0.0-release/
- 在上面基础上依次加上 artifactId,构件分隔符连接号,以及 version,于是构件路径就变成了:
com/test/saas/message-biz-misc/1.0.0-release/message-biz-misc-1.0.0-release
- 如果构件有 classifier ,就加上构件分隔符连字号,那么就变成:
com/test/saas/message-biz-misc/1.0.0-release/message-biz-misc-1.0.0-release-jdk15
- 检查构件的 extension,若 extension 存在,则加上句点分隔符和extension,如下就变成了:
com/test/saas/message-biz-misc/1.0.0-release/message-biz-misc-1.0.0-release-jdk15.jar
2. Maven 坐标依赖
依赖相关标签
<dependencies>
<dependency>
<groupId>...</groupId>
<artifactId>...</artifactId>
<version>...</version>
<type>...</type>
<scope>...</scope>
<optional>...</optional>
<exclusions>
<exclusion>...</exclusion>
</exclusions>
</dependency>
</dependencies>
- groupId, artifactId 和 version: 依赖的基本坐标,对于任何一个依赖来说,基本坐标是最重要的,Maven 根据坐标才能找到需要的依赖。
- type: 依赖的类型,对应项目坐标定义的 packaging。大部分情况下,该元素不必声明,其默认值为 jar。
- scope: 依赖的范围,如:compile, test, provided, runtime, system, import。
- optional: 标记依赖是否可选。
- exclusions: 用来排出传递性依赖。
传递性依赖和依赖范围
传递性依赖
如果依赖没有声明依赖范围,那么其依赖范围就是默认的 compile。假设 A 依赖于 B,B 依赖于 C,那么 A 对于 B 就是第一直接依赖,B 对于 C 是第二直接依赖,A 对于 C 是传递依赖,第一直接依赖的范围和第二直接依赖的范围决定了传递的依赖范围,如下图左边一列表示”第一直接依赖”,最上面一行表示”第二直接依赖”,由下表可以发现这样的规律:
- 如果第二直接依赖是 compile 的时候,传递性依赖范围和第一直接依赖范围一致。
- 如果第二直接依赖是 test 的时候,依赖不会得以传递。
- 如果第二直接依赖是 provided 的时候,只传递第一直接依赖范围也为 provided 的依赖,且传递依赖的范围同样为 provided。
- 当第二直接依赖范围是 runtime 的时候,传递依赖的范围和 第一直接传递依赖范围一致,但是 compile 除外,compile 的传递依赖范围是 runtime。
依赖范围
这里有三个阶段:编译,测试,运行,在这里只标注有效,没说则是无效:
- compile: 对于 classpath,编译,测试,运行,都有效。
- test: 对于 classpath,只对 测试 有效。
- provided: 对于 classpath 只对 编译,测试,有效。
- runtime: 对于 classpath 只对 测试,运行 有效。
- system: 对于 classpath 只对 编译,测试,有效。
依赖调解
项目 A 有这样的依赖关系: A -> B -> C -> X(1.0),A -> D -> X(1.0),X 是 A 的传递性依赖,但是两条路径上都有。Maven 依赖调解的第一原则是:路径最近者优先,因此 X(2.0) 会被引用解析使用。 如果,依赖路径一样的时候,Maven 第一原则不适用,在 Maven 2.0.9 之后,为了解决这种依赖的不确定性,Maven 引入调解第二原则,在第一调解优先的情况下,路径长度一样长,则在 POM 中依赖声明的顺序决定了谁会被解析引用。
可选依赖不具备传递性
排出依赖
传递性依赖会给项目隐式的创建引入很多依赖。如果 项目 A 依赖于 项目 B,但是由于某种原因不想引入传递性依赖 C,这时可以使用标签:
<exclusions></exclusions>
排除一个或者多个依赖,排除的时候只要声明groupId和artifactId就可以了,这样可以唯一确定这个构件。
归并依赖
为了方便依赖的管理,这里引入属性:
<properties></<properties>
如下:
<properties>
<common.model.version>1.0.4-RELEASE</common.model.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.xxx</groupId>
<artifactId>common-model</artifactId>
<version>${common.model.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
使用这个属性定义之后,Maven 运行的时候会将 POM 中的所有 ${common.model.version} 替换成实际的 1.0.4-RELEASE。
优化依赖
使用命令:
mvn dependency:list
可以查看当前项目的已解析依赖。同时每个依赖的范围也得以明示。
当这些依赖经 Maven 解析后,就会构成一个依赖树,通过这棵依赖树就能很清楚地看到某个依赖是通过哪条路径引入的:
mvn dependency:tree
在此基础上,命令:
mvn dependency:analyze
可以帮助分析当前项目的依赖。
该结果重要的是两个部分,如下:
[WARNING] Used undeclared dependencies found:
[WARNING] org.springframework:spring-tx:jar:3.2.13.RELEASE:compile
[WARNING] com.alibaba:dubbo:jar:3.1.6.4-RELEASE:compile
[WARNING] org.mybatis:mybatis:jar:3.3.0:compile
[WARNING] com.xxx.platform:bootstrap:jar:3.1.6.4-RELEASE:compile
[WARNING] com.xxx:common-model:jar:1.0.4-RELEASE:compile
[WARNING] org.springframework:spring-core:jar:4.3.8.RELEASE:compile
[WARNING] com.alibaba:fastjson:jar:1.2.29:compile
[WARNING] org.springframework:spring-context:jar:3.2.13.RELEASE:compile
[WARNING] org.jboss.spec.javax.annotation:jboss-annotations-api_1.1_spec:jar:1.0.1.Final:compile
[WARNING] com.google.code.gson:gson:jar:2.2.4:compile
[WARNING] org.aspectj:aspectjrt:jar:1.5.4:compile
[WARNING] org.jboss.resteasy:jaxrs-api:jar:3.0.7.Final:compile
[WARNING] Unused declared dependencies found:
[WARNING] ch.qos.logback:logback-core:jar:1.1.3:compile
[WARNING] ch.qos.logback:logback-classic:jar:1.1.3:compile
[WARNING] com.google.guava:guava:jar:16.0.1:compile
[WARNING] com.alibaba:druid:jar:1.0.13:compile
[WARNING] mysql:mysql-connector-java:jar:5.1.40:compile
[WARNING] org.apache.commons:commons-lang3:jar:3.4:compile
[WARNING] com.h2database:h2:jar:1.4.192:test
[WARNING] org.hibernate:hibernate-validator:jar:4.3.2.Final:compile
- Used undeclared dependencies found: 首先是 Used undeclared dependencies found 意指项目中使用到的,但是没有显式声明的依赖,这种依赖意味着潜在的风险,当前项目直接在使用它们,例如有很多相关的 Java import 声明,而这种依赖是通过直接依赖传递进来的,当升级直接依赖的时候,相关传递性依赖的版本可能发生变化,这种变化不易察觉,但是有可能导致当前项目出错。例如由于接口的改变,当前项目中的相关代码无法编译,这种隐藏的,潜在的威胁一旦出现,就往往需要耗费大量的时间来查明真相。因此,显式声明任何项目中直接用到的依赖。
- Unused declared dependencies found: 意指项目中未使用的,但显式声明的依赖,需要注意的是,对于这样一类依赖,我们不应该简单的直接删除其声明,而是应该仔细分析。由于 dependency:analyze 只会分析编译主代码和测试代码用到的依赖,一些执行测试和运行时需要的依赖它就发现不了。遇到这种情况要认真分析,小心删除。
二. Maven 生命周期和插件
Maven 生命周期
Maven 的生命周期包含以下几个步骤:项目清理,初始化,编译,测试,打包,集成测试,验证,部署和站点生成等几乎所有的构建步骤。
Maven 定义的生命周期和插件机制一方面保证了所有的 Maven 项目有一致的构建标准,另一方面又通过默认插件简化和稳定了实际项目的构建。此外,该机制还提供了足够的扩展空间,用户可以通过配置现有插件或者自行编写插件来自定义构建行为。
clean 生命周期
- pre-clean: 执行一些清理前需要完成的工作
- clean: 清理上一次,构建生成的文件
- post-clean: 执行一些清理后,需要完成的工作
default 生命周期
default 定义了真正构建时,所需要执行的所有的步骤,它是生命周期中,最核心的部分,包括如下阶段:
* validate
* initialize
* generate-sources
* porcess-sources 处理项目的主资源文件。一般来说,是编译 src/main/resources 目录的内容进行变量替换等工作后,复制到项目输出的主 classpath 中。
* generate-resources
* process-resources
* compile 编译项目的主源码。一般来说,是编译 src/main/java 目录下的 Java文件至项目输出的主 classpath目录中。
* process-classes
* generate-test-sources
* process-test-sources 处理项目测试资源文件。一般来说,是对 src/test/resources 目录的内容进行变量替换等工作后,复制到项目输出的测试 classpath 目录中。
* generate-test-resources
* process-test-resources
* test-compile 编译项目的测试代码。一般来说,是编译 src/test/java 目录下的 Java 文件至项目输出的测试 classpath 目录中。
* process-test-classes
* test 使用单元测试框架运行测试,测试代码不会被打包或部署。
* prepare-package
* package 接受编译好的代码,打包成可发布的格式,如 JAR。
* pre-integration-test
* integration-test
* post-integration-test
* verify
* install 将包安装到 Maven 本地仓库,供其他 Maven 项目使用。
* deploy 将最终的包复制到远程仓库,供其他开发人员和 Maven 项目使用。
site 的生命周期
site 生命周期的目的是建立和发布项目的站点,Maven 能够基于 POM 所包含的信息,自动生成一个站点,该生命周期包含如下阶段:
pre-site: 执行一些在站点之前需要完成的工作。
site: 生成项目文档。
post-site: 执行一些在生成站点之后,需要完成的工作。
site-deploy: 将生成的项目站点发布到服务器上。
相关的命令执行的生命周期
- mvn clean: 实际执行的是 clean 生命周期的 pre-clean 和 clean 阶段。
- mvn test: 该命令调用 default 生命周期的 test 阶段。实际执行的阶段为 default 生命周期的 validate,initialize 等,直到 test 的所有阶段。
自定义插件绑定
除了内置插件绑定之外,用户还可以自己选择将某个插件的目标绑定到生命周期的某个阶段上,例如:
<build>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.7.9</version>
<executions>
<execution>
<id>pre-test</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>post-test</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
</build>
上述配置除了基本的坐标配置外,还有插件执行配置:
<executions></executions>
executions 标签下的每个execution子元素,都可以用来执行一个任务,该例子中配置了一个id为post-test的任务,通过标签phase绑定到test阶段,再通过goals配置指定要执行插件目标。至此,自定义插件绑定完成。
有时候,即使不通过 phase 将其绑定生命周期阶段,插件默认有绑定生命周期阶段,可以通过如下命令查看:
mvn help:describe -Dplugin=org.jacoco:jacoco-maven-plugin:0.7.9 -Ddetail
输出如下信息:
jacoco:prepare-agent
Description: Prepares a property pointing to the JaCoCo runtime agent that
can be passed as a VM argument to the application under test. Depending on
the project packaging type by default a property with the following name is
set:
Resulting coverage information is collected during execution and by default
written to a file when the process terminates.
Implementation: org.jacoco.maven.AgentMojo
Language: java
Bound to phase: initialize
可以看到默认的绑定阶段是: initialize。 如果,多个插件目标绑定到同一个执行阶段上,这些插件声明的先后顺序决定了目标执行的顺序。
这里穿插说一下标签
mvn dependency:analyze
他的 goal 就是:dependency:tree,我对 goal 的理解是,Maven 构建项目的整个生命周期和插件绑定,插件负责执行该阶段的所需要的操作,而插件也有很多执行单元,这个就是 goal。