Maven 源码包的下载与工程化落地

示例项目

本文以一个 Spring Boot 2.7.18 单模块 Maven 工程为示例,Java 版本为 8,pom.xml 如下:

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.18</version>
        <relativePath/>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>spring-boot-demo</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <name>spring-boot-demo</name>

    <properties>
        <java.version>8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

后续所有下载、解压、目录结构的说明均围绕这个 pom.xml 声明的依赖展开。

源码从哪里来

Maven 仓库中,一个正式发布的构件通常同时附带一份源码包,命名遵循固定规则:

text 复制代码
<artifactId>-<version>-sources.jar

jar 内部按原始包路径存放 .java 源文件。例如 spring-boot-2.7.18-sources.jar 解开后会得到 org/springframework/boot/SpringApplication.java。源码包与编译包分离发布,仓库里有 jar 不代表一定有 sources jar,但主流开源框架基本都会提供。

IDE 能看,但被 jar 框住

VSCode、IntelliJ 等主流 IDE 都支持关联源码包,在跳转或断点时自动展示 .java 内容,体验接近阅读自身项目代码。这份能力依赖 IDE 在后台解压 jar 并建立索引。

问题在于,源码始终以 jar 形态存在。一旦需要在源码上做全局检索、跨包比对,或让外部工具读取某段实现逻辑,jar 的封闭结构就成了障碍。IDE 之外的工具链无法像人一样点击跳转,它们面对的是一个二进制压缩包。

Agent 协同开发的盲区

当前 Agent 辅助开发越来越普遍。当代码补全、问题排查、重构建议需要参考第三方库实现细节时,Agent 需要直接读取源文件内容。

而 sources jar 是压缩归档,Agent 无法像读取工程内普通文件那样直接访问其中的 .java。要让 Agent 真正看见第三方源码,最直接的方式是把源码从 jar 中释放出来,以裸文件形式落到工程目录下。

这正是下载源码并解压到 external/ 目录的核心动机:把只读的参考源码工程化,让 IDE 与 Agent 都能便捷访问。

把源码从 jar 中解出来

Maven 提供了官方命令下载全部依赖的源码包:

bash 复制代码
mvn dependency:sources

执行后,sources jar 会落到本地仓库固定路径:

text 复制代码
~/.m2/repository/<groupId 转斜杠>/<artifactId>/<version>/<artifactId>-<version>-sources.jar

例如 Spring Boot 核心包:

text 复制代码
~/.m2/repository/org/springframework/boot/spring-boot/2.7.18/spring-boot-2.7.18-sources.jar

到这一步,源码仍在仓库深处。下一步是把它们解压到工程下的 external/ 目录,以 jar 名作为子目录名:

bash 复制代码
GROUP_ID="org.springframework.boot"
ARTIFACT_ID="spring-boot"
VERSION="2.7.18"

GROUP_PATH=$(echo "$GROUP_ID" | tr '.' '/')
SOURCE_JAR="$HOME/.m2/repository/$GROUP_PATH/$ARTIFACT_ID/$VERSION/$ARTIFACT_ID-$VERSION-sources.jar"
TARGET_DIR="external/${ARTIFACT_ID}-${VERSION}-sources"

mkdir -p "$TARGET_DIR"
unzip -o "$SOURCE_JAR" -d "$TARGET_DIR"

解压后的目录结构:

text 复制代码
java-project/
├── external/
│   ├── spring-boot-2.7.18-sources/
│   │   └── org/springframework/boot/
│   │       └── SpringApplication.java
│   ├── spring-web-5.3.31-sources/
│   │   └── org/springframework/web/
│   │       └── ...
│   └── ...
├── pom.xml

external/ 目录定位为只读参考区,不参与项目编译,仅用于集中存放与检索第三方源码。

starter 是空壳,源码在子依赖里

示例项目 pom.xml 引入的 spring-boot-starter-web 是个典型例子。它本身是空壳 POM,只负责聚合以下依赖:

text 复制代码
spring-boot-starter
spring-boot-starter-json
spring-boot-starter-tomcat
spring-web
spring-webmvc

这些 starter 内部没有 Java 代码,自然没有 sources jar。真正的源码分布在 spring-webspring-webmvcspring-boot 等子依赖中。

因此下载源码不能只看 pom 直接声明的依赖,必须遍历全部传递依赖,逐个定位真正包含代码的构件。

自动化脚本一键完成

手动逐个解压不现实。以下是自动化脚本的完整内容:

bash 复制代码
#!/bin/bash
set -euo pipefail

PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$PROJECT_DIR"

EXTERNAL_DIR="$PROJECT_DIR/external"
DEPS_FILE="$PROJECT_DIR/target/third-party-deps.txt"
M2_REPO="${HOME}/.m2/repository"

echo "==> Project directory: $PROJECT_DIR"
echo "==> External directory: $EXTERNAL_DIR"

