Maven-本地 Maven 仓库离线合并与修复脚本

背景

在部分项目依赖无法从公共仓库(Maven Central)获取,且公司私有仓库因网络隔离或权限问题不可访问的场景下,若同事本地 .m2/repository 中存在所需依赖,直接复制文件往往会导致元数据缺失、Checksum 校验失败或构建报错。为解决此问题,编写了 maven-repo-manager.sh 脚本,用于安全地从同事的本地仓库合并依赖至当前环境,并自动修复元数据。

脚本功能概述

该脚本是一个基于 Bash 的本地仓库管理工具,核心功能包括:

  • 备份 (backup):在执行高风险操作前自动备份当前仓库。

  • 合并 (merge):将指定源目录下的 JAR/POM 文件解析并重新安装到本地仓库,确保元数据完整。

  • 修复 (fix):扫描现有仓库,重新生成缺失的元数据和校验和。

  • 回滚 (rollback):恢复到最近一次备份状态。

  • 干跑模式 (dry-run):模拟执行过程,不修改实际文件。

核心逻辑实现

1. 依赖解析机制

脚本通过遍历源目录中的 .jar.pom 文件,根据 Maven 本地仓库的标准目录结构反推 GAV(Group ID, Artifact ID, Version):

# 示例路径解析逻辑
# Source: /tmp/repo/com/example/my-lib/1.0.0/my-lib-1.0.0.jar
# groupId: com.example
# artifactId: my-lib
# version: 1.0.0
# packaging: jar/pom

利用 sed 和字符串处理提取路径信息,避免依赖复杂的正则表达式,提高兼容性。

2. 安全合并策略

不同于简单的 cp 命令,脚本调用 Maven 自带的 install:install-file 插件进行重灌:

mvn -q install:install-file \
    -Dfile="$file" \
    -DgroupId="$groupId" \
    -DartifactId="$artifactId" \
    -Dversion="$version" \
    -Dpackaging="$packaging" \
    -DlocalRepositoryPath="$REPO"

优势

  • 自动生成正确的 .metadata 文件(包含版本号列表)。

  • 计算并写入 .sha1.md5 校验文件。

  • 确保本地仓库索引的一致性。

3. 容错与安全设计

  • 前置备份:每次 mergefix 操作前,自动创建带时间戳的备份目录(如 .backup/20260407_180000)。

  • 空间检查:在备份前检查磁盘剩余空间,防止因空间不足导致系统异常。

  • TTL 保护:同一小时内不重复备份,减少冗余 IO。

  • 日志记录:所有操作输出到 logs/manager_YYYYMMDD.log,便于排查问题。

  • 确认机制:非静默模式下,关键操作需用户输入 yes 确认。

使用示例

场景一:从同事机器合并依赖

假设同事的本地仓库位于 /tmp/colleague-m2

./maven-repo-manager.sh merge -s /tmp/colleague-m2 -y

参数说明:

  • -s: 指定源仓库路径。

  • -y: 自动确认,跳过交互提示。

场景二:修复损坏的本地仓库

如果本地仓库元数据损坏,可执行:

./maven-repo-manager.sh fix -y

场景三:预演操作(Dry Run)

在不实际修改文件的情况下查看将要执行的操作:

./maven-repo-manager.sh merge -s /tmp/colleague-m2 --dry-run

注意事项

  1. 环境依赖:目标机器必须已安装 Maven 且 mvn 命令可用。

  2. 目录规范:源目录必须符合标准 Maven 本地仓库结构(groupId/artifactId/version/)。

  3. SNAPSHOT 版本:脚本主要针对 Release 版本,SNAPSHOT 版本的动态更新可能需要额外处理。

  4. 并发限制:当前版本为串行处理,若依赖数量巨大,建议分批次执行。

总结

该脚本提供了一种轻量级、可控的离线依赖解决方案,特别适用于内网隔离、网络受限或临时应急场景。通过自动化元数据重建和备份回滚机制,有效降低了手动操作带来的风险。后续可根据团队需求扩展对 war/ear 包的支持或增加并行处理优化。

附录

