Java 部署脚本,支持回滚

Java 部署脚本

Java Sprintboot jar 项目启动、停止脚本:https://www.cnblogs.com/vipsoft/p/15952112.html

SpringBoot 不同的环境,打不同的包名: https://www.cnblogs.com/vipsoft/p/18577679

shell 复制代码
#!/bin/bash

# 使用说明
usage() {
    echo "使用说明:"
    echo "  ./deploy.sh deploy <新jar包>      # 部署新版本,默认使用文件里写好的"
    echo "  ./deploy.sh rollback <版本号>     # 回滚到指定版本(格式: 20241229_1430),默认使用文件里写好的"
    echo "  ./deploy.sh stop                  # 停止应用"
    echo "  ./deploy.sh start                 # 启动应用"
    echo "  ./deploy.sh restart               # 重启应用"
    echo "  ./deploy.sh status                # 查看状态"
    echo "  ./deploy.sh logs [行数]           # 查看日志(默认50行),默认为 tail -f"
    echo "  ./deploy.sh backup                # 备份当前版本"
    echo ""
    echo "示例:"
    echo "  ./deploy.sh deploy app-1.27.1.jar1229"
    echo "  ./deploy.sh rollback vipsoft-gateway-1.0.1.jar1229"
}


# 应用配置
RUN_JAR_NAME="vipsoft-gateway-1.0.1.jar"
NEW_JAR_NAME="vipsoft-gateway-1.0.2.jar"
BACK_JAR_NAME="vipsoft-gateway-1.0.1.jar1229"

JAVA_OPTS="-Xms512m -Xmx1024m -XX:+UseG1GC"
SPRING_OPTS="--spring.profiles.active=test"
LOG_DIR="./logs"
PID_FILE="./app.pid"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)

# 创建日志目录
mkdir -p $LOG_DIR

# 颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color

# 打印带颜色的消息
print_info() {
    echo -e "${GREEN}[INFO]${NC} $1"
}

print_warn() {
    echo -e "${YELLOW}[WARN]${NC} $1"
}

print_error() {
    echo -e "${RED}[ERROR]${NC} $1"
}


# 部署新版本
deploy_jar() {
    local new_jar="$1"
    if [ ! -z "$1" ]; then
        NEW_JAR_NAME=$new_jar
    fi

    # 检查文件是否以 .jar 结尾
    if [[ ! "$NEW_JAR_NAME" =~ \.jar$ ]]; then
        print_error "文件名必须以 .jar 结尾: ${NEW_JAR_NAME}"
        exit 1
    fi

    if [ ! -f "$NEW_JAR_NAME" ]; then
        print_error "部署版本不存在: ${NEW_JAR_NAME}"
        exit 1
    fi
    print_info "部署: ${NEW_JAR_NAME}"
 
    # 停止应用
    stop
    
    # 替换jar包
    if [ -f "$RUN_JAR_NAME" ]; then
        #mv "$RUN_JAR_NAME" "${RUN_JAR_NAME}.${TIMESTAMP}"
        mv "$RUN_JAR_NAME" "${BACK_JAR_NAME}"        
        print_info "备份: ${RUN_JAR_NAME} => ${BACK_JAR_NAME}"
    fi
    
    #mv "$new_jar" "$RUN_JAR_NAME"

    RUN_JAR_NAME=$NEW_JAR_NAME

    # 启动应用
    start
}


# 回滚到指定版本
rollback_version() {
    local backup_jar="$1" 
    if [ ! -z "$1" ]; then
        BACK_JAR_NAME=$backup_jar
    fi
    if [ ! -f "$BACK_JAR_NAME" ]; then
        print_error "备份版本不存在: ${BACK_JAR_NAME}"
        echo "可用的备份版本:"
        ls -l ${NEW_JAR_NAME}.* 2>/dev/null || echo "暂无备份"
        exit 1
    fi
 
    # 停止应用
    stop
     
    if [ -f "$RUN_JAR_NAME" ]; then
        #mv "$RUN_JAR_NAME" "${RUN_JAR_NAME}.${TIMESTAMP}"
        mv "$RUN_JAR_NAME" "${BACK_JAR_NAME}"        
        print_info "备份: ${RUN_JAR_NAME}"
    fi
 
    mv "$BACK_JAR_NAME" "$RUN_JAR_NAME"

    print_info "回滚到版本: ${BACK_JAR_NAME}"
    #deploy_jar "$backup_jar"

    #RUN_JAR_NAME=$NEW_JAR_NAME

    # 启动应用
    start
}

