本文由康强同学总结分享
作者简介 大数据研发工程师,主要研究 Hive,Presto,Spark 等计算引擎。有任何问题,欢迎大家随时联系: - 知乎: tkanng - GitHub: tkanng - Email: tkanng@gmail.com |
Maven 简介
Maven 是一种声明式项目管理工具,通过在 POM 中配置 "who","what","where"等信息,即可满足编译、测试、打包、发布等项目构建需求。声明式的好处是,用户无需关心构建工具的实现细节,只需在 pom.xml 中配置好项目名,依赖等基础信息即可。坏处是,实现自定义的构建逻辑,相对复杂。(Maven 也提供了插件,如:maven-antrun-plugin,来运行用户自定义脚本。当然,插件最终 apply 到 Maven 的方式最终仍然是声明式的,即需要在 POM 中声明插件运行时机和插件相关配置。)
相对应地,Make 和 Ant 等构建工具是过程式项目管理工具,用户需要编写构建脚本并组织各脚本的依赖关系。过程式项目管理工具好处是,用户自由度很大;坏处是,项目管理经验无法复用,构建脚本编写较为复杂。
POM
基本概念
POM 文件是Maven的核心文件,包含项目构建相关的所有配置信息,如:项目源代码目录,class 文件输出目录等。Maven 执行 goal 时,会首先读取当前目录的 POM 文件,然后执行对应 goal。
Super POM
Super POM 是 Maven 的默认 POM。除显式声明以外,所有的POM都继承自Super POM. Super POM 中配置了默认的仓库地址,基本的 Plugin 和源代码路径等配置。Super POM 内容如下:
<project> <modelVersion>4.0.0modelVersion> <repositories> repositories> <pluginRepositories> <pluginRepository> pluginRepository> pluginRepositories> <build> <directory>${project.basedir}/targetdirectory> <outputDirectory>${project.build.directory}/classesoutputDirectory> <finalName>${project.artifactId}-${project.version}finalName> <testOutputDirectory>${project.build.directory}/test-classestestOutputDirectory> <sourceDirectory>${project.basedir}/src/main/javasourceDirectory> <scriptSourceDirectory>${project.basedir}/src/main/scriptsscriptSourceDirectory> <testSourceDirectory>${project.basedir}/src/test/javatestSourceDirectory> <resources> <resource> <directory>${project.basedir}/src/main/resourcesdirectory> resource> resources> <testResources> <testResource> <directory>${project.basedir}/src/test/resourcesdirectory> testResource> testResources> <pluginManagement> <plugins> plugins> pluginManagement> build>project>
Project Inheritance VS Project Aggregation
Project Inheritance: 继承已有pom.xml,便于做公共配置管理。Super POM 是 Project Inheritance 的典型例子。
Project Aggregation: 同一项目中存在多个module。
下图展示了 Presto 的 POM 结构,Presto 项目中既有项目继承(继承自io.airlift:airbase:80)也有项目聚合(多个模块)。
Lifecycle
Maven的生命周期就是为了对所有的构建过程进行抽象和统一。
Maven从大量项目和构建工具中学习和反思,然后总结了一套高度完善的、易扩展的生命周期。
Maven 生命周期包含项目清理、初始化、编译、测试、打包、集成测试、验证、部署和站点生成等几乎所有构建步骤。也就是说,几乎所有项目的构建,都能映射到这样一个生命周期上。
Lifecycle 是抽象的概念,生命周期本身不做任何实际工作。
Maven 设计中,实际工作都交由插件完成。
Lifecycles VS Phases
Maven 有三套独立的 Lifecycle:default、clean 和 site,每个 Lifecycle 包含多个 Phase。
下图详细的展示了Lifecycle 和 Phase的关系。
Default
default生命周期定义了真正构建时所需要执行的所有步骤,它是所有生命周期中最核心的部分,其包含的阶段如下:
validate:验证项目是否合法以及项目构建信息是否完备。
initialize:初始化。
generate-sources: 生成源代码,如:ANTLR 插件会根据语法文件生成对应的 Java 源代码。
process-sources: 处理项目主资源文件。一般来说,是对src/main/resources目录的内容进行变量替换等工作后,复制到项目输出的主classpath目录中。
generate-resources:生成资源文件。
process-resources:处理资源文件。
compile:编译项目的主源码。一般来说,是编译src/main/java目录下的Java文件至项目输出的主classpath目录中。
process-classes:处理class文件,如:字节码增强。
generate-test-sources:生成测试源代码,如:ANTLR。
process-test-sources:处理项目测试资源文件。一般来说,是对src/test/resources目录的内容进行变量替换等工作后,复制到项目输出的测试classpath目录中。
generate-test-resources:生成测试资源文件。
process-test-resources:拷贝或处理测试资源文件至目标测试目录。
test-compile:编译项目的测试代码。一般来说,是编译src/test/java目录下的Java文件至项目输出的测试classpath目录中。
process-test-classes:处理 test class文件。
test:使用单元测试框架运行测试,测试代码不会被打包或部署。
prepare-package:打包前置工作。
package:接受编译好的代码,打包成可发布的格式,如JAR,WAR等。
pre-integration-test:集成测试的前置工作
integration-test:集成测试。
post-integration-test :集成测试后,需要做的一些事情。
verify:检测所有的集成测试结果是否符合预期,保障代码质量。
install:将包安装到Maven本地仓库,供本地其他Maven项目使用。
deploy:将最终的包复制到远程仓库,供其他开发人员和Maven项目使用。
Clean
clean 生命周期的目的主要是清理项目。
pre-clean:执行一些清理前需要完成的工作。
clean:清理上一次构建生成的文件。
post-clean:执行一些清理后需要完成的工作。
Site
site 生命周期用于生成代码站点文档并发布至对应Web Server。
pre-site:前置工作。
site:生成代码对应的站点文档。
post-site:site后置工作,deploy 前置工作。
site-deploy:发布站点文档至对应的Web Server。
Plugins
一个 Plugin 包含多个goal,来完成项目构建的实际工作,如:Compiler plugin 有两个goal compile 和 testCompile,分别用于编译main代码与编译test代码。下图是default生命周期的插件内置绑定与具体goal的绑定关系:
常用 Plugin
maven-shade-plugin
maven-shade-plugin 是一个很强大的 Maven 插件,可以用来relocate 包名,解决依赖冲突问题;也可以生成一个可执行Jar包(又称 Uber Jar)。下面我们就简单介绍下这两个功能使用方法。
Relocation
我们以shade hive-exec 中的guava包进行说明。(hive-exec 会依赖calcite的代码,calcite代码也会依赖guava包)。
shaded-exec pom.xml:
shaded-exec 中 calcite 代码:
结论:maven-shade-plugin 会对满足对应pattern的所有class文件进行relocate,不会区分该class文件是否是本项目代码编译产生。
Executable Jar
结论:默认情况下,maven-shade-plugin 产生的 shaded jar,包含当前项目class文件以及compile依赖。这也是为什么有时候可以通过修改依赖的scope,即可影响JAR内容的。然而,如果项目中仅使用了默认的jar plugin,那么修改依赖scope,将不会影响输出的Jar的内容了,里面将永远只包含本项目的class。
maven-antrun-plugin
maven-antrun-plugin 使Maven可以运行我们自定义的脚本,灵活控制构建过程。
如下所示 pom.xml 就通过 maven-antrun-plugin 在 compile 阶段打印出 Maven 的四类class path,帮助我们定位编译问题。
<project> <build> <plugins> <plugin> <groupId>org.apache.maven.pluginsgroupId> <artifactId>maven-antrun-pluginartifactId> <version>3.0.0version> <executions> <execution> <id>compileid> <phase>compilephase> <configuration> <target> <property name="compile_classpath" refid="maven.compile.classpath"/> <property name="runtime_classpath" refid="maven.runtime.classpath"/> <property name="test_classpath" refid="maven.test.classpath"/> <property name="plugin_classpath" refid="maven.plugin.classpath"/> <echo message="compile classpath: ${compile_classpath}"/> <echo message="runtime classpath: ${runtime_classpath}"/> <echo message="test classpath: ${test_classpath}"/> <echo message="plugin classpath: ${plugin_classpath}"/> target> configuration> <goals> <goal>rungoal> goals> execution> executions> plugin> plugins> build>project>
PS:当插件目标被绑定到不同的生命周期阶段的时候,其执行顺序会由生命周期阶段的先后顺序决定。如果多个目标被绑定到同一个阶段,它们的执行顺序会是怎样?
答:当多个插件目标绑定到同一个阶段的时候,这些插件声明的先后顺序决定了目标的执行顺序。
Dependency Mechanism
Maven 作为一个项目管理工具,依赖管理是必不可少的。
Maven 依赖坐标
Maven 坐标为各种构件引入了秩序,任何一个构件都必须明确定义自己的坐标,而一组Maven坐标是通过一些元素定义的。Maven 坐标一般可以认为是一个五元组,即:(groupId,artifactId,version,type,classifier)。下面进行详细说明:
groupId:Maven 项目隶属的实际项目名称。
artifactId:实际项目中的一个模块名称。
version:版本。
type:
–jar: jar包,包含依赖项目主代码 class文件。
–test-jar:jar包,classifier 为 tests,包含依赖项目测试代码 class 文件。用于复用测试代码。
classifier: 用于区分从同一个POM中,构建出的不同 artifacts。比如:同一个项目可能同时提供 jdk11 和 jdk 8 对应的依赖,同一个项目可能同时提供shaded 和 un-shaded 的依赖版本等。
上述五个元素中,groupId、artifactId、version是必须定义的,type是可选的(默认为jar),而classifier是取决于对应依赖是否提供。
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <dependencies> <dependency> <groupId>junitgroupId> <artifactId>junitartifactId> <version>4.12version> <type>jartype> <scope>testscope> <optional>trueoptional> dependency> <dependency> <groupId>mygroupgroupId> <artifactId>myjarartifactId> <version>1.0version> <classifier>jdk11classifier> dependency> <dependency> <groupId>org.apache.hivegroupId> <artifactId>hive-commonartifactId> <version>1.0version> <scope>testscope> <type>test-jartype> dependency> dependencies>project>
依赖 Scope
依赖Scope作用有两个:
1. 限制依赖传递
2. 控制依赖是否出现在各个classpath中
Maven 中有五种依赖scope,分别是:compile,provided,runtime,test和system。
下面是引用自Maven官网的说明:
Maven中大致可以分成四类class path:
main 代码编译classpath:编译main代码的classpath
main 代码运行classpath:运行main代码的classpath
test 代码编译classpath:编译测试代码的classpath
test 代码运行classpath:运行测试代码的classpath
结合依赖的scope和四类classpath,总结出 classpath 与 依赖的关系,如下图:
依赖传递
A -> B (compile) 第一关系 : A 依赖 B compile
B -> C (compile) 第二关系 : B 依赖 C compile
当在A中配置:
<dependency> <groupId>com.BgroupId> <artifactId>BartifactId> <version>1.0version> dependency>
则会自动导入 C 包, 详细的关系传递如下表 :
依赖调节
依赖调解遵循以下两大原则:路径最短优先、声明顺序优先。
第一原则:路径最近者优先。把当前模块当作顶层模块,直接依赖的包则作为次层模块,间接依赖的包则作为次层模块的次层模块,依次递推...,最后构成一棵引用依赖树。假设当前模块是A,两种依赖路径如下所示:
A --> B --> X(1.1) // dist(A->X) = 2A --> C --> D --> X(1.0) // dist(A->X) = 3
此时,Maven可以按照第一原则自动调解依赖,结果是使用X(1.1)作为依赖。
第二原则:第一声明者优先。若冲突依赖的路径长度相同,那么第一原则就无法起作用了。假设当前模块是A,两种依赖路径如下所示:
A --> B --> X(1.1) // dist(A->X) = 2 A --> C --> X(1.0) // dist(A->X) = 2
当路径长度相同,则需要根据A直接依赖包在pom文件中的先后顺序来判定使用那条依赖路径,如果次级模块相同则向下级模块推,直至可以判断先后位置为止。
<dependencies> ... dependency B ... dependency Cdependencies>
假设依赖B位置在依赖C之前,则最终会选择X(1.1)依赖。
其它情况:覆盖策略。若相同类型但版本不同的依赖存在于同一个pom文件,依赖调解两大原则都不起作用,需要采用覆盖策略来调解依赖冲突,最终会引入最后一个声明的依赖。
<dependencies> <dependency> <groupId>commons-cligroupId> <artifactId>commons-cliartifactId> <version>1.2version> dependency> <dependency> <groupId>commons-cligroupId> <artifactId>commons-cliartifactId> <version>1.4version> dependency> <dependency> <groupId>commons-cligroupId> <artifactId>commons-cliartifactId> <version>1.3version> dependency>dependencies>
总结
1.Maven 是一种声明式的项目管理工具。
2.Maven 有3套独立的生命周期,具体的工作交由插件 goal 完成。
3.修改依赖Scope,并不能控制Jar的内容。
Hey!
我是小萝卜算子
欢迎扫码关注公众号
在成为最厉害最厉害最厉害的道路上
很高兴认识你