其他

Maven 将源代码编译打包成 JAR 的完整流程

Maven 将 src 目录下的源代码构建成 JAR 包的过程是一个标准化、自动化的流程,涉及多个生命周期阶段和插件协同工作。以下是详细解析:

一、Maven 构建生命周期


Maven 构建过程基于 生命周期(Lifecycle) 设计,主要包含三个内置生命周期:

  1. Clean Lifecycle:清理项目(如删除 target 目录)。
  2. Default Lifecycle:核心构建流程(编译、测试、打包等)。
  3. Site Lifecycle:生成项目文档。

其中,将源代码转为 JAR 的核心是 Default Lifecycle,它包含多个 阶段(Phase),按顺序执行。

二、Default Lifecycle 关键阶段


将源代码构建成 JAR 主要涉及以下阶段:

  1. compile:编译主源代码(src/main/java)。
  2. test-compile:编译测试代码(src/test/java)。
  3. test:执行单元测试。
  4. package:将编译后的代码打包为 JAR/WAR 等格式。
  5. install:将 JAR 安装到本地仓库。
  6. 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
  • 执行流程
    1. 读取 pom.xml 中的 source 和 target 配置(默认 Java 1.7)。
    2. 将 src/main/java 下的 Java 源文件编译为字节码(.class 文件)。
    3. 编译后的 .class 文件输出到 target/classes 目录。
    4. 将 src/main/resources 下的资源文件复制到 target/classes

3. 测试阶段(test)

  • 插件maven-surefire-plugin
  • 执行流程
    1. 编译 src/test/java 下的测试代码到 target/test-classes
    2. 执行测试(默认使用 JUnit 或 TestNG)。
    3. 测试报告生成到 target/surefire-reports

4. 打包阶段(package)

  • 插件maven-jar-plugin(默认)或 maven-war-plugin(Web 项目)
  • 执行流程
    1. 收集 target/classes 目录下的所有 .class 文件和资源文件。
    2. 创建 META-INF/MANIFEST.MF 清单文件,包含:
      • Main-Class:主类(若配置了启动类)。
      • Class-Path:依赖项路径(可选)。
    3. 将所有文件压缩为 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.Classjava -jar app.jar(内置类路径处理逻辑)
应用场景作为库被其他项目依赖(如 Spring 框架模块)独立部署的应用(如微服务、单机程序)
优势体积小,依赖管理灵活部署便捷,无需手动管理依赖路径

七、总结


Maven 将源代码构建成 JAR 的过程是一个 标准化、自动化 的流水线:

  1. 目录规范:遵循 src/main/java 和 src/main/resources 的标准结构。
  2. 插件驱动:通过 compiler-pluginjar-plugin 等插件完成各阶段任务。
  3. 生命周期管理:严格按照 Default Lifecycle 执行编译、测试、打包。
  4. 依赖处理:自动解析 pom.xml 中的依赖,确保构建一致性。