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 | 几乎不用改脚本 | 个人开发,追求最小成本 |
| PowerShell | Windows 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 中追加 backup 和 rollback 命令:
备份与回滚函数
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] 部分脚本未经完整验证,请根据实际环境修改后再使用。
这些脚本的核心价值不在于脚本本身,而在于”把可重复的操作自动化”这个思路。使用前的几点建议:
- 根据实际环境修改脚本中的路径、应用名称和服务器配置
- 在生产环境使用前,先在测试环境充分验证
- 确保脚本具有执行权限(
chmod +x *.sh)
当然,如果项目规模进一步扩大,你可能需要考虑 Jenkins、GitLab CI/CD 或 GitHub Actions 这样的 CI/CD 工具。但对于很多中小团队来说,一组维护良好的 Shell 脚本已经足够解决问题了——简单、透明、可控。
Footnotes
-
SSH 免密登录基于非对称加密:本地生成公私钥对(
ssh-keygen),将公钥添加到服务器的~/.ssh/authorized_keys。连接时客户端用私钥签名,服务器用公钥验证,从而免去密码输入。详见 OpenSSH 官方文档。 ↩