思维导图
Maven 与构建
什么是 Maven
翻译:知识的积累、专家、内行。跨平台的项目管理工具。Apache 组织的开源项目。主要服务于基于 Java 平台的项目构建、依赖管理和项目信息管理。
类似于 linux 平台的 yum、apt,前端领域的 npm。Maven 前身为 Ant 目前 tomcat 的源码就是用 Ant 来构建和管理,更先进的工具有 Gradle, Spring 工程在用。
什么是构建
何为构建:编译、运行单元测试、生成文档、打包、部署的过程,这就是构建。
构建的步骤:
清理 clean:将以前编译得到的旧文件 class 字节码文件删除。
编译 compile:将 java 源程序编译成 class 字节码文件。
测试 test:自动测试,自动调用 junit 程序。
报告 report:测试程序执行的结果。
打包 package:动态 Web 工程打 War 包,java 工程打 jar 包。
安装 install:将打包得到的文件复制到 “仓库” 中的指定位置(Maven特定的概念)。
部署 deploy:将动态 Web 工程生成的 war 包复制到 Servlet 容器下,使其可以运行。
项目骨架
pom:Project Object Model
根目录:工程名 |---src:源码 |---|---main:主程序 |---|---|---java:主程序代码路径 |---|---|---resource:主程序配置文件路径 |---|---test:测试 |---|---|---java:测试代码路径 |---|---|---resource:测试配置文件路径 |---pom.xml:maven 配置文件
简单演示
## 1. 使用 archetype 命令生成 maven 简单骨架 mvn archetype:generate -DarchetypeCatalog=internal ## 2. 编译当前生成的项目 mvn compile ## 3. 使用其他命令 mvn test-compile mvn package mvn clean mvn install mvn depoly 暂时不演示
坐标与依赖
什么是坐标
类比为数学中平面几何,坐标(x、y ),任何一个坐标都能唯一标识该平面中的一个点。
该点对应到 maven 就是 .jar、.war 等文件的文件。
Maven 使用 groupId、artifactId、version、packaging、classifier 等元素来组成自己的坐标,并定义一组这样的规则,只要能提供正确坐标元素 Maven 就能找到对应的构件。
坐标元素
groupId:定义当前 Maven 项目隶属的实际项目。
artifactId:定义实际项目中的一个 Maven 项目(模块)。
packaging:定义 Maven 项目打包方式。jar、war、pom。默认为 jar。
version:定义 Maven 项目当前所处的版本。
classifier:区分从同一 artifact 构建的具有不同内容的构件。
classifier 使用场景
区分基于不同 JDK 版本的包
<dependency> <groupId>net.sf.json-lib</groupId> <artifactId>json-lib</artifactId> <version>2.2.2</version> <classifier>jdk13</classifier> <!--<classifier>jdk15</classifier>--> </dependency>
区分项目的不同组成部分
<dependency> <groupId>net.sf.json-lib</groupId> <artifactId>json-lib</artifactId> <version>2.2.2</version> <classifier>jdk15-javadoc</classifier> <!--<classifier>jdk15-sources</classifier> --> </dependency>
构件名与坐标是对应的,一般规则是:artifactId-version[-classifier].packaging。
依赖声明
<dependencies> <dependency> <groupId></groupId> <artifactId></artifactId> <version></version> <type></type> <optional></optional> <exclusions> <exclusion> <artifactId></artifactId> <groupId></groupId> </exclusion> ... </exclusions> </dependency> ... </dependencies>
groupId、artifactId、version:依赖的基本坐标。
type:依赖的类型,对应项目对应的 packaging,一般不必声明。
scope:依赖的范围,后面详解。
optional:标记依赖是否可选。
exclusions:用来排除传递性依赖。
依赖范围
compile:编译依赖范围
如果没有指定,默认使用该依赖范围。对于编译、测试、运行三种 classpath 都有效。如:spring-core。
test:测试依赖范围
只对于测试 classpath 有效,只需要在编译测试及运行测试才需要,在打包的时候不会打进去。如:JUnit。
provided:已提供依赖范围
对于编译和测试 classpath 有效,但运行时无效。如:servlet-api 编译和测试项目的时候都需要,但在实际运行中,容器已经提供,不需要 maven 重复的引用。
runtime:运行时依赖范围
对于测试和运行的 classpath 有效,但在编译主代码时无效。如:JDBC 驱动的实现包。只有在执行测试或者运行项目时,才需要具体的 JDBC 驱动。
system:系统依赖范围
与 provided 依赖范围完全一致,但是使用该范围时必须通过 systemPath 元素显式地指定依赖文件的路径。由于此类依赖不是通过 maven 仓库解析的,而且往往与本机系统绑定,可能造成构建不可移植,因此应该谨慎使用。systemPath 元素可以引用环境变量,如:
<dependencies> <dependency> <groupId>javax.sql</groupId> <artifactId>jdbc-stdxt</artifactId> <version>2.0</version> <scope>system</scope> <systemPath>${java.home}/lib/rt.jar</systemPath> </dependency> </dependencies>
import:导入依赖范围
只在 dependencyManagement 标签中生效,导入已经定义好的 pom 文件中 dependencyManagement 节点内容
<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-framework-bom</artifactId> <version>4.3.16.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
依赖机制与特性
依赖传递
- A->B(compile):第一直接依赖
- B->C(compile):第二直接依赖
- A->C(compile):传递性依赖
当在A中配置
<dependency> <groupId>com.B</groupId> <artifactId>B</artifactId> <version>1.0</version> </dependency>
则会自动导入 C 包。
传递性依赖的范围如下图所示:
依赖调解
当传递性依赖出现问题时,能够清楚地知道该传递性依赖是从哪条依赖路径中引入的。
一、路径最近者优先原则
A->B->C->X(1.0)
A->D->X(2.0)
由于只能导入一个版本的包,按照最短路径选择导入 X(2.0)
二、第一声明者优先原则
A->B->Y(1.0)
A->C->Y(2.0)
此时由于依赖路径长度一致,按照第一声明者优先原则。在路径长度一致的前提下,如果 B 依赖在 POM 文件中声明顺序在 C 依赖之前,那么 Y(1.0) 则会被引入。如下依赖可用于测试:
<dependencies> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.4.1</version> <exclusions> <exclusion> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi</artifactId> <version>3.9</version> <exclusions> <exclusion> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> <version>1.10</version> </dependency> </dependencies>
这里有一点需要特别注意,看如下依赖:
<dependencies> <dependency> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> <version>1.11</version> </dependency> <dependency> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> <version>1.10</version> </dependency> </dependencies>
按照两原则,期望得到的结果应该是 1.11 版本的构建将被依赖。但实际结果却依赖了 1.10 版本。what!这不是违反了 maven 依赖调解的最先定义原则?
其实这个是 dependency 插件的功能,默认采用的是复写的策略,当构建声明处于同一 pom 中,且 groupid 和 artifactId 一致时,以最新声明为准,后面的覆盖前面的。
注意这里没涉及到依赖调解的功能。我的理解是依赖调解只发生于构建来自不同 pom 时,而此时构建声明处于同一 pom,故不会触发依赖调解。
可选依赖
A->B、B->X(可选)、B->Y(可选)。
项目 A 依赖于项目 B,项目 B 依赖于项目 X 和 Y。
理论上项目 A 中,会把 B、X、Y 项目都依赖进来。
但是 X、Y 两个依赖对于 B 来讲可能是互斥的,如 B 是数据库隔离包,支持多种数据库 MySQL、Oracle,在构建 B 项目时,需要这两种数据库的支持,但在使用这个工具包时,只会依赖一个数据库。
此时就需要在 B 项目 pom 文件中将 X、Y 声明为可选依赖,如下:
<dependency> <groupId>com.X</groupId> <artifactId>X</artifactId> <version>1.0</version> <optionnal>true</optionnal> </dependency> <dependency> <groupId>com.Y</groupId> <artifactId>Y</artifactId> <version>1.0</version> <optionnal>true</optionnal> </dependency>
使用 optionnal 元素标识以后,只会对当前项目 B 产生影响,当其他的项目依赖 B 项目时,这两个依赖都不会被传递。
项目 A 依赖于项目 B,如果实际应用数据库是 X, 则在 A 的 pom 中就需要显式地声明 X 依赖。
仓库
仓库分类:包括本地仓库和远程仓库。其中远程仓库包括:私服和中央仓库。搜索构建的顺序:
本地仓库
maven settings profile 中的 repository;
pom.xml 中 profile 中定义的repository;
pom.xml 中 repositorys (按定义顺序找);
maven settings mirror;
central 中央仓库;
生命周期
Maven 的生命周期是为了对所有构建过程进行的抽象和统一,其中包含项目的清理、初始化、编译、测试、打包、集成测试、验证、部署和站点生成等几乎所有的构建步骤。
Maven 的生命周期是抽象的,本身是不做任何实际的工作。实际的任务都交给插件来完成。
意味着 Maven 只在父类中定义了算法的整体结构,子类通过重写父类的方法,来控制实际行为(设计模式中的模板方法 Template Method)。伪代码如下:
public abstract class AbstractBuilder { public void build() { init(); compile(); test(); package(); integrationTest(); deploy(); } protected abstract void init(); protected abstract void compile(); protected abstract void test(); protected abstract void package(); protected abstract void integrationTest(); protected abstract void deploy(); }
三套生命周期
Maven 的生命周期并不是一个整体,Maven 拥有三套相互独立的生命周期,它们分别为 clean、default 和 site。
clean 生命周期的目的是清理项目;
default 生命周期的目的是构建项目;
site 生命周期的目的是建立项目站点;
单个生命周期执行顺序
每个生命周期包含一些阶段(phase),这些阶段是有顺序的,并且后面的阶段依赖于前面的阶段。
以 clean 生命周期为例,它包含的阶段有 pre-clean、clean和post-clean。当调用 pre-clean 时,只有 pre-clean 阶段得以执行;
当调用 clean 的时候,pre-clean和clean阶段会得以顺序执行,以此类推。
各个生命周期之间的关系
三套生命周期本身是相互独立的,用户可以仅调用 clean 生命周期的某个阶段,或者仅仅调用 default 生命周期的某个阶段,而不会对其他生命周期产生任何影响。
例如,当用户调用 clean 生命周期的 clean 阶段的时候,不会触发 default 生命周期的任何阶段,反之亦然。
生命周期各个阶段详解
clean
default
包含 23 个阶段,此处只介绍重点步骤,如下表:
site