#!/usr/bin/env bash
#
# ------------------------------------------------------------
# Script: maven-repo-manager.sh
# Author: DevOps Expert
# Description:
#   Enterprise-grade Maven local repository manager.
#   Supports backup, merge, fix, rollback with safety-first design.
#
# Usage:
#   ./maven-repo-manager.sh <command> [options]
#
# Commands:
#   backup      Backup repository
#   merge       Merge external repo into local repo
#   fix         Fix metadata and checksums
#   rollback    Rollback to last backup
#   help        Show help
#
# Options:
#   -r, --repo <path>     Local repo path (default ~/.m2/repository)
#   -s, --source <path>   Source repo path (for merge)
#   -f, --force           Force overwrite
#   -y, --yes             Auto confirm
#   -j, --jobs <num>      Parallel jobs (default 4)
#   --dry-run             Simulation mode
#
# Example:
#   ./maven-repo-manager.sh merge -s /tmp/repo -y
#
# ------------------------------------------------------------

set -euo pipefail

#######################################
# CONFIG
#######################################
DEFAULT_REPO="$HOME/.m2/repository"
BACKUP_DIR=".backup"
LOG_DIR="logs"
BACKUP_TTL=3600
JOBS=4
DRY_RUN=false

# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
NC='\033[0m'

#######################################
# GLOBAL VARS
#######################################
REPO="$DEFAULT_REPO"
SOURCE=""
FORCE=false
YES=false
START_TIME=$(date +%s)
LOG_FILE=""
GROUP_INPUT=""

#######################################
# LOGGING
#######################################
init_log() {
    mkdir -p "$LOG_DIR"
    LOG_FILE="$LOG_DIR/manager_$(date +%Y%m%d).log"
}

log() {
    local level="$1"; shift
    local msg="$*"
    echo -e "$msg"
    echo "[$(date '+%F %T')] [$level] $msg" >> "$LOG_FILE"
}

log_info()  { log "INFO"  "${GREEN}[INFO]${NC} $*"; }
log_warn()  { log "WARN"  "${YELLOW}[WARN]${NC} $*"; }
log_error() { log "ERROR" "${RED}[ERROR]${NC} $*"; }

#######################################
# UTIL
#######################################
confirm() {
    if [ "$YES" = true ]; then return 0; fi
    echo -n "Type 'yes' to continue: "
    read -r ans
    [ "$ans" = "yes" ]
}

run_cmd() {
    if [ "$DRY_RUN" = true ]; then
        log_info "[DRY-RUN] $*"
    else
        eval "$@"
    fi
}

get_repo_size() {
    du -sk "$REPO" | awk '{print $1}'
}

get_free_space() {
    df -k "$REPO" | awk 'NR==2 {print $4}'
}

#######################################
# BACKUP
#######################################
backup_repo() {
    mkdir -p "$BACKUP_DIR"

    local last_backup
    last_backup=$(ls -1 "$BACKUP_DIR" 2>/dev/null | tail -n1 || true)

    if [ -n "$last_backup" ]; then
        local last_time
        last_time=$(date -j -f "%Y%m%d_%H%M%S" "$last_backup" +%s 2>/dev/null || date -d "$last_backup" +%s)
        local now
        now=$(date +%s)

        if (( now - last_time < BACKUP_TTL )); then
            log_warn "Skip backup (within TTL)"
            return
        fi
    fi

    local repo_size free_space
    repo_size=$(get_repo_size)
    free_space=$(get_free_space)

    if (( free_space < repo_size * 12 / 10 )); then
        log_error "Not enough disk space"
        exit 1
    fi

    local ts
    ts=$(date +%Y%m%d_%H%M%S)
    local target="$BACKUP_DIR/$ts"

    log_info "Creating backup: $target"
    run_cmd "cp -a \"$REPO\" \"$target\""
}

#######################################
# ROLLBACK
#######################################
rollback_repo() {
    local last
    last=$(ls -1 "$BACKUP_DIR" | tail -n1)

    if [ -z "$last" ]; then
        log_error "No backup found"
        exit 1
    fi

    log_warn "Rollback to $last"
    confirm || exit 1

    run_cmd "rm -rf \"$REPO\""
    run_cmd "cp -a \"$BACKUP_DIR/$last\" \"$REPO\""

    log_info "Rollback complete"
}

