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中的依赖,确保构建一致性。