一文搞懂 Maven 原理

作者简介大数据研发工程师,主要研究 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 POMSuper POM 是 Maven 的默认 POM。除显式声明以外,所有的POM都继承自Super POM. Super POM 中配置了默认的仓库地址,基本的 Plugin 和源代码路径等配置。Super POM 内容如下:<project> <modelVersion>4.0.0</modelVersion> <!-- NOTE: 依赖 repo 和 插件repo默认仓库地址--> <repositories><!-- ....默认repo --> </repositories> <pluginRepositories> <pluginRepository><!-- ....默认repo --> </pluginRepository> </pluginRepositories> <!-- NOTE: 项目默认配置,如:mian代码目录,--> <build> <directory>${project.basedir}/target</directory> <outputDirectory>${project.build.directory}/classes</outputDirectory> <finalName>${project.artifactId}-${project.version}</finalName> <testOutputDirectory>${project.build.directory}/test-classes</testOutputDirectory> <sourceDirectory>${project.basedir}/src/main/java</sourceDirectory> <scriptSourceDirectory>${project.basedir}/src/main/scripts</scriptSourceDirectory> <testSourceDirectory>${project.basedir}/src/test/java</testSourceDirectory> <resources> <resource> <directory>${project.basedir}/src/main/resources</directory> </resource> </resources> <testResources> <testResource> <directory>${project.basedir}/src/test/resources</directory> </testResource> </testResources> <pluginManagement> <!-- NOTE: These plugins will be removed from future versions of the super POM --> <!-- They are kept for the moment as they are very unlikely to conflict with lifecycle mappings (MNG-4453) --> <plugins><!-- ....这里是一系列的默认插件 --> </plugins> </pluginManagement> </build></project>Project Inheritance VS Project AggregationProject Inheritance: 继承已有pom.xml,便于做公共配置管理。Super POM 是 Project Inheritance 的典型例子。Project Aggregation: 同一项目中存在多个module。下图展示了 Presto 的 POM 结构,Presto 项目中既有项目继承(继承自io.airlift:airbase:80)也有项目聚合(多个模块)。

LifecycleMaven的生命周期就是为了对所有的构建过程进行抽象和统一。Maven从大量项目和构建工具中学习和反思,然后总结了一套高度完善的、易扩展的生命周期。Maven 生命周期包含项目清理、初始化、编译、测试、打包、集成测试、验证、部署和站点生成等几乎所有构建步骤。也就是说,几乎所有项目的构建,都能映射到这样一个生命周期上。Lifecycle 是抽象的概念,生命周期本身不做任何实际工作。Maven 设计中,实际工作都交由插件完成。Lifecycles VS PhasesMaven 有三套独立的 Lifecycle:default、clean 和 site,每个 Lifecycle 包含多个 Phase。下图详细的展示了Lifecycle 和 Phase的关系。

Defaultdefault生命周期定义了真正构建时所需要执行的所有步骤,它是所有生命周期中最核心的部分,其包含的阶段如下: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项目使用。Cleanclean 生命周期的目的主要是清理项目。pre-clean:执行一些清理前需要完成的工作。clean:清理上一次构建生成的文件。post-clean:执行一些清理后需要完成的工作。Sitesite 生命周期用于生成代码站点文档并发布至对应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的绑定关系:

常用 Pluginmaven-shade-pluginmaven-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-pluginmaven-antrun-plugin 使Maven可以运行我们自定义的脚本,灵活控制构建过程。如下所示 pom.xml 就通过 maven-antrun-plugin 在 compile 阶段打印出 Maven 的四类class path,帮助我们定位编译问题。<project> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-antrun-plugin</artifactId> <version>3.0.0</version> <executions> <execution> <id>compile</id> <phase>compile</phase> <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>run</goal> </goals> </execution> </executions> </plugin> </plugins> </build></project>PS:当插件目标被绑定到不同的生命周期阶段的时候,其执行顺序会由生命周期阶段的先后顺序决定。如果多个目标被绑定到同一个阶段,它们的执行顺序会是怎样?答:当多个插件目标绑定到同一个阶段的时候,这些插件声明的先后顺序决定了目标的执行顺序。Dependency MechanismMaven 作为一个项目管理工具,依赖管理是必不可少的。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>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <type>jar</type> <scope>test</scope> <optional>true</optional> </dependency> <dependency> <groupId>mygroup</groupId> <artifactId>myjar</artifactId> <version>1.0</version> <classifier>jdk11</classifier> </dependency> <dependency> <groupId>org.apache.hive</groupId> <artifactId>hive-common</artifactId> <version>1.0</version> <scope>test</scope> <type>test-jar</type> </dependency> </dependencies></project>依赖 Scope依赖Scope作用有两个:1. 限制依赖传递2. 控制依赖是否出现在各个classpath中Maven 中有五种依赖scope,分别是:compile,provided,runtime,test和system。下面是引用自Maven官网的说明:

Maven中大致可以分成四类class path:main 代码编译classpath:编译main代码的classpathmain 代码运行classpath:运行main代码的classpathtest 代码编译classpath:编译测试代码的classpathtest 代码运行classpath:运行测试代码的classpath结合依赖的scope和四类classpath,总结出 classpath 与 依赖的关系,如下图:

依赖传递A -> B (compile) 第一关系 : A 依赖 B compileB -> C (compile) 第二关系 : B 依赖 C compile当在A中配置:<dependency> <groupId>com.B</groupId> <artifactId>B</artifactId> <version>1.0</version> </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文件中的先后顺序来判定使用那条依赖路径,如果次级模块相同则向下级模块推,直至可以判断先后位置为止。<!-- A pom.xml --><dependencies> ... dependency B ... dependency C</dependencies>假设依赖B位置在依赖C之前,则最终会选择X(1.1)依赖。其它情况:覆盖策略。若相同类型但版本不同的依赖存在于同一个pom文件,依赖调解两大原则都不起作用,需要采用覆盖策略来调解依赖冲突,最终会引入最后一个声明的依赖。<!-- 该pom文件最终引入commons-cli:commons-cli:1.3.jar依赖包。 --><dependencies> <dependency> <groupId>commons-cli</groupId> <artifactId>commons-cli</artifactId> <version>1.2</version> </dependency> <dependency> <groupId>commons-cli</groupId> <artifactId>commons-cli</artifactId> <version>1.4</version> </dependency> <dependency> <groupId>commons-cli</groupId> <artifactId>commons-cli</artifactId> <version>1.3</version> </dependency></dependencies>总结1.Maven 是一种声明式的项目管理工具。2.Maven 有3套独立的生命周期,具体的工作交由插件 goal 完成。3.修改依赖Scope,并不能控制Jar的内容。Hey!我是小萝卜算子

(0)

相关推荐