Maven 将 src
目录下的源代码构建成 JAR 包的过程是一个标准化、自动化的流程,涉及多个生命周期阶段和插件协同工作。以下是详细解析:
一、Maven 构建生命周期
Maven 构建过程基于 生命周期(Lifecycle) 设计,主要包含三个内置生命周期:
- Clean Lifecycle:清理项目(如删除
target
目录)。 - Default Lifecycle:核心构建流程(编译、测试、打包等)。
- Site Lifecycle:生成项目文档。
其中,将源代码转为 JAR 的核心是 Default Lifecycle,它包含多个 阶段(Phase),按顺序执行。
二、Default Lifecycle 关键阶段
将源代码构建成 JAR 主要涉及以下阶段:
- compile:编译主源代码(
src/main/java
)。 - test-compile:编译测试代码(
src/test/java
)。 - test:执行单元测试。
- package:将编译后的代码打包为 JAR/WAR 等格式。
- install:将 JAR 安装到本地仓库。
- deploy:将 JAR 发布到远程仓库。
三、源代码到 JAR 的详细流程
1. 源代码目录结构
Maven 项目默认遵循标准目录结构:
虽然 Maven 技术上允许自定义目录结构,但 强烈建议遵循标准结构。这样做的好处远大于灵活性带来的短期便利,尤其是在团队协作和长期项目维护中。标准结构是 Maven 生态系统的基石,也是其 “约定优于配置” 理念的核心体现。
project/ ├── src/ │ ├── main/ │ │ ├── java/ # 主 Java 源代码 │ │ ├── resources/ # 主资源文件(如配置文件) │ │ └── webapp/ # Web 应用特有目录(如 JSP) │ └── test/ │ ├── java/ # 测试 Java 代码 │ └── resources/ # 测试资源文件 └── pom.xml # Maven 项目配置文件
2. 编译阶段(compile)
- 插件:
maven-compiler-plugin
- 执行流程:
- 读取
pom.xml
中的source
和target
配置(默认 Java 1.7)。 - 将
src/main/java
下的 Java 源文件编译为字节码(.class
文件)。 - 编译后的
.class
文件输出到target/classes
目录。 - 将
src/main/resources
下的资源文件复制到target/classes
。
- 读取
3. 测试阶段(test)
- 插件:
maven-surefire-plugin
- 执行流程:
- 编译
src/test/java
下的测试代码到target/test-classes
。 - 执行测试(默认使用 JUnit 或 TestNG)。
- 测试报告生成到
target/surefire-reports
。
- 编译
4. 打包阶段(package)
- 插件:
maven-jar-plugin
(默认)或maven-war-plugin
(Web 项目) - 执行流程:
- 收集
target/classes
目录下的所有.class
文件和资源文件。 - 创建
META-INF/MANIFEST.MF
清单文件,包含:Main-Class
:主类(若配置了启动类)。Class-Path
:依赖项路径(可选)。
- 将所有文件压缩为 JAR 格式,输出到
target
目录,命名规则为:
- 收集
${artifactId}-${version}.jar
例如:spring-petclinic-2.7.1.jar
5. 可选:生成可执行 JAR
对于 Spring Boot 等应用,需通过 spring-boot-maven-plugin
生成可执行 JAR:
<plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin>
该插件会:
- 将依赖 JAR 嵌入到最终 JAR 的
BOOT-INF/lib
目录。 - 修改
MANIFEST.MF
,设置Start-Class
为应用入口类。 - 生成可直接运行的胖 JAR(Fat JAR)。
四、pom.xml 关键配置示例
<project> <!-- 项目基本信息 --> <groupId>org.springframework.samples</groupId> <artifactId>spring-petclinic</artifactId> <version>2.7.1</version> <!-- 依赖管理 --> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>2.7.1</version> </dependency> <!-- 更多依赖... --> </dependencies> <!-- 插件配置 --> <build> <plugins> <!-- 编译插件 --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.1</version> <configuration> <source>11</source> <target>11</target> </configuration> </plugin> <!-- Spring Boot 打包插件(生成可执行 JAR) --> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>2.7.1</version> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> </plugins> </build> </project>
五、最终产物结构
1. 标准 JAR 结构
my-app-1.0.0.jar ├── META-INF/ │ └── MANIFEST.MF └── com/ └── example/ └── App.class # 编译后的类文件
2. Spring Boot 可执行 JAR 结构
spring-petclinic-2.7.1.jar ├── META-INF/ │ └── MANIFEST.MF # 包含 Start-Class ├── BOOT-INF/ │ ├── classes/ # 应用类和资源 │ └── lib/ # 依赖 JAR └── org/ └── springframework/ └── boot/... # Spring Boot 启动类
六、可执行 JAR 与标准 JAR 的核心区别
结构差异
1. 标准 JAR(普通 JAR)
- 结构特点:
- 仅包含编译后的类文件(
.class
)和资源文件 - 依赖的第三方库通常不在 JAR 内部,需单独提供
META-INF/MANIFEST.MF
中可能不含Main-Class
或仅指定简单入口类
- 仅包含编译后的类文件(
my-app.jar ├── com/ │ └── example/ │ └── Main.class # 应用主类 ├── resources/ │ └── config.properties └── META-INF/ └── MANIFEST.MF
2. 可执行 JAR(胖 JAR/Fat JAR)
- 结构特点:
- 包含应用自身的类文件、资源文件
- 内嵌所有依赖的第三方 JAR 包(通常在
BOOT-INF/lib
目录) META-INF/MANIFEST.MF
中包含Main-Class
和Start-Class
配置- 通常包含启动类(如 Spring Boot 的
JarLauncher
)
- 示例结构(Spring Boot 可执行 JAR):
spring-boot-app.jar ├── BOOT-INF/ │ ├── classes/ # 应用自身的类和资源 │ │ ├── com/ │ │ │ └── example/ │ │ │ └── Application.class │ │ └── application.properties │ └── lib/ # 内嵌的依赖 JAR │ ├── spring-boot-2.7.1.jar │ ├── spring-core-5.3.20.jar │ └── ... ├── META-INF/ │ └── MANIFEST.MF # 包含启动配置 └── org/ └── springframework/boot/loader/ # 启动类
运行机制差异
1. 标准 JAR 的运行方式
- 需要在命令行中指定主类和依赖路径
- 依赖的 JAR 必须在
classpath
中可用
java -cp my-app.jar:lib/* com.example.Main
2. 可执行 JAR 的运行方式
- 直接使用
java -jar
命令启动 - 无需额外指定依赖路径,内部包含所有依赖
java -jar spring-boot-app.jar
创建方式差异
1. 标准 JAR 的创建
- 使用 Maven 的
maven-jar-plugin
或 Gradle 的jar
任务 - 默认配置即可,无需特殊插件
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>3.2.2</version> <configuration> <archive> <manifest> <mainClass>com.example.Main</mainClass> </manifest> </archive> </configuration> </plugin>
2. 可执行 JAR 的创建
- 需要特殊插件将依赖打包到 JAR 内部
- Spring Boot 项目:使用
spring-boot-maven-plugin
<plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>2.7.1</version> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin>
优缺点对比
1. 标准 JAR 的优缺点
- 优点:
- 体积小,不包含重复依赖
- 依赖管理更灵活,可动态替换
- 缺点:
- 部署复杂,需单独管理依赖
- 版本冲突风险高
2. 可执行 JAR 的优缺点
- 优点:
- 部署简单,只需一个文件
- 依赖隔离,避免版本冲突
- 跨环境一致性好
- 缺点:
- 体积大,包含所有依赖
- 调试依赖问题较困难
- 依赖更新需重新打包整个 JAR
适用场景
1. 标准 JAR 的适用场景
- 作为库供其他项目引用
- 依赖管理由应用程序统一处理
- 开发阶段频繁调试依赖
2. 可执行 JAR 的适用场景
- 独立运行的应用程序(如微服务)
- 简化部署流程
- 需要快速分发和部署的场景
- Spring Boot、Quarkus 等框架推荐的打包方式
标准 JAR(Java Archive)依赖的第三方库通常不包含在 JAR 内部,而需要单独提供,这种设计主要与 Java 类加载机制、开发规范及应用场景 有关。
可执行 JAR 与标准 JAR 的设计差异对比
维度 | 标准 JAR | 可执行 JAR(如 Fat JAR) |
---|---|---|
依赖包含方式 | 不包含第三方依赖,需通过类路径引用 | 包含所有依赖库(通常打包在 BOOT-INF/lib 等目录) |
执行方式 | java -cp classpath com.main.Class | java -jar app.jar (内置类路径处理逻辑) |
应用场景 | 作为库被其他项目依赖(如 Spring 框架模块) | 独立部署的应用(如微服务、单机程序) |
优势 | 体积小,依赖管理灵活 | 部署便捷,无需手动管理依赖路径 |
七、总结
Maven 将源代码构建成 JAR 的过程是一个 标准化、自动化 的流水线:
- 目录规范:遵循
src/main/java
和src/main/resources
的标准结构。 - 插件驱动:通过
compiler-plugin
、jar-plugin
等插件完成各阶段任务。 - 生命周期管理:严格按照 Default Lifecycle 执行编译、测试、打包。
- 依赖处理:自动解析
pom.xml
中的依赖,确保构建一致性。