cd ..

SpringBoot 应用自动部署脚本

本文档详细说明了 SpringBoot 应用的部署脚本和执行脚本的使用方法,包括在不同环境下的部署策略。

每次改完代码,你是不是都要经历这样一套流程:手动 mvn package、打开 FTP 工具上传 JAR 包、SSH 登上服务器、停掉旧服务、启动新服务、再看看日志有没有报错?整套操作下来少说五分钟,一天部署个三四次就是二十分钟纯机械劳动。

更要命的是,手动操作总会犯错——忘了停旧进程导致端口冲突、传错了 JAR 包版本、配置文件没跟着更新。这些”低级失误”往往发生在你最忙、最没耐心的时候。

所以,把部署流程脚本化是必要的。下面分享两个我在实际项目中使用的脚本:一个管理应用生命周期的 start.sh,一个实现一键部署的 deploy.sh

应用启动脚本 (start.sh)

这个脚本负责应用在服务器上的启动、停止、重启和状态查看。它部署在远程服务器上,是整个自动化流程的基础。

脚本内容

start.sh
#!/bin/bash

# 应用名称
APP_NAME="yg-admin"
# JAR包路径
JAR_FILE="yg-admin.jar"
# 配置文件路径
CONFIG_FILE="application-prod.yml"
# 激活的配置文件
PROFILE="prod"
# JVM参数
JVM_OPTS="-Dfile.encoding=UTF-8"

# 获取应用PID
get_pid() {
  PID=$(pgrep -f "$JAR_FILE")
  echo $PID
}

# 检查应用是否运行
is_running() {
  PID=$(get_pid)
  if [ -n "$PID" ]; then
      return 0  # 正在运行
  else
      return 1  # 未运行
  fi
}

# 启动应用
start() {
  echo "正在启动 $APP_NAME..."

  # 检查应用是否已经在运行
  if is_running; then
      PID=$(get_pid)
      echo "$APP_NAME 已经在运行中,PID: $PID"
      return 1
  fi

  # 检查JAR文件是否存在
  if [ ! -f "$JAR_FILE" ]; then
      echo "错误: $JAR_FILE 文件不存在!"
      return 1
  fi

  # 检查配置文件是否存在
  if [ ! -f "$CONFIG_FILE" ]; then
      echo "警告: $CONFIG_FILE 配置文件不存在!"
      read -p "是否继续启动?(y/n): " CONTINUE
      if [ "$CONTINUE" != "y" ]; then
          echo "启动已取消"
          return 1
      fi
  fi

  # 启动应用
  nohup java $JVM_OPTS -jar $JAR_FILE --spring.config.additional-location=file:$CONFIG_FILE --spring.profiles.active=$PROFILE >/dev/null 2>&1 &

  # 记录PID
  PID=$!
  echo "应用正在启动,PID: $PID"

  # 等待几秒,确认应用是否成功启动
  sleep 5
  if ps -p $PID > /dev/null; then
      echo "$APP_NAME 启动成功!"
      return 0
  else
      echo "$APP_NAME 启动失败,请检查日志!"
      return 1
  fi
}

# 停止应用
stop() {
  echo "正在停止 $APP_NAME..."

  # 检查应用是否在运行
  if ! is_running; then
      echo "$APP_NAME 没有运行!"
      return 0
  fi

  # 获取PID
  PID=$(get_pid)

  # 尝试优雅停止
  echo "正在停止进程 PID: $PID..."
  kill -15 $PID

  # 等待应用停止
  TIMEOUT=30
  for ((i=0; i<TIMEOUT; i++)); do
      if ! ps -p $PID > /dev/null; then
          echo "$APP_NAME 已成功停止!"
          return 0
      fi
      sleep 1
      echo -n "."
  done

  # 如果超时,强制终止
  echo ""
  echo "无法优雅停止应用,正在强制终止..."
  kill -9 $PID

  # 再次检查
  sleep 2
  if ! ps -p $PID > /dev/null; then
      echo "$APP_NAME 已被强制终止!"
      return 0
  else
      echo "错误:无法终止 $APP_NAME!"
      return 1
  fi
}

# 重启应用
restart() {
  echo "正在重启 $APP_NAME..."

  # 先停止应用
  if is_running; then
      stop
      # 等待一段时间确保完全停止
      sleep 5
  fi

  # 然后启动应用
  start
}

