工欲善其事,必先利其器。熟悉标准化的 Java 项目管理和构建工具原理,切勿知其已然,而不知其所以然,有助于今后处理jar包版本冲突,促进成长。
Maven主要功能:
一个基本的maven管理的 Java项目目录结构如下:
a-maven-project // 项目名
├── pom.xml // 项目描述文件
├── src
│ ├── main
│ │ ├── java // 存放 Java 源码的目录
│ │ └── resources // 存放资源文件的目录
│ └── test
│ ├── java // 存放测试源码的目录
│ └── resources // 存放测试资源的目录
└── target // 存放所有编译、打包生成的文件
注: 目录结构都是约定好的标准结构,千万不要随意修改目录结构;项目描述文件 pom.xml ,通过 groupId,artifactId 和 version 可以定位 唯一jar包 坐标。
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
小结:
Maven 是一个 Java 项目的管理和构建工具:
原理说明:
我们的项目依赖 abc 这个 jar 包,而 abc 又依赖 xyz 这个 jar 包:
┌──────────────┐
│Sample Project│
└──────────────┘
│
▼
┌──────────────┐
│ abc │
└──────────────┘
│
▼
┌──────────────┐
│ xyz │
└──────────────┘
当我们声明了 abc 的依赖时,Maven 自动把 abc 和 xyz 都加入了我们的项目依赖,不需要我们自己去研究 abc 是否需要依赖 xyz。如:
看一个复杂依赖示例:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>1.4.2.RELEASE</version>
</dependency>
当我们声明一个 spring-boot-starter-web 依赖时,Maven 会自动解析并判断最终需要大概二三十个其他依赖:
spring-boot-starter-web
spring-boot-starter
spring-boot
sprint-boot-autoconfigure
spring-boot-starter-logging
logback-classic
logback-core
slf4j-api
jcl-over-slf4j
slf4j-api
jul-to-slf4j
slf4j-api
log4j-over-slf4j
slf4j-api
spring-core
snakeyaml
spring-boot-starter-tomcat
tomcat-embed-core
tomcat-embed-el
tomcat-embed-websocket
tomcat-embed-core
jackson-databind
...
Maven 定义了几种依赖关系,分别是 compile、test、runtime 和 provided:
scope 说明 示例
- test 编译 Test 时需要用到该 jar 包 junit
- runtime 编译时不需要,但运行时需要用到 mysql
- provided 编译时需要用到,但运行时由 JDK 或某个服务器提供 servlet-api
- compile 编译时需要用到该 jar 包(默认) commons-logging
test 依赖表示仅在测试时使用,正常运行时并不需要
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.3.2</version>
<scope>test</scope>
</dependency>
runtime 依赖表示编译时不需要,但运行时需要, 经典依赖是 JDBC 驱动,例如 MySQL 驱动
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.48</version>
<scope>runtime</scope>
</dependency>
最典型的 provided 依赖是 Servlet API,编译的时候需要,但是运行时,Servlet 服务器内置了相关的 jar,所以运行期不需要:
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.0</version>
<scope>provided</scope>
</dependency>
注:
- Maven 并不会每次都从中央仓库下载 jar 包。
- 一个 jar 包一旦被下载过,就会被 Maven 自动缓存在本地目录(用户主目录的.m2 目录)
- 除了第一次编译时因为下载需要时间会比较慢,后续过程因为有本地缓存,并不会重复下载相同的 jar 包
唯一jar 包ID
对于某个依赖,Maven 只需要 3 个变量即可唯一确定某个 jar 包:
- groupId:属于组织的名称,类似 Java 的包名;
- artifactId:该 jar 包自身的名称,类似 Java 的类名;
- version:该 jar 包的版本。
通过上述 3 个变量,即可唯一确定某个 jar 包。Maven 通过对 jar 包进行 PGP 签名确保任何一个 jar 包一经发布就无法修改。修改已发布 jar 包的唯一方法是发布一个新版本。
因此,某个 jar 包一旦被 Maven 下载过,即可永久地安全缓存在本地。
注:只有以 -SNAPSHOT 结尾的版本号会被 Maven 视为开发版本,开发版本每次都会重复下载,这种 SNAPSHOT 版本只能用于内部私有的 Maven repo,公开发布的版本不允许出现 SNAPSHOT。
小结:
lifecycle 和 phase
使用 Maven 时,我们首先要了解什么是 Maven 的生命周期(lifecycle)。
Maven 的生命周期由一系列阶段(phase)构成,以内置的生命周期 default 为例,它包含以下 phase:
validate
initialize
generate-sources
process-sources
generate-resources
process-resources
compile
process-classes
generate-test-sources
process-test-sources
generate-test-resources
process-test-resources
test-compile
process-test-classes
test
prepare-package
package
pre-integration-test
integration-test
post-integration-test
verify
install
deploy
在实际开发过程中,经常使用的命令有:
mvn clean:清理所有生成的 class 和 jar;
mvn clean compile:先清理,再执行到 compile;
mvn clean test:先清理,再执行到 test,因为执行 test 前必须执行 compile,所以这里不必指定 compile;
mvn clean package:先清理,再执行到 package。
大多数 phase 在执行过程中,因为我们通常没有在 pom.xml 中配置相关的设置,所以这些 phase 什么事情都不做。
goal
执行一个 phase 又会触发一个或多个 goal,goal 的命名总是 abc:xyz 这种形式。
执行的 phase 对应执行的 goal
compile compiler:compile
test compiler:testCompile surefile:test
为了方便理解:
lifecycle 相当于 Java 的 package,它包含一个或多个 phase;
phase 相当于 Java 的 class, 它包含一个或多个 goal;
goal 相当于 class 的 method, 它其实才是真正干活的。
大多数情况,我们只要指定 phase,就默认执行这些 phase 默认绑定的 goal,只有少数情况,我们可以直接指定运行一个 goal,例如,
启动 Tomcat 服务器:mvn tomcat:run
小结:
Maven 通过 lifecycle、phase 和 goal 来提供标准的构建流程。
最常用的构建命令是指定 phase,然后让 Maven 执行到指定的 phase:
- mvn clean
- mvn clean compile
- mvn clean test
- mvn clean package
通常情况,我们总是执行 phase 默认绑定的 goal,因此不必指定 goal。
在软件开发中,把一个大项目分拆为多个模块是降低软件复杂度的有效方法:
single-project
├── pom.xml
i. 拆分成3个模块
mutiple-project
├── module-a
│ ├── pom.xml
│ └── src
├── module-b
│ ├── pom.xml
│ └── src
└── module-c
├── pom.xml
└── src
ii. 对于高度相似的的模块om.xml文件,可以提取出共同部分作为 parent
parent 的 是 pom 而不是 jar,因为 parent 本身不含任何 Java 代码。编写 parent 的 pom.xml 只是为了在各个模块中减少重复的配置。现在我们的整个工程结构如下:
multiple-project
├── pom.xml
├── parent
│ └── pom.xml
├── module-a
│ ├── pom.xml
│ └── src
├── module-b
│ ├── pom.xml
│ └── src
└── module-c
├── pom.xml
└── src
iii. 例:如果模块 A 和模块 B 的 pom.xml 高度相似
<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/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.itranswarp.learnjava</groupId>
<artifactId>parent</artifactId>
<version>1.0</version>
<packaging>pom</packaging>
<name>parent</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.28</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.5.2</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
<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/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.itranswarp.learnjava</groupId>
<artifactId>parent</artifactId>
<version>1.0</version>
<relativePath>../parent/pom.xml</relativePath>
</parent>
<artifactId>module-a</artifactId>
<packaging>jar</packaging>
<name>module-a</name>
</project>
<dependencies>
<dependency>
<groupId>com.itranswarp.learnjava</groupId>
<artifactId>module-b</artifactId>
<version>1.0</version>
</dependency>
</dependencies>
<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>com.itranswarp.learnjava</groupId>
<artifactId>build</artifactId>
<version>1.0</version>
<packaging>pom</packaging>
<name>build</name>
<modules>
<module>parent</module>
<module>module-a</module>
<module>module-b</module>
<module>module-c</module>
</modules>
</project>
在根目录执行 mvn clean package 时,Maven 根据根目录的 pom.xml 找到包括 parent 在内的共 4 个 ,一次性全部编译
小结:
Maven 支持模块化管理,可以把一个大项目拆成几个模块:
可以通过继承在 parent 的 pom.xml 统一定义重复配置;
可以通过 modules 编译多个模块。
Maven是如何加载的Jar包的呢?
Maven是怎么处理jar包冲突的呢?
答:我会在下期 Java classloader 加载源码解析说明。