#######################################
# GAV PARSE
#######################################
parse_pom() {
    local pom="$1"

    groupId=$(sed -n 's:.*<groupId>\(.*\)</groupId>.*:\1:p' "$pom" | head -1)
    artifactId=$(sed -n 's:.*<artifactId>\(.*\)</artifactId>.*:\1:p' "$pom" | head -1)
    version=$(sed -n 's:.*<version>\(.*\)</version>.*:\1:p' "$pom" | head -1)
    packaging=$(sed -n 's:.*<packaging>\(.*\)</packaging>.*:\1:p' "$pom" | head -1)

    packaging=${packaging:-jar}
}

#######################################
# FIX MISSING (NEW FUNCTION)
#######################################
fix_missing() {
    if [ -z "$GROUP_INPUT" ]; then
        log_error "Missing required argument: -g <groupId:artifactId:version>"
        log_info "Example: ./maven-repo-manager.sh fix-missing -g com.taobao:parent:1.0.1"
        exit 1
    fi

    log_info "Parsing GAV input: $GROUP_INPUT"

    # 智能解析 groupId:artifactId:version
    IFS=':' read -r gid aid ver <<< "$GROUP_INPUT"

    if [ -z "$gid" ] || [ -z "$aid" ] || [ -z "$ver" ]; then
        log_error "Invalid format. Expected 'groupId:artifactId:version'"
        exit 1
    fi

    log_info "Resolved: Group=$gid, Artifact=$aid, Version=$ver"

    # 创建临时目录
    local TEMP_DIR
    TEMP_DIR=$(mktemp -d)
    local POM_FILE="$TEMP_DIR/pom.xml"

    # 生成最小化合法 POM
    cat > "$POM_FILE" << EOF
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    
    <groupId>$gid</groupId>
    <artifactId>$aid</artifactId>
    <version>$ver</version>
    <packaging>pom</packaging>
    
    <name>Placeholder Parent POM</name>
    <description>Minimal placeholder generated by maven-repo-manager to resolve missing dependency.</description>
    
    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>
    
    <dependencyManagement/>
    <build/>
</project>
EOF

    # 执行安装
    log_info "Installing placeholder to local repo..."
    
    # 构建命令
    local CMD="mvn -q install:install-file \
        -Dfile=\"$POM_FILE\" \
        -DgroupId=\"$gid\" \
        -DartifactId=\"$aid\" \
        -Dversion=\"$ver\" \
        -Dpackaging=pom \
        -DlocalRepositoryPath=\"$REPO\""

    if [ "$DRY_RUN" = true ]; then
        log_info "[DRY-RUN] Would execute: $CMD"
    else
        eval "$CMD"
        if [ $? -ne 0 ]; then
            log_error "Failed to install placeholder."
            rm -rf "$TEMP_DIR"
            exit 1
        fi
    fi

    # 清理
    rm -rf "$TEMP_DIR"
    log_info "✅ Placeholder created successfully: $gid:$aid:$ver"
}

#######################################
# MERGE
#######################################
merge_repo() {
    [ -z "$SOURCE" ] && { log_error "Source required"; exit 1; }

    backup_repo

    log_info "Merging via Maven install..."

    find "$SOURCE" -type f \( -name "*.jar" -o -name "*.pom" \) | while read -r file; do

        dir=$(dirname "$file")
        version=$(basename "$dir")
        artifact_dir=$(dirname "$dir")
        artifactId=$(basename "$artifact_dir")
        groupPath=$(echo "$artifact_dir" | sed "s|$SOURCE/||")
        groupId=$(echo "$groupPath" | sed "s|/$artifactId||" | tr '/' '.')

        filename=$(basename "$file")

        # 推断 packaging
        case "$filename" in
            *.pom)
                packaging="pom"
                ;;
            *.jar)
                packaging="jar"
                ;;
            *)
                log_warn "Skip unsupported file: $file"
                continue
                ;;
        esac

        log_info "Installing: $groupId:$artifactId:$version ($packaging)"

        cmd="mvn -q install:install-file \
            -Dfile=\"$file\" \
            -DgroupId=\"$groupId\" \
            -DartifactId=\"$artifactId\" \
            -Dversion=\"$version\" \
            -Dpackaging=\"$packaging\" \
            -DlocalRepositoryPath=\"$REPO\""

        if [ "$FORCE" = true ]; then
            cmd="$cmd -DupdateReleaseInfo=true"
        fi

        run_cmd "$cmd"
    done

    log_info "Merge (via mvn) complete"
}