# 显示应用状态
status() {
  if is_running; then
      PID=$(get_pid)
      echo "$APP_NAME 正在运行,PID: $PID"
      # 显示运行时间
      RUNTIME=$(ps -o etime= -p $PID)
      echo "运行时间: $RUNTIME"
  else
      echo "$APP_NAME 没有运行"
  fi
}

# 帮助信息
show_help() {
  echo "使用方法: $0 {start|stop|restart|status}"
  echo "  start   - 启动应用"
  echo "  stop    - 停止应用"
  echo "  restart - 重启应用"
  echo "  status  - 显示应用状态"
}

# 主函数
case "$1" in
  start)
      start
      ;;
  stop)
      stop
      ;;
  restart)
      restart
      ;;
  status)
      status
      ;;
  *)
      show_help
      exit 1
      ;;
esac

exit $?

设计要点

这个脚本有几个值得注意的细节:

优雅停止优先stop 函数先发 kill -15(SIGTERM),给应用 30 秒时间处理正在进行的请求和释放资源。只有超时了才会 kill -9 强制终止。这在生产环境中非常重要——粗暴地杀进程可能导致数据不一致。

启动前的安全检查:启动前会检查应用是否已经在运行(防止重复启动)、JAR 文件是否存在、配置文件是否就位。这些看似简单的检查,能避免大量部署事故。

配置参数集中管理:应用名、JAR 路径、配置文件等全部在脚本顶部声明为变量。换项目时只需要改这几行,脚本的其余部分完全复用。

应用部署脚本 (deploy.sh)

有了 start.sh 管理服务器上的应用生命周期,接下来需要一个在本地执行的脚本,把”构建 - 上传 - 重启”这条链路串起来。

脚本内容

deploy.sh
#!/bin/bash

export JAVA_HOME=$(/usr/libexec/java_home -v 1.8)

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

# 定义服务器信息
SERVER_ALIAS="yg_iot"
SERVER_PATH="/home/admin/backend"
JAR_NAME="yg-admin.jar"

echo -e "${YELLOW}开始构建项目...${NC}"

mvn clean && mvn package

# 检查Maven构建是否成功
if [ $? -ne 0 ]; then
  echo -e "${RED}Maven构建失败!部署终止。${NC}"
  exit 1
fi

echo -e "${YELLOW}构建成功,准备部署到服务器...${NC}"

# 删除服务器上的旧JAR包
echo -e "${YELLOW}删除服务器上的旧JAR包...${NC}"
ssh $SERVER_ALIAS "cd $SERVER_PATH && rm -f $JAR_NAME"

# 上传新的JAR包
echo -e "${YELLOW}上传新的JAR包到服务器...${NC}"
cd yg-admin/target && scp $JAR_NAME $SERVER_ALIAS:$SERVER_PATH/

# 检查上传是否成功
if [ $? -ne 0 ]; then
  echo -e "${RED}JAR包上传失败!部署终止。${NC}"
  exit 1
fi

# 重启应用
echo -e "${YELLOW}重启应用...${NC}"
ssh $SERVER_ALIAS "cd $SERVER_PATH && sh start.sh restart"

# 检查重启是否成功
if [ $? -ne 0 ]; then
  echo -e "${RED}应用重启失败!请检查服务器日志。${NC}"
  exit 1
else
  echo -e "${GREEN}部署完成!应用已成功重启。${NC}"
fi

# 显示应用状态
echo -e "${YELLOW}获取应用状态...${NC}"
ssh $SERVER_ALIAS "cd $SERVER_PATH && sh start.sh status"

echo -e "${GREEN}部署流程全部完成!${NC}"

工作流程

整个部署流程是一条直线:Maven 构建 -> 删除旧包 -> 上传新包 -> 重启服务 -> 确认状态。每一步都有错误检查,任何一环失败都会立即终止并给出红色提示,不会带着错误继续往下走。

这里有个前提条件:你需要在 ~/.ssh/config 中配置好服务器别名(如 yg_iot),并且设置了免密登录。否则每次 SSH 和 SCP 都会弹出密码输入框,自动化也就无从谈起了。

Windows 环境怎么办?

不是所有团队都用 Mac 或 Linux 开发。如果你在 Windows 上工作,有几种方案可以选择。

1. 使用 Git Bash 或 WSL

最省事的方案。Git Bash 或 WSL 提供了 Linux 兼容的 Shell 环境,上面的脚本几乎不用改,只需要调整 Java 路径:

#!/bin/bash

