從「手動啟動 Jar」到「一鍵部署運維」,一套成熟的 Shell 腳本能解決 90% 的發布痛點。本文提供基於 Spring Boot 的生產級部署腳本三件套:start.sh、stop.sh、dump.sh,並詳細解析關鍵邏輯。
project-root/
├── bin/ # 腳本目錄
│ ├── start.sh
│ ├── stop.sh
│ └── dump.sh
├── lib/ # 依賴 JAR 包
├── conf/ # 配置文件
├── logs/ # 日志輸出(運行時創建)
├── dump/ # 診斷快照(運行時創建)
└── app.jar # 業務主 JAR (Spring Boot Fat Jar)
Maven 打包插件配置:
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<layout>ZIP</layout> <!-- 或 JAR -->
<includes>
<include>
<groupId>nothing</groupId>
<artifactId>scripts</artifactId>
</include>
</includes>
</configuration>
</plugin>
#!/bin/bash
# version: 1.0.0
cd `dirname $0` # 進入腳本所在目錄 (bin/)
BIN_DIR=`pwd`
cd .. # 返回項目根目錄
DEPLOY_DIR=`pwd`
SERVER_NAME=`basename $DEPLOY_DIR` # 以根目錄名作為服務名
CONF_DIR=$DEPLOY_DIR/conf
LIB_DIR=$DEPLOY_DIR/lib
LOGS_DIR=$DEPLOY_DIR/logs
STDOUT_FILE=$LOGS_DIR/stdout.log
# 1. 防重複啟動
PIDS=`ps -ef | grep java | grep "$CONF_DIR" | awk '{print $2}'`
if [ -n "$PIDS" ]; then
echo "ERROR: The $SERVER_NAME already started! PID: $PIDS"
exit 1
fi
# 2. 準備日志目錄
mkdir -p $LOGS_DIR
# 3. 構建 Classpath
LIB_JARS=`ls $LIB_DIR | grep .jar | awk '{print "'$LIB_DIR'/"$0}' | tr "\n" ":"`
MAIN_JAR=`ls $DEPLOY_DIR | grep .jar | awk '{print "'$DEPLOY_DIR'/"$0}' | tr "\n" ":"`
CLASSPATH="$CONF_DIR:$LIB_JARS:$MAIN_JAR"
# 4. JVM 參數(根據架構自動調整)
JAVA_OPTS=" -Djava.net.preferIPv4Stack=true -Dlog.home=$LOGS_DIR"
BITS=`java -version 2>&1 | grep -i 64-bit`
if [ -n "$BITS" ]; then
JAVA_MEM_OPTS=" -server -Xms2g -Xmx2g -XX:PermSize=128m -XX:MaxPermSize=128m \
-XX:SurvivorRatio=6 -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled \
-XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=80 \
-XX:+ScavengeBeforeFullGC -XX:+CMSScavengeBeforeRemark \
-XX:+PrintGCDateStamps -verbose:gc -XX:+PrintGCDetails \
-Xloggc:gc.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 \
-XX:GCLogFileSize=100M -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=dump.hprof"
else
JAVA_MEM_OPTS=" -server -Xms1g -Xmx1g -XX:PermSize=128m -XX:MaxPermSize=128m \
-XX:SurvivorRatio=2 -XX:+UseParallelGC"
fi
# 5. 啟動應用
echo -e "Starting the $SERVER_NAME ...\c"
nohup java $JAVA_OPTS $JAVA_MEM_OPTS -classpath $CLASSPATH \
com.example.Application > $STDOUT_FILE 2>&1 &
# 6. 健康檢查等待
COUNT=0
while [ $COUNT -lt 1 ]; do
echo -e ".\c"
sleep 1
COUNT=`ps -f | grep java | grep "$DEPLOY_DIR" | awk '{print $2}' | wc -l`
if [ $COUNT -gt 0 ]; then break; fi
done
echo "OK!"
PIDS=`ps -f | grep java | grep "$DEPLOY_DIR" | awk '{print $2}'`
echo "PID: $PIDS"
echo "STDOUT: $STDOUT_FILE"
dirname $0 + cd ..:動態定位項目根目錄,支持任意路徑調用grep "$CONF_DIR":精準匹配進程,避免誤殺同機其他 Java 進程conf:lib/*:app.jar,無需修改 Manifestnohup ... &:後台守護進程模式#!/bin/bash
cd `dirname $0`; BIN_DIR=`pwd`; cd ..; DEPLOY_DIR=`pwd`
CONF_DIR=$DEPLOY_DIR/conf
SERVER_NAME=`basename $DEPLOY_DIR`
PIDS=`ps -ef | grep java | grep "$CONF_DIR" | awk '{print $2}'`
if [ -z "$PIDS" ]; then
echo "ERROR: The $SERVER_NAME does not started!"
exit 1
fi
# 支持 dump 參數:停止前導出診斷信息
if [ "$1" == "dump" ]; then
$BIN_DIR/dump.sh
fi
echo -e "Stopping the $SERVER_NAME ...\c"
for PID in $PIDS; do
kill $PID > /dev/null 2>&1 # 先發 SIGTERM (15),給 Spring 關閉鉤子執行時間
done
# 等待進程退出
COUNT=0
while [ $COUNT -lt 1 ]; do
echo -e ".\c"
sleep 1
COUNT=1
for PID in $PIDS; do
PID_EXIST=`ps -f -p $PID | grep java`
if [ -n "$PID_EXIST" ]; then COUNT=0; break; fi
done
done
echo "OK!"
echo "PID: $PIDS"
kill $PID 發送 SIGTERM (15),非 SIGKILL (9),讓 Spring 執行 @PreDestroy、關閉線程池、釋放鎖./stop.sh dump 一鍵「先診斷後停止」#!/bin/bash
cd `dirname $0`; BIN_DIR=`pwd`; cd ..; DEPLOY_DIR=`pwd`
CONF_DIR=$DEPLOY_DIR/conf
SERVER_NAME=`basename $DEPLOY_DIR`
PIDS=`ps -ef | grep java | grep "$CONF_DIR" | awk '{print $2}'`
if [ -z "$PIDS" ]; then
echo "ERROR: The $SERVER_NAME does not started!"
exit 1
fi
LOGS_DIR=$DEPLOY_DIR/logs
DUMP_DIR=$LOGS_DIR/dump
DUMP_DATE=`date +%Y%m%d%H%M%S`
DATE_DIR=$DUMP_DIR/$DUMP_DATE
mkdir -p $DATE_DIR
echo -e "Dumping the $SERVER_NAME ...\c"
for PID in $PIDS; do
jstack $PID > $DATE_DIR/jstack-$PID.dump 2>&1; echo -e ".\c"
jinfo $PID > $DATE_DIR/jinfo-$PID.dump 2>&1; echo -e ".\c"
jstat -gcutil $PID > $DATE_DIR/jstat-gcutil-$PID.dump 2>&1; echo -e ".\c"
jstat -gccapacity $PID > $DATE_DIR/jstat-gccapacity-$PID.dump 2>&1; echo -e ".\c"
jmap $PID > $DATE_DIR/jmap-$PID.dump 2>&1; echo -e ".\c"
jmap -heap $PID > $DATE_DIR/jmap-heap-$PID.dump 2>&1; echo -e ".\c"
jmap -histo $PID > $DATE_DIR/jmap-histo-$PID.dump 2>&1; echo -e ".\c"
if [ -r /usr/sbin/lsof ]; then
/usr/sbin/lsof -p $PID > $DATE_DIR/lsof-$PID.dump; echo -e ".\c"
fi
done
# 系統級快照
for cmd in netstat iostat mpstat vmstat free sar uptime; do
if [ -r /usr/bin/$cmd ] || [ -r /bin/$cmd ]; then
$cmd > $DATE_DIR/$cmd.dump 2>&1; echo -e ".\c"
fi
done
echo "OK!"
echo "DUMP: $DATE_DIR"
| 工具 | 輸出文件 | 診斷價值 |
|---|---|---|
| jstack | jstack-PID.dump | 線程堆棧,定位死鎖、CPU 高、線程阻塞 |
| jinfo | jinfo-PID.dump | JVM 參數、系統屬性、命令行參數 |
| jstat -gcutil | jstat-gcutil-PID.dump | GC 統計:Eden/Survivor/Old 使用率、GC 次數/耗時 |
| jstat -gccapacity | jstat-gccapacity-PID.dump | 各代容量大小、最大值 |
| jmap | jmap-PID.dump | 堆內存映射、類加載統計 |
| jmap -heap | jmap-heap-PID.dump | 堆配置詳情、GC 算法、各區大小 |
| jmap -histo | jmap-histo-PID.dump | 對象統計:實例數、大小、類名(找內存洩漏) |
| lsof | lsof-PID.dump | 進程打開文件、網絡連接 |
| netstat/iostat/... | 系統級.dump | 網絡、磁盤 I/O、CPU、內存、負載 |
src/main/scripts,Maven 打包自動拷貝到 bin/chmod +x bin/*.sh./bin/start.sh | 停止:./bin/stop.sh | 診斷:./bin/dump.sh | 停止前診斷:./bin/stop.sh dumptail -f logs/stdout.log 或 tail -f logs/gc.loglogs/ 和 dump/ 歷史文件這套腳本經過多個生產項目驗證,支撐了從啟動、運行、故障診斷到優雅下線的完整生命週期管理。