返回

SpringBoot 应用自动部署脚本

两个脚本搞定 SpringBoot 应用的构建、上传和部署,告别手动操作。

graph LR
    A[本地开发] -->|mvn package| B[构建 JAR]
    B -->|scp 上传| C[服务器]
    C -->|start.sh stop| D[停止旧服务]
    D -->|start.sh start| E[启动新服务]
    style A fill:transparent
    style E fill:transparent

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

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

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

部署前检查清单

在开始之前,确认以下准备工作已经就绪:

  • 本地安装了 JDK 和 Maven
  • 服务器已安装 JDK 运行环境
  • 已配置 SSH 免密登录(~/.ssh/config 中配好服务器别名)1
  • 服务器目标目录已创建且有写入权限
  • 防火墙已开放应用端口(如 8080)
  • 脚本已赋予执行权限(chmod +x *.sh

应用启动脚本 (start.sh)

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

脚本内容

start.sh(点击展开完整脚本)
#!/bin/bash

APP_NAME="yg-admin"
JAR_FILE="yg-admin.jar"
CONFIG_FILE="application-prod.yml"
PROFILE="prod"
JVM_OPTS="-Dfile.encoding=UTF-8"

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

  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=$!
  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=$(get_pid)

  # 优雅停止:先 SIGTERM,超时再 SIGKILL
  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'

SERVER_ALIAS="yg_iot"
SERVER_PATH="/home/admin/backend"
JAR_NAME="yg-admin.jar"

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

mvn clean && mvn package

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

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

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

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 构建 -> 删除旧包 -> 上传新包 -> 重启服务 -> 确认状态。每一步都有错误检查,任何一环失败都会立即终止并给出红色提示,不会带着错误继续往下走。

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

Windows 环境怎么办?

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

脚本选择指南

方案前置条件改动成本推荐场景
Git Bash / WSL安装 Git 或启用 WSL几乎不用改脚本个人开发,追求最小成本
PowerShellWindows 10+ 自带需要重写脚本团队统一用 Windows 原生工具
Batch 脚本安装 PuTTY 工具集需要重写脚本老旧 Windows 环境,无法装其他工具

1. 使用 Git Bash 或 WSL

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

# 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(点击展开完整脚本)
$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

Write-Host "删除服务器上的旧JAR包..." @YELLOW
ssh $SERVER_ALIAS "cd $SERVER_PATH && rm -f $JAR_NAME"

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.bat(点击展开完整脚本)
@echo off
setlocal enabledelayedexpansion

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 构建成功,准备部署到服务器...

echo 删除服务器上的旧JAR包...
plink %SERVER_ALIAS% "cd %SERVER_PATH% && rm -f %JAR_NAME%"

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 部署流程全部完成!

[!NOTE] 批处理脚本需要安装 PuTTY 工具集(包含 plink 和 pscp)并配置好 SSH 连接。Windows 10 及以上版本建议优先使用内置的 OpenSSH 客户端。

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

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

端口占用问题

偶尔会遇到旧进程没有完全退出,端口还被占着的情况。可以在 start.sh 中加一个端口检查函数:

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

JVM 内存设置

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

JVM_OPTS="-Xms512m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m"
参数作用建议值
-Xms初始堆内存服务器内存的 1/4
-Xmx最大堆内存服务器内存的 1/2,且不超过 4G
-XX:MetaspaceSize元空间初始大小128m–256m
-XX:MaxMetaspaceSize元空间上限256m–512m

进程管理

脚本方案适合中小项目。如果应用需要开机自启、崩溃自动重启等能力,建议引入 Supervisor:

Supervisor 配置示例
[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

备份与回滚

没有回滚能力的部署流程是不完整的。万一新版本有 Bug,你需要能迅速切回上一个版本。在 start.sh 中追加 backuprollback 命令:

备份与回滚函数
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
}

常见问题排查

[!WARNING] 部署脚本出错时,先看错误信息再动手。以下是高频问题。

Maven 构建失败:检查 JAVA_HOME 是否指向正确的 JDK 版本,以及本地 Maven 仓库是否完整。运行 mvn clean package -U 强制更新依赖试试。

SCP 上传报 “Permission denied”:确认 SSH 免密登录是否配好。手动 ssh yg_iot 能不能无密码连上?如果不行,检查 ~/.ssh/config 和公钥是否已添加到服务器的 ~/.ssh/authorized_keys

应用启动后立即退出:多半是配置文件路径错误或端口冲突。用 nohup java -jar xxx.jar > app.log 2>&1 & 临时把日志输出到文件,查看具体报错。

端口冲突 “Address already in use”lsof -i :8080 找到占用端口的进程,确认是旧实例残留还是其他服务占用。如果是旧实例,用 kill -9 <abbr title="Process ID">PID</abbr> 强制终止后重试。

脚本执行报 “Permission denied”:别忘了给脚本加执行权限,在终端中按 Enter 执行 chmod +x start.sh deploy.sh

写在最后

[!NOTE] 部分脚本未经完整验证,请根据实际环境修改后再使用。

这些脚本的核心价值不在于脚本本身,而在于”把可重复的操作自动化”这个思路。使用前的几点建议:

  1. 根据实际环境修改脚本中的路径、应用名称和服务器配置
  2. 在生产环境使用前,先在测试环境充分验证
  3. 确保脚本具有执行权限(chmod +x *.sh

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

Footnotes

  1. SSH 免密登录基于非对称加密:本地生成公私钥对(ssh-keygen),将公钥添加到服务器的 ~/.ssh/authorized_keys。连接时客户端用私钥签名,服务器用公钥验证,从而免去密码输入。详见 OpenSSH 官方文档