# Windows下设置Java环境变量(使用具体路径)
export JAVA_HOME="C:/Program Files/Java/jdk1.8.0_xxx"
export PATH=$JAVA_HOME/bin:$PATH

# 其余脚本保持不变
...

2. 使用 PowerShell 脚本

如果团队更习惯 Windows 原生工具,PowerShell 是更好的选择。语法和 Bash 不同,但逻辑完全一致:

deploy.ps1
# PowerShell脚本 - deploy.ps1

# 设置Java环境
$env:JAVA_HOME = "C:\Program Files\Java\jdk1.8.0_xxx"
$env:Path = "$env:JAVA_HOME\bin;$env:Path"

# 定义颜色
$GREEN = @{ForegroundColor = "Green"}
$RED = @{ForegroundColor = "Red"}
$YELLOW = @{ForegroundColor = "Yellow"}

# 定义服务器信息
$SERVER_ALIAS = "yg_iot"
$SERVER_PATH = "/home/admin/backend"
$JAR_NAME = "yg-admin.jar"

Write-Host "开始构建项目..." @YELLOW

# 清理并打包项目
mvn clean
if ($LASTEXITCODE -ne 0) {
  Write-Host "Maven清理失败!" @RED
  exit 1
}

mvn package
if ($LASTEXITCODE -ne 0) {
  Write-Host "Maven构建失败!部署终止。" @RED
  exit 1
}

Write-Host "构建成功,准备部署到服务器..." @YELLOW

# 使用SSH命令(需要安装OpenSSH客户端)
# 删除服务器上的旧JAR包
Write-Host "删除服务器上的旧JAR包..." @YELLOW
ssh $SERVER_ALIAS "cd $SERVER_PATH && rm -f $JAR_NAME"

# 上传新的JAR包
Write-Host "上传新的JAR包到服务器..." @YELLOW
Set-Location -Path "yg-admin\target"
scp $JAR_NAME "${SERVER_ALIAS}:${SERVER_PATH}/"

if ($LASTEXITCODE -ne 0) {
  Write-Host "JAR包上传失败!部署终止。" @RED
  exit 1
}

# 重启应用
Write-Host "重启应用..." @YELLOW
ssh $SERVER_ALIAS "cd $SERVER_PATH && sh start.sh restart"

if ($LASTEXITCODE -ne 0) {
  Write-Host "应用重启失败!请检查服务器日志。" @RED
  exit 1
} else {
  Write-Host "部署完成!应用已成功重启。" @GREEN
}

# 显示应用状态
Write-Host "获取应用状态..." @YELLOW
ssh $SERVER_ALIAS "cd $SERVER_PATH && sh start.sh status"

Write-Host "部署流程全部完成!" @GREEN

3. 使用批处理脚本

对于一些老旧环境或者不方便安装 PowerShell 的场景,传统的 batch 脚本也能胜任。不过需要用 PuTTY 工具集来处理 SSH 连接:

deploy.batch
@echo off
setlocal enabledelayedexpansion

:: 设置Java环境
set "JAVA_HOME=C:\Program Files\Java\jdk1.8.0_xxx"
set "PATH=%JAVA_HOME%\bin;%PATH%"

:: 定义服务器信息
set "SERVER_ALIAS=yg_iot"
set "SERVER_PATH=/home/admin/backend"
set "JAR_NAME=yg-admin.jar"

echo 开始构建项目...

:: 清理并打包项目
call mvn clean
if %ERRORLEVEL% neq 0 (
  echo Maven清理失败!
  exit /b 1
)

call mvn package
if %ERRORLEVEL% neq 0 (
  echo Maven构建失败!部署终止。
  exit /b 1
)

echo 构建成功,准备部署到服务器...

:: 使用PuTTY的pscp和plink工具进行SSH操作
echo 删除服务器上的旧JAR包...
plink %SERVER_ALIAS% "cd %SERVER_PATH% && rm -f %JAR_NAME%"

:: 上传新的JAR包
echo 上传新的JAR包到服务器...
cd yg-admin\target
pscp %JAR_NAME% %SERVER_ALIAS%:%SERVER_PATH%/

if %ERRORLEVEL% neq 0 (
  echo JAR包上传失败!部署终止。
  exit /b 1
)

:: 重启应用
echo 重启应用...
plink %SERVER_ALIAS% "cd %SERVER_PATH% && sh start.sh restart"

if %ERRORLEVEL% neq 0 (
  echo 应用重启失败!请检查服务器日志。
  exit /b 1
) else (
  echo 部署完成!应用已成功重启。
)