# 备份当前jar包
backup_current_jar() {
    if [ -f "$JAR_NAME" ]; then
        # 创建备份目录
        mkdir -p $BACKUP_DIR
        
        local backup_name="${JAR_NAME}.${TIMESTAMP}"
        print_info "备份当前版本: $backup_name"
        cp "$JAR_NAME" "$BACKUP_DIR/$backup_name"
        
        # 清理旧备份,保留最近5个
        (cd $BACKUP_DIR && ls -t ${JAR_NAME}.* 2>/dev/null | tail -n +6 | xargs rm -f 2>/dev/null || true)
    else
        print_warn "当前jar包不存在,无需备份"
    fi
}

start() {
    if [ -f $PID_FILE ] && kill -0 $(cat $PID_FILE) 2>/dev/null; then
        echo "${RUN_JAR_NAME} 应用已经在运行,PID: $(cat $PID_FILE)"
        exit 1
    fi
    
    echo "正在启动应用 ${RUN_JAR_NAME}..."
    nohup java $JAVA_OPTS -jar $RUN_JAR_NAME $SPRING_OPTS > $LOG_DIR/app.out 2>&1 &
    echo $! > $PID_FILE
    echo "应用启动成功 ${RUN_JAR_NAME},PID: $(cat $PID_FILE)"
    show_logs
}

stop() {
    if [ ! -f $PID_FILE ]; then
        echo "PID文件不存在,应用可能未运行"
        return 1
    fi
    
    PID=$(cat $PID_FILE)
    echo "正在停止应用 ${RUN_JAR_NAME},PID: ${PID}"
    
    # 优雅停止
    kill $PID 2>/dev/null
    
    # 等待最多30秒
    for i in {1..30}; do
        if kill -0 $PID 2>/dev/null; then
            sleep 1
        else
            echo "应用已停止 ${RUN_JAR_NAME}"
            rm -f $PID_FILE
            return 0
        fi
    done
    
    # 强制停止
    echo "强制停止应用 ${RUN_JAR_NAME}..."
    kill -9 $PID 2>/dev/null
    rm -f $PID_FILE
    echo "应用已强制停止 ${RUN_JAR_NAME}"
}

restart() {
    stop
    sleep 2
    start
}

status() {
    if [ -f $PID_FILE ] && kill -0 $(cat $PID_FILE) 2>/dev/null; then
        echo "应用正在运行,PID: $(cat $PID_FILE)"
        ps -p $(cat $PID_FILE) -o pid,cmd,lstart
    else
        echo "应用未运行"
    fi
}

# 查看日志
show_logs() {
    local logFile=$LOG_DIR/debug.log
    if [ -z "$1" ]; then
        tail -f $logFile
    elif [ -f "$logFile" ]; then
        local lines=${1:-50}
        tail -n $lines $logFile
    else
        print_warn "日志文件不存在:${logFile}"
    fi
}

# 主函数
main() {
    case "$1" in
        deploy)
            # if [ -z "$2" ]; then
            #     print_error "请指定要部署的jar包"
            #     usage
            #     exit 1
            # fi
            deploy_jar "$2"
            ;;
        rollback)
            # if [ -z "$2" ]; then
            #     print_error "请指定要回滚的版本号"
            #     usage
            #     exit 1
            # fi
            rollback_version "$2"
            ;;
        start)
            start
            ;;
        stop)
            stop
            ;;
        restart)
            restart
            ;;
        status)
            status
            ;;

        logs)
            show_logs "$2"
            ;;
        *)
            echo "使用说明: $0 {start|stop|restart|status}"
            exit 1
            ;;
    esac
}    

