#!/usr/bin/env bash ############################################################################### # Spring Boot 应用部署脚本 # # 主要功能: # 1. 初始化部署变量(JDK 路径、运行目录、日志目录、外置配置目录、JVM 参数等) # 2. 自动创建部署相关目录(如不存在) # 3. 基于 Jar 名称进行进程检查(支持指定 Jar 或自动发现 Jar) # 4. 停止同名应用进程(优先 kill -15,超时后 kill -9) # 5. 使用 nohup 启动应用并进行启动结果校验 # # 使用说明: # 1. 首次使用请根据实际环境修改“变量初始化区” # 2. 脚本支持通过命令行覆盖部分变量,示例: # ./deploy.sh --jar-name app.jar --profile prod --debug true # 3. 日志输出为中文,便于定位部署阶段和问题 ############################################################################### set -u ############################################ # 一、变量初始化区(请按需修改) ############################################ # JDK 安装目录(必须能找到 bin/java) JDK_HOME="/usr/local/jdk" # Spring Boot Jar 运行目录(放置 jar 包的位置) APP_HOME="/opt/apps/springboot" # 应用日志输出目录(脚本会自动创建) LOG_DIR="/opt/logs/springboot" # 外置 Spring Boot 配置文件目录(脚本会自动创建) # 建议在该目录中放置 application-.yml 或 application.yml CONFIG_DIR="/opt/config/springboot" # 指定 Jar 包名称(例如 demo.jar) # 若留空则自动从 APP_HOME 中寻找第一个 *.jar 作为候选 JAR_NAME="" # JVM 参数(可按机器资源调整) JVM_OPTS="-Xms512m -Xmx1024m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${LOG_DIR}/heapdump.hprof -Dfile.encoding=UTF-8" # Spring Boot Profile(dev/test/prod 等) SPRING_PROFILE="prod" # Java 启动附加参数(例如时区等系统属性) JAVA_SYS_PROPS="-Duser.timezone=Asia/Shanghai" # Debug 开关:true/false DEBUG_ENABLED="false" # Debug 端口和监听地址(仅在 DEBUG_ENABLED=true 时生效) DEBUG_PORT="5005" DEBUG_SUSPEND="n" # y: 启动时等待调试器连接;n: 不等待 DEBUG_ADDRESS="0.0.0.0" # 停止进程等待超时时间(秒) STOP_TIMEOUT="30" # 启动后健康检查等待时间(秒) START_WAIT="8" # 启动日志文件 CONSOLE_LOG_FILE="${LOG_DIR}/console.out" # 运行用户(用于日志提示,不强制切换用户) RUN_USER="$(whoami)" # 额外 Spring 参数(可按需增加,例如 --server.port=8080) SPRING_EXTRA_ARGS="" ############################################ # 二、内部变量与通用函数 ############################################ SCRIPT_NAME="$(basename "$0")" CURRENT_TIME="$(date '+%Y-%m-%d %H:%M:%S')" # 彩色输出(若终端不支持可改为空) COLOR_RESET="\033[0m" COLOR_INFO="\033[1;34m" COLOR_WARN="\033[1;33m" COLOR_ERROR="\033[1;31m" COLOR_SUCCESS="\033[1;32m" log_info() { echo -e "${COLOR_INFO}[信息][$(date '+%Y-%m-%d %H:%M:%S')]${COLOR_RESET} $*" } log_warn() { echo -e "${COLOR_WARN}[警告][$(date '+%Y-%m-%d %H:%M:%S')]${COLOR_RESET} $*" } log_error() { echo -e "${COLOR_ERROR}[错误][$(date '+%Y-%m-%d %H:%M:%S')]${COLOR_RESET} $*" } log_success() { echo -e "${COLOR_SUCCESS}[成功][$(date '+%Y-%m-%d %H:%M:%S')]${COLOR_RESET} $*" } print_header() { echo "============================================================" echo "脚本名称 : ${SCRIPT_NAME}" echo "执行时间 : ${CURRENT_TIME}" echo "执行用户 : ${RUN_USER}" echo "============================================================" } usage() { cat < 指定 JDK_HOME --app-home 指定 APP_HOME --log-dir 指定 LOG_DIR --config-dir 指定 CONFIG_DIR --jar-name 指定 Jar 包名称(如 demo.jar) --profile 指定 Spring Profile(如 dev/test/prod) --debug 是否开启远程调试 --debug-port 调试端口(默认 5005) --stop-timeout 停止超时时间(秒) --start-wait 启动后等待检查时间(秒) --help 显示帮助 示例: ${SCRIPT_NAME} --jar-name demo.jar --profile prod --debug false EOF } # 参数解析:允许在不改脚本文件的情况下按需覆盖变量 parse_args() { while [[ $# -gt 0 ]]; do case "$1" in --jdk-home) JDK_HOME="$2"; shift 2 ;; --app-home) APP_HOME="$2"; shift 2 ;; --log-dir) LOG_DIR="$2"; shift 2 ;; --config-dir) CONFIG_DIR="$2"; shift 2 ;; --jar-name) JAR_NAME="$2"; shift 2 ;; --profile) SPRING_PROFILE="$2"; shift 2 ;; --debug) DEBUG_ENABLED="$2"; shift 2 ;; --debug-port) DEBUG_PORT="$2"; shift 2 ;; --stop-timeout) STOP_TIMEOUT="$2"; shift 2 ;; --start-wait) START_WAIT="$2"; shift 2 ;; --help) usage; exit 0 ;; *) log_error "未知参数:$1" usage exit 1 ;; esac done } # 基础前置校验,避免关键路径或命令不存在导致后续异常 pre_check() { log_info "开始执行部署前检查..." if [[ ! -d "${JDK_HOME}" ]]; then log_error "JDK_HOME 不存在:${JDK_HOME}" exit 1 fi if [[ ! -x "${JDK_HOME}/bin/java" ]]; then log_error "未找到可执行 Java:${JDK_HOME}/bin/java" exit 1 fi # APP_HOME 不强制提前存在,后续会自动创建 if ! command -v nohup >/dev/null 2>&1; then log_error "系统未找到 nohup 命令,请先安装 coreutils 或确认环境。" exit 1 fi log_success "部署前检查通过。" } # 创建目录:若不存在则创建,存在则提示复用 ensure_dirs() { log_info "开始检查并创建部署目录..." for d in "${APP_HOME}" "${LOG_DIR}" "${CONFIG_DIR}"; do if [[ ! -d "$d" ]]; then mkdir -p "$d" log_info "目录不存在,已创建:$d" else log_info "目录已存在,跳过创建:$d" fi done log_success "目录检查/创建完成。" } # 自动发现 Jar:当未手工指定 JAR_NAME 时,从 APP_HOME 取第一个 jar auto_select_jar() { if [[ -n "${JAR_NAME}" ]]; then log_info "已指定 Jar 包名称:${JAR_NAME}" return 0 fi log_info "未指定 Jar 包名称,开始从运行目录自动发现:${APP_HOME}" # 使用 find 获取第一个 jar,排序后取第一项可提升稳定性 local found found="$(find "${APP_HOME}" -maxdepth 1 -type f -name '*.jar' | sort | head -n 1)" if [[ -z "${found}" ]]; then log_error "在 ${APP_HOME} 未发现任何 jar 包,请先上传应用 Jar。" exit 1 fi JAR_NAME="$(basename "${found}")" log_success "自动选定 Jar 包:${JAR_NAME}" } # 返回匹配 Jar 名称的进程 PID(可能多个) get_pids_by_jar() { local jar="$1" # 过滤掉 grep 本身,匹配 java 命令行中包含目标 jar 的进程 ps -ef | grep java | grep "${jar}" | grep -v grep | awk '{print $2}' } # 优雅停止应用:优先 kill -15,超时后 kill -9 stop_app_if_running() { log_info "开始检查同名进程(基于 Jar 名称):${JAR_NAME}" local pids pids="$(get_pids_by_jar "${JAR_NAME}" || true)" if [[ -z "${pids}" ]]; then log_info "未发现运行中的同名进程,无需停止。" return 0 fi log_warn "发现运行中进程 PID:${pids}" log_info "优先执行 kill -15 进行优雅停止..." for pid in ${pids}; do kill -15 "${pid}" 2>/dev/null || true done local waited=0 while [[ ${waited} -lt ${STOP_TIMEOUT} ]]; do sleep 1 waited=$((waited + 1)) local remaining remaining="$(get_pids_by_jar "${JAR_NAME}" || true)" if [[ -z "${remaining}" ]]; then log_success "应用已在 ${waited} 秒内优雅停止。" return 0 fi done log_warn "等待 ${STOP_TIMEOUT} 秒后进程仍存在,执行 kill -9 强制停止..." local force_pids force_pids="$(get_pids_by_jar "${JAR_NAME}" || true)" for pid in ${force_pids}; do kill -9 "${pid}" 2>/dev/null || true done sleep 1 local final_check final_check="$(get_pids_by_jar "${JAR_NAME}" || true)" if [[ -n "${final_check}" ]]; then log_error "强制停止后仍检测到进程:${final_check},请人工排查。" exit 1 fi log_success "应用已强制停止。" } # 组装 debug 参数 build_debug_opts() { if [[ "${DEBUG_ENABLED}" == "true" ]]; then echo "-agentlib:jdwp=transport=dt_socket,server=y,suspend=${DEBUG_SUSPEND},address=${DEBUG_ADDRESS}:${DEBUG_PORT}" else echo "" fi } # 启动应用并校验进程是否存在 start_app() { local jar_path="${APP_HOME}/${JAR_NAME}" if [[ ! -f "${jar_path}" ]]; then log_error "目标 Jar 不存在:${jar_path}" exit 1 fi local debug_opts debug_opts="$(build_debug_opts)" log_info "开始启动应用..." log_info "JDK 目录:${JDK_HOME}" log_info "运行目录:${APP_HOME}" log_info "日志目录:${LOG_DIR}" log_info "配置目录:${CONFIG_DIR}" log_info "Spring Profile:${SPRING_PROFILE}" log_info "Debug 开关:${DEBUG_ENABLED}" # 启动命令说明: # 1. 使用 nohup 后台启动,避免会话退出导致进程结束 # 2. --spring.config.location 指向外置配置目录 # 3. 标准输出与错误输出统一重定向到 console.out nohup "${JDK_HOME}/bin/java" \ ${JVM_OPTS} \ ${JAVA_SYS_PROPS} \ ${debug_opts} \ -jar "${jar_path}" \ --spring.profiles.active="${SPRING_PROFILE}" \ --spring.config.location="${CONFIG_DIR}/" \ ${SPRING_EXTRA_ARGS} \ >> "${CONSOLE_LOG_FILE}" 2>&1 & local new_pid=$! log_info "启动命令已提交,后台进程 PID(提交时): ${new_pid}" log_info "等待 ${START_WAIT} 秒后进行启动结果检查..." sleep "${START_WAIT}" local started_pids started_pids="$(get_pids_by_jar "${JAR_NAME}" || true)" if [[ -n "${started_pids}" ]]; then log_success "应用启动成功,当前 PID:${started_pids}" log_info "启动日志文件:${CONSOLE_LOG_FILE}" else log_error "应用启动失败,未检测到进程。请检查日志:${CONSOLE_LOG_FILE}" exit 1 fi } # 打印当前配置,便于部署时核对 print_config() { echo "---------------- 当前部署配置 ----------------" echo "JDK_HOME = ${JDK_HOME}" echo "APP_HOME = ${APP_HOME}" echo "LOG_DIR = ${LOG_DIR}" echo "CONFIG_DIR = ${CONFIG_DIR}" echo "JAR_NAME = ${JAR_NAME:-<自动发现>}" echo "SPRING_PROFILE = ${SPRING_PROFILE}" echo "DEBUG_ENABLED = ${DEBUG_ENABLED}" echo "DEBUG_PORT = ${DEBUG_PORT}" echo "STOP_TIMEOUT(s) = ${STOP_TIMEOUT}" echo "START_WAIT(s) = ${START_WAIT}" echo "CONSOLE_LOG_FILE = ${CONSOLE_LOG_FILE}" echo "------------------------------------------------" } main() { print_header # 1) 解析参数 parse_args "$@" # 2) 打印配置 print_config # 3) 前置检查 pre_check # 4) 创建目录 ensure_dirs # 5) 选择 Jar 包(指定或自动发现) auto_select_jar # 6) 每次启动前都进行同名进程检查并执行停止 stop_app_if_running # 7) 启动应用并检查结果 start_app log_success "部署流程执行完成。" } main "$@"