文章目录

  • 1. 用法
  • 2. 说明
  • 3. 源码(run.sh)


一个用于运行 Java SpringBoot 的 Linux 脚本 run.sh

  1. 自动根据所在目录获取最新的可执行 jar、war
  2. 自动使用环境变量 JAVA_HOME 进行执行命令(可配置)
  3. 支持优雅下线(默认等待60秒,可配置,如果超过60秒则强制下线)
  4. 支持监听 SpringBoot 上线状态
  5. 支持添加自定义运行参数(如:JVM 参数、SpringBoot 参数)
  6. 支持常用功能:启动、停止、重启、查看状态、查看日志
  7. SpringBoot 项目支持查看应用端口信息

目录结构如下

├── fastboot-0.0.1.jar                      # 应用jar
├── logs
│   ├── console.log                         # 应用控制台输入的日志
│   └── server.pid                          # 应用的pid
└── run.sh                                  # 执行脚本

可配置的参数

java 执行原生es命令 performRequest_JAVA

1. 用法

添加文件的执行权限

# 首次执行第一次即可
chmod +x run.sh

java 执行原生es命令 performRequest_linux_02

重启并清空日志

java 执行原生es命令 performRequest_应用程序_03

查看状态

java 执行原生es命令 performRequest_java_04

启动

java 执行原生es命令 performRequest_linux_05

停止

java 执行原生es命令 performRequest_linux_06

2. 说明

# 赋予执行权限(初始化执行一次)
chmod +x run.sh 
# 启动应用程序
sh run.sh start
# 停止应用程序
sh run.sh stop
# 查看应用程序
# sh run.sh status
# 重启应用程序
sh run.sh restart c  # 加 c 删除历史日志文件
# 查看应用日志
sh run.sh logs

3. 源码(run.sh)