mkdir -p "$EXTERNAL_DIR"

echo ""
echo "==> Step 1: Download dependency source jars"
mvn dependency:sources

echo ""
echo "==> Step 2: Resolve dependency list"
mkdir -p target
mvn dependency:list -DoutputFile="$DEPS_FILE"

echo ""
echo "==> Step 3: Extract source jars to $EXTERNAL_DIR"

extracted_count=0
missing_count=0

while IFS= read -r line || [[ -n "$line" ]]; do
    line=$(echo "$line" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')

    [[ -z "$line" ]] && continue
    [[ "$line" == "The following files have been resolved:" ]] && continue
    [[ "$line" == "The following files have NOT been resolved:" ]] && continue
    [[ "$line" =~ ^\[INFO\] ]] && continue

    IFS=':' read -r group artifact type version scope <<< "$line"

    [[ -z "$group" || -z "$artifact" || -z "$version" ]] && continue

    # 跳过 test scope 依赖
    scope=$(echo "$scope" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
    [[ "$scope" == "test" ]] && continue

    group_path=$(echo "$group" | tr '.' '/')
    jar_name="${artifact}-${version}-sources.jar"
    jar_path="$M2_REPO/$group_path/$artifact/$version/$jar_name"
    # 目录名直接使用 jar 文件名(去掉 .jar 后缀)
    dir_name="${jar_name%.jar}"
    target_dir="$EXTERNAL_DIR/$dir_name"

    if [[ ! -f "$jar_path" ]]; then
        echo "    [SKIP] source jar not found: $jar_path"
        missing_count=$((missing_count + 1))
        continue
    fi

    mkdir -p "$target_dir"
    echo "    [EXTRACT] $group:$artifact:$version -> $target_dir"
    unzip -o -q "$jar_path" -d "$target_dir"
    extracted_count=$((extracted_count + 1))

done < "$DEPS_FILE"

echo ""
echo "==> Done. Extracted: $extracted_count, missing sources: $missing_count"

脚本分三步完成:第一步调用 mvn dependency:sources 下载全部依赖的源码包到本地仓库;第二步调用 mvn dependency:list 将完整依赖清单输出到 target/third-party-deps.txt,供后续遍历;第三步逐行读取依赖清单,按 groupId 转路径规则定位 sources jar,跳过不存在的和 test 作用域的,解压到 external/ 目录下以 jar 名命名的子目录。脚本结束会输出成功解压数量与缺失数量,便于核对覆盖率。

实际落地结果

在示例项目执行脚本后,external/ 目录下落地二十多个源码包,覆盖 Spring 核心生态与内嵌 Tomcat。典型几个:

text 复制代码
spring-boot-2.7.18-sources
spring-boot-autoconfigure-2.7.18-sources
spring-context-5.3.31-sources
spring-core-5.3.31-sources
spring-web-5.3.31-sources
spring-webmvc-5.3.31-sources
tomcat-embed-core-9.0.83-sources
logback-core-1.2.12-sources
snakeyaml-1.30-sources

部分 starter 类依赖确实没有 sources jar,脚本自动跳过并计数,印证了前面提到的空壳特性。

注意事项

源码下载是一次性动作。除非依赖版本升级或新增依赖,否则无需重复执行,本地仓库已有的 sources jar 会被复用。

个别依赖未发布 sources jar 时,脚本会跳过并计入 missing 计数,多见于小众库或内部私有库,主流框架基本齐全。

external/ 目录定位为只读参考区,不参与编译。它的价值在于让第三方源码以裸文件形式集中存放,IDE 与 Agent 都能便捷访问。

相关推荐
念何架构之路1 小时前
restartmanager-重启管理子系统
java·开发语言
Java内核笔记1 小时前
告别第三方库!Spring Boot 4 原生 API 版本控制全解析:4 种策略 + 实战案例
java·后端
小小洋洋2 小时前
OpenWrt 从U盘迁移到内置 eMMC,并完成扩容与 Docker 安装
java·docker·eureka
就改了2 小时前
SpringBoot 自定义线程池 + 实时监控指标
java·spring boot·后端
plainGeekDev2 小时前
运行时获取依赖 → 编译时注入
android·java·kotlin
玖石书4 小时前
ASP.NET Core 迁移至 Spring系列:类库框架篇
java·后端·asp.net
笨蛋不要掉眼泪4 小时前
RabbitMQ消息队列:延迟消息
java·rabbitmq·java-rabbitmq
Adios7944 小时前
搜索二维矩阵 II
java·数据结构·算法
摇滚侠4 小时前
《SpringBoot 3:入门与应用实战》第 15 章 生产级特性 监控指标 Metrics 阅读笔记
java·spring boot·笔记
骇客野人5 小时前
IDEA 安装 Trae(TRAE AI: Coding Assistant)完整步骤
java·ide·intellij-idea