使用脚本快速从本地提取、替换生产服务器class文件

生产环境遇到bug,需要紧急修改几个Java文件,然后尽快部署到生产系统中。如果代码中混有其他改动,无法完整打包,那么如何尽快提取出改完的class文件并快速地放到服务器的正确位置呢?

从本机提取文件

先在自己的IDE中把代码改出来,编译,运行。避免打WAR包,否则还要费心思解压,而且有些系统(例如Mac)解出来之后时间戳还不对。

重新生成的class文件的修改时间肯定比其他class文件新,这样我们就能用一些脚本将它们挑出来了。

Windows系统

使用PowerShell脚本。放在 D:\tiqu.ps1 中:

[CmdletBinding()]
Param (
    [string] 
    $from = '.', 

    [string]
    $to = '..\output',

    [AllowNull()]
    [string]
    $time,

    [AllowNull()]
    [int]
    $min,

    [int]
    $flat = 0
)

if ($time) {
    $minTime = [DateTime]::ParseExact($time, 'hh:mm', $null)
} elseif ($min) {
    $minTime = (Get-Date).AddMinutes(-$min);
} else {
    $minTime = [DateTime]::ParseExact('00', 'hh', $null)
}

# 新建文件夹
New-Item -Path $to -ItemType Directory -Force | Out-Null

# 搜索文件
Push-Location $from

$fileList = Get-ChildItem -Path . -Recurse | ? {$_.LastWriteTime -gt $minTime}
foreach ($file in $fileList) {
    $relativePath = Resolve-Path -Path $file.FullName -Relative
    
    if ($flat -eq 0) {
        # 保持目录结构
        Write-Output $relativePath
        $newPath = Join-Path -Path $to -ChildPath $relativePath
        $newDir = Split-Path -Path $newPath
        New-Item -Path $newDir -ItemType Directory -Force | Out-Null
        Copy-Item -Path $file.FullName -Destination $newDir -Force | Out-Null
    } else {
        # 不保持目录结构,直接复制
        Write-Output $file.Name
        Copy-Item -Path $file.FullName -Destination $to -Force | Out-Null
    }
}

Pop-Location

操作时,启动PowerShell,输入

# 进入到Web应用所在目录,IDEA通常是artifacts,Eclipse通常是WebRoot
cd X:\xxxxxx\out\artifacts

# 提取5分钟之前到现在修改的文件,放到 ..\临时替换 目录中
D:\tiqu.ps1 -to ..\临时替换 -min 5

# 提取从9:20到现在之间修改的文件,放到 ..\output 目录中
D:\tiqu.ps1 -time 09:20 

Linux/Mac/Cygwin

将以下脚本放在/usr/local/bin/tiqu中,设置好x权限。

注意Mac系统自带getopt是BSD版本,功能比GNU版少,只支持一个字母的短参数,需另外安装gnu-getopt(后面假设安装到了/usr/local/opt/gnu-getopt/bin/getopt)。

#!/bin/bash
 
help() {
    echo "tiqu [options]"
    echo "默认值:把当前目录、当天修改文件放到 out 目录中"
    echo
    echo "  --from=<path>, -f       指定待查找目录"
    echo "  --to=<path>, -t         指定放置位置,如果没有则自动 mkdir"
    echo "  --time=<HH:mm>          复制 HH:mm 之后修改的文件"
    echo "  --min=<min>             复制从 min 分钟之前到现在的文件,和上面参数冲突"
    echo "  --exclude=<list>, -e    复制时排除符合<list>规则的文件"
    echo "  --exclude-from=<file>   复制时排除匹配了<file>规则配置列表文件的文件"
    echo "  --flat, -b              把所有文件平摊到同一目录中,不要保持目录结构(默认值:保持目录结构)"
    echo "  --help                  显示本帮助"
    echo
}
 
fromPath="."
toPath="./out"
findTime="00:00"
findMin=-1
excludeCmd=""
flat=0
 
# 苹果系统
export PATH="/usr/local/opt/gnu-getopt/bin:$PATH"
 
TEMP=`getopt -o hf:t:e:b --long help,from:,to:,time:,min:,exclude:,exclude-from,flat -- "$@"`
eval set -- "$TEMP"
 
while true; do
    case "$1" in
        -f|--from)
            fromPath="$2"
            shift 2
            ;;
        -t|--to)
            toPath="$2"
            shift 2
            ;;
        --time)
            if [[ "$2" =~ ^[01]?[0-9]:[0-5][0-9]$ ]]; then
                findTime="$2"
            else
                echo 错误:无效时间格式!
                exit 1
            fi
            shift 2
            ;;
        --min)
            findMin="$2"
            shift 2
            ;;
        -e|--exclude)
            excludeCmd="--exclude=\"$2\""
            shift 2
            ;;
        --exclude-from)
            excludeCmd="--exclude-from=\"$2\""
            shift 2
            ;;
        -b|--flat)
            flat=1
            shift 1
            ;;
        -h|--help)
            help
            exit 0
            ;;
        --)
            shift
            break
            ;;
        *)
            # echo "Internal error!"
            # exit 1
            ;;
    esac
done
 