#######################################
# CHECKSUM
#######################################
fix_repo() {
    backup_repo

    log_info "Fixing repo via Maven reinstall..."

    find "$REPO" -type f \( -name "*.jar" -o -name "*.pom" \) | while read -r file; do

        dir=$(dirname "$file")
        version=$(basename "$dir")
        artifact_dir=$(dirname "$dir")
        artifactId=$(basename "$artifact_dir")

        groupPath=$(echo "$artifact_dir" | sed "s|$REPO/||")
        groupId=$(echo "$groupPath" | sed "s|/$artifactId||" | tr '/' '.')

        filename=$(basename "$file")

        case "$filename" in
            *.pom)
                packaging="pom"
                ;;
            *.jar)
                packaging="jar"
                ;;
            *)
                continue
                ;;
        esac

        log_info "Reinstalling: $groupId:$artifactId:$version"

        cmd="mvn -q install:install-file \
            -Dfile=\"$file\" \
            -DgroupId=\"$groupId\" \
            -DartifactId=\"$artifactId\" \
            -Dversion=\"$version\" \
            -Dpackaging=\"$packaging\" \
            -DlocalRepositoryPath=\"$REPO\""

        run_cmd "$cmd"
    done

    log_info "Fix complete (Maven rebuild)"
}

#######################################
# ARG PARSE
#######################################
parse_args() {
    COMMAND="${1:-help}"
    shift || true

    while [[ $# -gt 0 ]]; do
        case "$1" in
            -r|--repo) REPO="$2"; shift 2 ;;
            -s|--source) SOURCE="$2"; shift 2 ;;
            -f|--force) FORCE=true; shift ;;
            -y|--yes) YES=true; shift ;;
            -j|--jobs) JOBS="$2"; shift 2 ;;
            --dry-run) DRY_RUN=true; shift ;;
            -g|--group) GROUP_INPUT="$2"; shift 2 ;; 
            *) shift ;;
        esac
    done
}

#######################################
# HELP
#######################################
show_help() {
    echo "Maven Repo Manager"
    echo ""
    echo "Usage: $0 <command> [options]"
    echo ""
    echo "Commands:"
    echo "  backup                  Backup repository"
    echo "  merge -s <path>         Merge external repo into local repo"
    echo "  fix                     Fix metadata and checksums"
    echo "  rollback                Rollback to last backup"
    echo "  fix-missing -g <gav>    Create placeholder for missing dependency (e.g., com.taobao:parent:1.0.1)"
    echo ""
    echo "Options:"
    echo "  -r, --repo <path>       Local repo path (default ~/.m2/repository)"
    echo "  -s, --source <path>     Source repo path (for merge)"
    echo "  -g, --group <gav>       GAV string for fix-missing command"
    echo "  -f, --force             Force overwrite"
    echo "  -y, --yes               Auto confirm"
    echo "  -j, --jobs <num>        Parallel jobs (default 4)"
    echo "  --dry-run               Simulation mode"
    echo ""
    echo "Examples:"
    echo "  $0 merge -s /tmp/repo -y"
    echo "  $0 fix-missing -g com.kingbase8:pgjdbc-core-parent:1.1.2"
}

#######################################
# MAIN
#######################################
main() {
    parse_args "$@"
    init_log

    case "$COMMAND" in
        backup) backup_repo ;;
        merge) merge_repo ;;
        fix) fix_repo ;;
        rollback) rollback_repo ;;
        fix-missing) fix_missing ;;
        help|*) show_help ;;
    esac

    local end
    end=$(date +%s)

    log_info "Done in $((end - START_TIME))s"
}

main "$@"


Maven-本地 Maven 仓库离线合并与修复脚本
https://blog.cikaros.cn/archives/wei-ming-ming-wen-zhang-OCVMsio0
作者
Cikaros
发布于
2026年04月07日
许可协议