# 脚本入口
if [ $# -eq 0 ]; then
    usage
    exit 1
fi

main "$@"

说明

bash 复制代码
if [[ ! "$NEW_JAR_NAME" =~ \.jar$ ]]; then
    print_error "文件名必须以 .jar 结尾: ${NEW_JAR_NAME}"
    exit 1
fi
# =~ 是bash的正则匹配操作符
# \.jar 中的 \. 表示匹配实际的点号(正则中点是特殊字符)
# $ 表示字符串结尾
# 双中括号 [[ ]] 支持正则匹配,单中括号 [ ] 不支持

为什么有时候 $后面加 {} 有时候不加

这是Shell中变量引用的不同方式,主要区别在于明确变量边界和使用数组

1. 基本规则:什么时候需要 {}

需要 {} 的情况:

bash 复制代码
# 1. 变量名后面紧跟其他字符(没有空格分隔)
name="world"
echo "Hello, ${name}!"      # ✅ 正确: Hello, world!
echo "Hello, $name!"        # ⚠️ 有风险: 会被解释为 $name! 变量

# 2. 数组元素访问
files=("a.txt" "b.txt")
echo "${files[0]}"          # ✅ 正确: a.txt
echo "$files[0]"            # ❌ 错误: 只会输出 a.txt[0]

# 3. 特殊参数扩展
echo "${#name}"             # ✅ 输出字符串长度: 5
echo "$#name"               # ❌ 错误: 输出参数个数+name

# 4. 变量默认值
echo "${JAR_NAME:-default.jar}"  # ✅ 如果JAR_NAME为空,使用默认值

不需要 {} 的情况:

bash 复制代码
# 1. 变量名后面是空格或换行
name="world"
echo "Hello, $name"         # ✅ 正确: Hello, world

# 2. 变量名后面是特殊字符(会被shell识别为终止)
echo "Value: $name."        # ✅ 正确: Value: world.
echo "File: $name.txt"      # ✅ 正确(但如果有变量$nametxt会出错)

# 3. 简单变量引用
JAR_NAME="app.jar"
echo "检测到的Jar包: $JAR_NAME"  # ✅ 正确

2. 数组的特殊性

这是你问题中的关键区别:

bash 复制代码
# 数组变量必须使用 ${}
missing=("java" "git" "mvn")

# ✅ 正确:使用 ${missing[*]} 或 ${missing[@]}
echo "缺少命令: ${missing[*]}"      # 输出: 缺少命令: java git mvn
echo "缺少命令: ${missing[@]}"      # 同上(但在循环中有区别)

# ❌ 错误:不加 {} 会被当成普通变量
echo "缺少命令: $missing[*]"        # 输出: 缺少命令: java[*]
echo "缺少命令: $missing[@]"        # 输出: 缺少命令: java[@]

3. 具体对比分析

你的例子1:数组必须用 ${}

bash 复制代码
missing=("java" "git")

# 正确示例
echo "缺少命令: ${missing[*]}"  # ✅ 输出: 缺少命令: java git
echo "缺少命令: ${missing[@]}"  # ✅ 输出: 缺少命令: java git

# 错误示例
echo "缺少命令: $missing[*]"    # ❌ 输出: 缺少命令: java[*]
echo "缺少命令: $missing"       # ❌ 输出: 缺少命令: java(只显示第一个元素)

你的例子2:普通变量可以不用 {}

bash 复制代码
JAR_NAME="app-1.27.jar"

# 都可以,但第一种更安全
echo "检测到的Jar包: ${JAR_NAME}"  # ✅ 推荐
echo "检测到的Jar包: $JAR_NAME"    # ✅ 可接受

4. 完整示例展示区别

bash 复制代码
#!/bin/bash

# 场景1:普通变量
APP_NAME="myapp"
VERSION="1.27"

# 正确但可能有问题
echo "应用: $APP_NAME$VERSION"      # ✅ 输出: myapp1.27
echo "应用: ${APP_NAME}${VERSION}"  # ✅ 输出: myapp1.27(更明确)

# 有问题的情况
echo "应用: $APP_NAME_$VERSION"    # ❌ 尝试访问 $APP_NAME_ 变量
echo "应用: ${APP_NAME}_$VERSION"  # ✅ 正确: myapp_1.27

# 场景2:数组变量
commands=("start" "stop" "restart")

echo "命令: ${commands[*]}"         # ✅ 正确: start stop restart
echo "命令: $commands[*]"           # ❌ 错误: start[*]
echo "命令: $commands"              # ❌ 错误: start(只取第一个)

# 场景3:带特殊字符
file="data"
echo "文件: $file.txt"              # ⚠️ 可能出错:如果存在 $filetxt 变量
echo "文件: ${file}.txt"            # ✅ 安全: data.txt

5. 最佳实践建议

总是使用 {}(推荐)

bash 复制代码
# 始终使用 ${},这样最安全
echo "${variable}"
echo "${array[@]}"
echo "${JAR_NAME}"

安全引用表格

场景 推荐写法 风险写法 说明
普通变量 ${VAR} | $VAR 加{}总是安全的
变量拼接 ${VAR}text | $VARtext 避免歧义
数组所有元素 ${ARRAY[@]} | $ARRAY 普通引用只取第一个
数组特定元素 ${ARRAY[0]} | $ARRAY[0] 必须加{}
带特殊字符 ${VAR}_suffix | $VAR_suffix 避免错误解析

6. 高级用法:变量扩展

{} 还能实现强大的变量扩展功能:

bash 复制代码
# 1. 默认值
echo "${NAME:-无名氏}"      # 如果NAME为空,显示"无名氏"
echo "$NAME"                # 如果NAME为空,显示空

# 2. 变量长度
name="hello"
echo "长度: ${#name}"       # 输出: 5
echo "长度: $#name"         # 错误: 输出参数个数

# 3. 字符串切片
str="abcdefg"
echo "${str:0:3}"          # 输出: abc(从0开始取3个)
echo "${str: -3}"          # 输出: efg(最后3个)

# 4. 模式替换
path="/home/user/file.txt"
echo "${path/file/doc}"    # 输出: /home/user/doc.txt
echo "${path//\// }"       # 输出:  home user file.txt(替换所有/)

7. 你的具体问题解答

bash 复制代码
# 你的代码中:
echo "缺少命令: ${missing[*]}"  # ✅ 必须加{},因为missing是数组
echo "检测到的Jar包: $JAR_NAME"  # ✅ 可以不加,但加了更安全

# 更好的写法(总是加{}):
echo "缺少命令: ${missing[*]}"     # 明确表示是数组
echo "检测到的Jar包: ${JAR_NAME}"   # 安全,避免后续修改带来的问题

总结

  1. 数组变量必须用 ${}${array[@]}${array[*]}
  2. 普通变量建议用 ${}:更安全,更明确
  3. 变量名边界模糊时用 ${}${var}_suffix
  4. 使用高级功能时用 ${}:默认值、字符串操作等

黄金规则 :当有疑问时,总是使用 ${},这样永远不会出错。多两个字符能避免很多难以调试的问题。

在Shell脚本中,return 1exit 1 有重要的区别:

核心区别

特性 return 1 exit 1
作用范围 函数内部 整个脚本
执行后 返回调用处继续执行 立即终止脚本
可捕获 会被函数调用者接收 整个进程退出
退出状态 设置 $? 为1 脚本以状态码1退出

详细解释

1. return 1 - 函数返回值

bash 复制代码
#!/bin/bash

test_return() {
    echo "函数开始"
    if [ $1 -eq 0 ]; then
        return 0  # 成功
    else
        return 1  # 失败
    fi
}

test_return 0
echo "返回值: $?"  # 输出: 0

test_return 1
echo "返回值: $?"  # 输出: 1
echo "脚本继续执行..."  # 这行会执行

2. exit 1 - 退出脚本

bash 复制代码
#!/bin/bash

test_exit() {
    echo "函数开始"
    if [ $1 -eq 0 ]; then
        return 0
    else
        echo "发生错误,退出脚本"
        exit 1  # 整个脚本立即终止!
    fi
}

test_exit 0
echo "这行会执行"  # 会执行

test_exit 1
echo "这行永远不会执行"  # 不会执行