:: 显示应用状态
echo 获取应用状态...
plink %SERVER_ALIAS% "cd %SERVER_PATH% && sh start.sh status"

echo 部署流程全部完成!

备注: 批处理脚本需要安装 PuTTY 工具集(包含 plink 和 pscp)并配置好 SSH 连接。

生产环境还需要考虑什么?

上面的脚本覆盖了基本的部署场景,但在真正的生产环境中,你可能还需要处理以下问题。

1. 端口占用问题

偶尔会遇到旧进程没有完全退出,端口还被占着的情况。加一个端口检查函数能提前发现问题:

check_port() {
    PORT=$1
    if netstat -tuln | grep -q ":$PORT "; then
        echo "端口 $PORT 已被占用!"
        return 1
    fi
    return 0
}

# 使用示例
if ! check_port 8080; then
    echo "尝试使用备用端口 8081..."
    PORT=8081
    if ! check_port $PORT; then
        echo "备用端口也被占用,启动失败!"
        exit 1
    fi
fi

2. JVM 内存设置

默认的 JVM 参数在生产中几乎肯定不够用。根据服务器资源和应用特性,合理配置内存参数:

# JVM内存设置示例
JVM_OPTS="-Xms512m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m"

3. 进程管理

脚本方案适合中小项目。如果应用需要开机自启、崩溃自动重启等能力,建议引入专业的进程管理工具:

# Supervisor配置示例 (/etc/supervisor/conf.d/myapp.conf)
[program:myapp]
command=java -jar /path/to/app.jar
directory=/path/to
user=appuser
autostart=true
autorestart=true
startsecs=10
startretries=3
stdout_logfile=/var/log/myapp/stdout.log
stderr_logfile=/var/log/myapp/stderr.log

4. 备份与回滚

没有回滚能力的部署流程是不完整的。万一新版本有 Bug,你需要能迅速切回上一个版本:

# 备份当前版本
backup() {
    echo "备份当前版本..."
    BACKUP_DIR="backups"
    mkdir -p $BACKUP_DIR

    if [ -f "$JAR_FILE" ]; then
        TIMESTAMP=$(date +"%Y%m%d%H%M%S")
        cp "$JAR_FILE" "$BACKUP_DIR/$APP_NAME-$TIMESTAMP.jar"
        echo "备份完成: $BACKUP_DIR/$APP_NAME-$TIMESTAMP.jar"
        # 记录当前备份版本
        echo "$BACKUP_DIR/$APP_NAME-$TIMESTAMP.jar" > "$BACKUP_DIR/latest_backup.txt"
    else
        echo "没有找到当前版本,跳过备份"
    fi
}

# 回滚到上一版本
rollback() {
    echo "正在回滚到上一版本..."
    BACKUP_DIR="backups"
    LATEST_BACKUP_FILE=$(cat "$BACKUP_DIR/latest_backup.txt" 2>/dev/null)

    if [ -f "$LATEST_BACKUP_FILE" ]; then
        echo "找到备份版本: $LATEST_BACKUP_FILE"
        cp "$LATEST_BACKUP_FILE" "$JAR_FILE"
        echo "回滚完成,准备重启应用..."
        restart
    else
        echo "没有找到可用的备份版本,无法回滚!"
        return 1
    fi
}

写在最后

重要: 有些脚本未经验证,请自行修改!

这些脚本的核心价值不在于脚本本身,而在于”把可重复的操作自动化”这个思路。在实际使用前,有几点需要注意:

  1. 根据你的实际环境修改脚本中的路径、应用名称和服务器配置
  2. 确保服务器上已安装所需的依赖(如 Java、Maven)
  3. 在生产环境使用前,先在测试环境充分验证
  4. 针对不同操作系统可能需要调整部分命令(如 Linux 与 macOS 的差异)
  5. 对于敏感操作(如停止服务、删除文件),考虑添加确认步骤
  6. 添加日志记录功能,便于追踪部署过程中的问题
  7. 根据实际需求调整 JVM 参数和内存设置
  8. 使用前请确认脚本具有执行权限(chmod +x *.sh

当然,如果项目规模进一步扩大,你可能需要考虑 Jenkins、GitLab CI/CD 或 GitHub Actions 这样的 CI/CD 工具。但对于很多中小团队来说,一组维护良好的 Shell 脚本已经足够解决问题了——简单、透明、可控。