# 计算时间
if [ $findMin -eq -1 ]; then
    IFS=: read nowh nowm nows <<< "`date +%T`"
    IFS=: read bh bm <<< "$findTime"

    nowh=${nowh#0}
    nowm=${nowm#0}
    bh=${bh#0}
    bm=${bm#0}
 
    findMin=$(((nowh*60+nowm)-(bh*60+bm)))
    if [ $findMin -lt 0 ]; then
        findMin=0
    fi
fi
 
findMin=$((-findMin))
 
# 将toPath转成绝对路径
mkdir -p "$toPath"
cd "$toPath"
toPath="`pwd`"
cd -
 
# 使用rsync复制文件
cd "$fromPath"
if [ $flat -eq 0 ]; then
    # 保持目录结构
    find . -type f -mmin $findMin -exec rsync -aR {} "$toPath" \;
else
    # 不要保持目录结构
    find . -type f -mmin $findMin -exec rsync -a {} "$toPath" \;
fi
 
# 展示已复制文件
cd "$toPath"
find .

操作时:

# 进入到Web应用所在目录,IDEA通常是artifacts,Eclipse通常是WebRoot
cd xxxxxx/out/artifacts

# 提取5分钟之前到现在修改的文件,放到 ../临时替换 目录中
tiqu --to=../临时替换 --min=5

# 提取从9:20到现在之间修改到文件,放到 ./out 目录中
tiqu --time=9:20 

程序会自动搜索已修改文件,并且会按目录结构组织好,之后你就可以直接打包了。

在服务器上替换(Linux)

如果已经按目录结构整理好,直接解压替换(unzip -o xxx.zip -d 应用所在目录)便是。对于war包,可使用zip命令直接把文件替换到包里面(cd xxx; zip -r war包 *)。

如果提供的是散装的class文件,那么找目录会很费劲,可以借助脚本自动找目录,自动替换。可修改以下脚本中的MYBASE变量,然后保存到/usr/local/bin/tihuan中,加好x权限:

#!/bin/bash

# 应用所在位置。通常不会变化,可以写死在脚本里头。
MYBASE=/opt/xxx

help() {
    echo "tihuan [options] 文件名1 文件名2 ..."
    echo
    echo "  --to=<path>, -t         指定搜索目录(默认值:${MYBASE})"
    echo "  --dry-run, -n           只生成命令,不要实际替换"
    echo "  --force, -f             不用确认,直接动手替换"
    echo "  --help                  显示本帮助"
}
 
# 苹果系统
export PATH="/usr/local/opt/gnu-getopt/bin:$PATH"
 
TEMP=`getopt -o ht:nf --long help,to:,dry-run,force -- "$@"`
eval set -- "$TEMP"

toPath="$MYBASE"
dryrun=0
force=0
 
while true; do
    case "$1" in
        -t|--to)
            toPath="$2"
            shift 2
            ;;
        -n|--dry-run)
            dryrun=1
            shift 1
            ;;
        -f|--force)
            force=1
            shift 1
            ;;
        -h|--help)
            help
            exit 0
            ;;
        --)
            shift
            break
            ;;
        *)
            # echo "Internal error!"
            # exit 1
            ;;
    esac
done

i=0
for file in "$@"; do
    i=$((i+1))

    if ! [ -f "$file" ]; then
        >&2 echo "[文件 #${i}] 源文件不存在:${file}"
        continue
    fi

    fileName=`basename "$file"`

    find "$toPath" -name "$fileName" > /tmp/tmpfilelist
    list=`cat /tmp/tmpfilelist`

    if [ -n "$list" ] && [ "$list" != "\n" ]; then
        echo "[文件 #${i}] 已找到"

        cat /tmp/tmpfilelist | while read target; do
            echo $target
        done

        if [ $force -eq 0 ]; then
            echo -n "是否继续替换 [y/n]?"
            read choice
        else
            choice="y"
        fi

        if [ "$choice" == "y" ]; then
            if [ $dryrun -eq 0 ]; then
                echo "[文件 #${i}] 已进行替换"
            else
                echo "[文件 #${i}] 请运行以下命令"
            fi
            cat /tmp/tmpfilelist | while read target; do
                if [ $dryrun -eq 0 ]; then
                    cp "$1" "$target"
                else
                    echo "cp \"$file\" \"$target\""
                fi
            done
        else
            echo "[文件 #${i}] 已放弃替换"
        fi
    else
        echo "[文件 #${i}] 未找到文件,未进行替换"
    fi

    echo
done

rm /tmp/tmpfilelist 2> /dev/null

操作时:

# 此处省略下载文件与备份操作。实际操作时注意备份!
# 以下根据实际情况二选一

# 展开(exploded)模式
tihuan XXXXX.class

# war包模式
mkdir tmp
unzip -o war包 -d tmp
tihuan --to=tmp XXXXX.class
cd tmp
zip -r 原先的war包 *

后续操作

  • 本文的操作应当只用于应急处置。问题处理好之后,代码该提交提交,基线该打patch打patch,忘了的话就是极大的隐患。
  • 这次事情紧急,直接从开发库中取了文件。给不紧急的bug打patch时,一定要取基线代码,从基线上打,打完再合并回去。
  • 每次正式升级之前一定要打基线,从基线取代码,打整包,生产环境配置文件用事先预备好的备份覆盖升级包,不要单独换文件!换文件升级,有第一次,就会有第二次。越是换文件,后面就越不敢打整包,几个星期以后,全世界就没有人知道生产环境与本地代码有什么区别了。