#!/bin/bash
#
# ====================================================== 使用说明 ======================================================
# 1. 赋予执行权限: chmod +x run.sh
# 2. 启动应用程序: sh run.sh start
# 3. 停止应用程序: sh run.sh stop
# 4. 查看应用程序: sh run.sh status
# 5. 重启应用程序: sh run.sh restart -c -d --ignore-console # -c 清空日志目录,-d 以后台进程方式启动,--ignore-console 忽略控制台输出
# 6. 查看应用日志: sh run.sh logs
# ====================================================== 作者信息 ======================================================
# @author: houyu
# ====================================================== 初始准备 =====================================================
# 刷新环境变量
export PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin:/root/bin
source /etc/profile
# ====================================================== 参数设置 ======================================================
# === JAVA_HOME 路径(默认使用环境变量的 JAVA_HOME)
#JAVA_HOME_PATH="/usr/local/java/jdk1.8.0_281"
JAVA_HOME_PATH="${JAVA_HOME}"
# ---------------------------------------------------------------
# === 目录路径(默认使用脚本文件所在的目录路径)
DIR_PATH=$(cd "$(dirname "$0")" && pwd)
# ---------------------------------------------------------------
# === 服务文件名(默认使用目录下最新的.jar或者.war文件)
SERVER_FILE_NAME=$(ls -t "${DIR_PATH}" | grep -E '\.jar$|\.war$' | grep -v "_execution." | head -1)
# ---------------------------------------------------------------
# === Java 参数
JAVA_OPT="${JAVA_OPT} -Duser.timezone=Asia/Shanghai -Dfile.encoding=UTF-8"
JAVA_OPT="${JAVA_OPT} -Xms128m -Xmx128m"
JAVA_OPT="${JAVA_OPT} -XX:-OmitStackTraceInFastThrow -XX:+HeapDumpOnOutOfMemoryError"
JAVA_OPT="${JAVA_OPT} -XX:-UseLargePages -XX:HeapDumpPath=${DIR_PATH}/logs/heap_dump.hprof"
JAVA_OPT="${JAVA_OPT} -Xloggc:${DIR_PATH}/logs/gc.log -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCDateStamps"
JAVA_OPT="${JAVA_OPT} -XX:+PrintGCTimeStamps -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=100M"
# ---------------------------------------------------------------
# === Spring 参数
SPRING_OPT="${SPRING_OPT} --spring.profiles.active=prod"
# ---------------------------------------------------------------
# === 停止时等待超时的秒数
STOP_TIMEOUT=60
# ====================================================== 方法定义 ======================================================
# 控制台文件
CONSOLE_FILE="${DIR_PATH}/logs/console.log"
# 服务PID
SERVER_PID=0
# 服务端口
SERVER_PORT=0
# 输入的所有参数
INPUT_PARAMETER=$*
# =======================================
# 解析服务的 pid
# =======================================
resolve_server_pid() {
    SERVER_PID=0
    if [ -f "${DIR_PATH}/logs/server.pid" ]; then
        # pid 文件存在则读取文件获取pid
        SERVER_PID=$(cat "${DIR_PATH}/logs/server.pid")
        if [[ $(ps -ef | awk '{print $2}' | grep -w -c "${SERVER_PID}") -eq 0 ]]; then
            SERVER_PID=0
            rm -rf "${DIR_PATH}/logs/server.pid"
            sleep 0.1
        fi
    #else
    #    # pid 文件不存在则尝试根据路径下的应用名称获取pid
    #    if [[ $(pgrep -f -c "java.*${DIR_PATH}/${SERVER_FILE_NAME}") -gt 0 ]]; then
    #        SERVER_PID=$(pgrep -f "java.*${DIR_PATH}/${SERVER_FILE_NAME}" | head -1)
    #    fi
    fi
}
# =======================================
# 解析服务的 port
# =======================================
retry_resolve_server_port() {
    SERVER_PORT=0
    # 检测5分钟(300秒)
    for(( i=0; i<=300; i++ )); do
        # 判断 pid 是否存在
        resolve_server_pid
        if test "${SERVER_PID}" -eq 0; then
            break
        fi
        # 判断 pid 的 port 是否绑定
        if test "$(netstat -tlnp | grep -c -E "\s+${SERVER_PID}/")" -gt 0; then
            SERVER_PORT=$(netstat -tlnp | grep -E "\s+${SERVER_PID}/" | head -1 | awk '{print $4}' | awk -F ":" '{print $NF}')
            break
        fi
        [[ $i%5 -eq 0 ]] && echo "--- Observing server port using ${i}s"
        sleep 1
    done
}
# =======================================
# 启动
# =======================================
start() {
    echo "----------------------------------------------------------------------------- Start  [0] ------"
    # 检查服务状态
    resolve_server_pid
    if [[ ${SERVER_PID} -ne 0 ]]; then
        echo "--- Do not start. ${SERVER_FILE_NAME} already started! [ SERVER_PID = ${SERVER_PID} ]"
        echo "----------------------------------------------------------------------------- Start  [1] ------"
        return
    fi
    # 检查 SERVER_FILE_NAME 是否存在
    if [ ! -e "${DIR_PATH}/${SERVER_FILE_NAME}" ]; then
        echo -e "\033[31mPlease set a valid variable SERVER_FILE_NAME!\033[0m"
        exit 1
    fi
    # 检查 SERVER_FILE_NAME 是否需要复制
    if [[ $(echo "${SERVER_FILE_NAME}" | grep -c "_execution.") -eq 0 ]]; then
        _EXECUTION_SERVER_FILE_NAME=$(echo "${SERVER_FILE_NAME}" | sed 's/\(.*\)\.\(.*\)/\1_execution.\2/g')
        # 强制复制一份文件用于运行
        echo -e "--- \033[33mForce copy file to running, execution file is ${_EXECUTION_SERVER_FILE_NAME}\033[0m"
        \cp "${DIR_PATH}/${SERVER_FILE_NAME}" "${DIR_PATH}/${_EXECUTION_SERVER_FILE_NAME}"
        SERVER_FILE_NAME="${_EXECUTION_SERVER_FILE_NAME}"
    fi
    # 检查 JAVA_HOME_PATH
    if [ ! -d "${JAVA_HOME_PATH}" ]; then
        echo -e "\033[31mPlease set a valid variable JAVA_HOME_PATH!\033[0m"
        exit 1
    fi
    # 检查日志目录
    if [[ $(echo "$INPUT_PARAMETER" | grep -c -E "\s+-[A-Za-z]*c") -gt 0 ]]; then
        # 存在 -c clear 则删除日志目录
        echo -e "--- \033[33mDeleting ${SERVER_FILE_NAME} file directory(${DIR_PATH}/logs)\033[0m"
        rm -rf "${DIR_PATH}/logs"
    fi
    test -d "${DIR_PATH}/logs" || mkdir -p "${DIR_PATH}/logs"
    #
    echo "--- Starting ${SERVER_FILE_NAME} ..."
    # 执行脚本
    # nohup java -Xmx128m -jar /app/server.jar --spring.profiles.active=prod >> /app/logs/console.log 2>&1 & echo $! > /app/logs/server.pid
    # 封装运行参数
    _RUN_OPT="${JAVA_HOME_PATH}/bin/java ${JAVA_OPT} -jar ${DIR_PATH}/${SERVER_FILE_NAME}"
    [ -n "${SPRING_OPT}" ] && _RUN_OPT="${_RUN_OPT} ${SPRING_OPT}"
    echo "--- ${_RUN_OPT}"
    echo "${_RUN_OPT}" > "${CONSOLE_FILE}"
    # 处理服务控制台文件
    _SERVER_CONSOLE_FILE="${CONSOLE_FILE}"
    if [[ $(echo "$INPUT_PARAMETER" | grep -c -E "\s+--ignore-console") -gt 0 ]]; then
        # 存在 -ignore-console 则忽略服务输出的日志
        echo "--ignore-console" >> "${CONSOLE_FILE}"
        _SERVER_CONSOLE_FILE="/dev/null"
    fi
    #
    _PREV_PATH=$(pwd)
    cd "${DIR_PATH}"
    nohup ${_RUN_OPT} >> "${_SERVER_CONSOLE_FILE}" 2>&1 & echo $! > "${DIR_PATH}/logs/server.pid"
    cd "${_PREV_PATH}"
    sleep 1
    #
    echo "--- Running ${SERVER_FILE_NAME} ..."
    echo -e "--- You can check the log file \033[36m${CONSOLE_FILE}\033[0m or execute the command on the next line"
    echo -e "--- \033[40;37mtail -f -n 200 ${CONSOLE_FILE}\033[0m"
    # 判断运行是否成功
    resolve_server_pid
    if [[ ${SERVER_PID} -eq 0 ]]; then
        echo -e "--- \033[31mStart failure ${SERVER_FILE_NAME}\033[0m"
    else
        if [[ $(echo "$INPUT_PARAMETER" | grep -c -E "\s+-[A-Za-z]*d") -gt 0 ]]; then
            # 存在 -d Daemon 以后台进程方式启动
            echo -e "--- \033[32mStart successfully ${SERVER_FILE_NAME} [ SERVER_PID = ${SERVER_PID}, \033[0m\033[33mSERVER_PORT = unknown \033[0m\033[32m]\033[0m"
        else
            retry_resolve_server_port
            if [[ ${SERVER_PORT} -eq 0 ]]; then
                echo -e "--- \033[31mStart failure ${SERVER_FILE_NAME}\033[0m"
            else
                _SERVER_PORT_TEXT=$(netstat -tulnp | grep -E "\s+${SERVER_PID}/" | awk 'BEGIN{ORS=" "}{print $1"-"$4}')
                echo -e "--- \033[32mStart successfully ${SERVER_FILE_NAME} [ SERVER_PID = ${SERVER_PID}, SERVER_PORT = ${_SERVER_PORT_TEXT}]\033[0m"
            fi
        fi
    fi
    echo "----------------------------------------------------------------------------- Start  [1] ------"
}
# =======================================
# 停止
# =======================================
stop() {
    echo "----------------------------------------------------------------------------- Stop   [0] ------"
    #
    resolve_server_pid
    if [[ ${SERVER_PID} -eq 0 ]]; then
        echo "--- Stopped ${SERVER_FILE_NAME}"
        echo "----------------------------------------------------------------------------- Stop   [1] ------"
        return
    fi
    echo "--- Graceful Stopping ${SERVER_FILE_NAME} ..."
    # 进行优雅停止
    kill -15 ${SERVER_PID}
    _WAIT_SECONDS=0
    while true; do
        resolve_server_pid
        if [[ ${SERVER_PID} -eq 0 ]]; then
            echo "--- Stopped ${SERVER_FILE_NAME}"
            break
        fi
        if [[ ${_WAIT_SECONDS} -ge ${STOP_TIMEOUT} ]]; then
            # 强制终止进程
            echo -e "--- \033[31mWait timeout(${_WAIT_SECONDS}s), forced shutdown [kill -9 ${SERVER_FILE_NAME}].\033[0m"
            sudo kill -9 ${SERVER_PID}
            break
        fi
        sleep 1
        ((_WAIT_SECONDS++))
        echo "--- Wait ${_WAIT_SECONDS}s"
    done
    if [[ $? -eq 0 ]]; then
        echo "--- Stop successfully ${SERVER_FILE_NAME}"
        rm -rf "${DIR_PATH}/logs/server.pid"
    else
        echo "--- Stop failure ${SERVER_FILE_NAME}"
    fi
    echo "----------------------------------------------------------------------------- Stop   [1] ------"
}
# =======================================
# 状态
# =======================================
status() {
    echo "----------------------------------------------------------------------------- status [0] ------"
    #
    resolve_server_pid
    if [[ ${SERVER_PID} -eq 0 ]]; then
        echo -e "--- \033[33mStopped $SERVER_FILE_NAME\033[0m"
    else
        _SERVER_PORT_TEXT=$(netstat -tulnp | grep -E "\s+${SERVER_PID}/" | awk 'BEGIN{ORS=" "}{print $1"-"$4}')
        [ -z "$_SERVER_PORT_TEXT" ] && _SERVER_PORT_TEXT="unknown "
        echo -e "--- \033[32mRunning ${SERVER_FILE_NAME} [ SERVER_PID = ${SERVER_PID}, SERVER_PORT = ${_SERVER_PORT_TEXT}]\033[0m"
        echo -e "--- You can check the log file \033[36m${CONSOLE_FILE}\033[0m or execute the command on the next line"
        echo -e "--- \033[40;37mtail -f -n 200 ${CONSOLE_FILE}\033[0m"
    fi
    echo "----------------------------------------------------------------------------- status [1] ------"
}
# =======================================
# 查看日志
# =======================================
logs() {
    tail -f -n 200 "${CONSOLE_FILE}"
}
# =======================================
# 切割日志
# =======================================
split_logs() {
    echo "----------------------------------------------------------------------------- split  [0] ------"
    #
    if [[ $(find "${CONSOLE_FILE}" -size +20M | wc -l) -gt 0 ]]; then
        # 大于20M
        _DATE_TIME=$(date +%Y%m%d_%H%M%S)
        # split -b 10m -d large_file.log new_file_prefix
        # -b 以字节分割; -d 序号用数字
        split -b 20m -d "${CONSOLE_FILE}" "${CONSOLE_FILE}_${_DATE_TIME}_"
        # 清空文件
        cat /dev/null > "${CONSOLE_FILE}"
        # 删除多余的日志, 只保留最后的20份
        find "${DIR_PATH}/logs" -name "$(basename "${CONSOLE_FILE}")_*" | sort | head -n -20 | xargs -i rm -rf {}
        #
        echo "--- Split finished ${SERVER_FILE_NAME} ${CONSOLE_FILE}"
    else
        echo "--- Do not split ${SERVER_FILE_NAME} ${CONSOLE_FILE}"
    fi
    echo "----------------------------------------------------------------------------- split  [1] ------"
}
# ====================================================== 入口分支 ======================================================
#
case "$1" in
'start')
    start
    ;;
'stop')
    stop
    ;;
'restart')
    stop
    start
    ;;
'status')
    status
    ;;
'logs')
    logs
    ;;
'split')
    split_logs
    ;;
*)
    echo "requires parameter [start|stop|restart|status|logs|split] [-c] [-d] [--ignore-console]"
    exit 1
    ;;
esac
#
#