#!/bin/bash set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # ==================== 生产配置区:按服务器实际路径修改 ==================== # JDK 安装目录。留空时使用服务器已有 JAVA_HOME;仍为空时使用 PATH 中的 java。 BACKEND_JAVA_HOME="" # 后端 Jar 所在目录。生产目录结构为:启动脚本在外层,Jar 位于 backend/ruoyi-admin.jar。 APP_HOME="${SCRIPT_DIR}/backend" # 后端 Jar 文件名。 JAR_NAME="ruoyi-admin.jar" # Spring Profile。 SPRING_PROFILES_ACTIVE="uat" # JVM 参数。 JAVA_OPTS="-Duser.timezone=Asia/Shanghai -Xms512m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError" # 额外应用启动参数,例如:--server.port=8080 APP_ARGS="" # 停止进程等待秒数。 STOP_WAIT_SECONDS=30 # ==================== 以下为脚本逻辑,一般不需要修改 ==================== if [[ "${APP_HOME}" != /* ]]; then APP_HOME="${SCRIPT_DIR}/${APP_HOME}" fi JAR_PATH="${APP_HOME}/${JAR_NAME}" RELATIVE_JAR_PATH="" if [[ "${APP_HOME}" == "${SCRIPT_DIR}/"* ]]; then RELATIVE_JAR_PATH="${APP_HOME#${SCRIPT_DIR}/}/${JAR_NAME}" fi LOG_DIR="${APP_HOME}/logs" CONSOLE_LOG="${LOG_DIR}/backend-console.log" PID_FILE="${LOG_DIR}/backend-java.pid" APP_MARKER="-Dccdi.backend.prod.home=${APP_HOME}" JAVA_CMD="java" timestamp() { date "+%Y-%m-%d %H:%M:%S" } log_info() { printf '[%s] %s\n' "$(timestamp)" "$1" } log_error() { printf '[%s] %s\n' "$(timestamp)" "$1" >&2 } usage() { cat <<'EOF' 用法: ./start-java-backend-prod.sh [start|stop|restart|status|logs] 默认动作: start 先关闭旧后端进程,再启动生产后端 Jar,启动成功后持续输出控制台日志 常用配置: 配置统一写在脚本顶部“生产配置区”,包括 BACKEND_JAVA_HOME、APP_HOME、SPRING_PROFILES_ACTIVE、JAVA_OPTS。 示例: ./start-java-backend-prod.sh restart EOF } ensure_command() { local command_name="$1" if ! command -v "${command_name}" >/dev/null 2>&1; then log_error "缺少命令: ${command_name}" exit 1 fi } resolve_java_cmd() { local configured_java_home="${BACKEND_JAVA_HOME}" if [[ -z "${configured_java_home}" ]]; then configured_java_home="${JAVA_HOME:-}" fi if [[ -n "${configured_java_home}" ]]; then configured_java_home="${configured_java_home%/}" if [[ ! -x "${configured_java_home}/bin/java" ]]; then log_error "配置的 JAVA_HOME 无效,未找到可执行文件: ${configured_java_home}/bin/java" exit 1 fi export JAVA_HOME="${configured_java_home}" JAVA_CMD="${JAVA_HOME}/bin/java" else ensure_command "java" JAVA_CMD="java" fi log_info "使用 Java 命令: ${JAVA_CMD}" } get_process_table() { local process_table if ! process_table="$(ps -ef 2>/dev/null)"; then log_error "执行 ps -ef 失败,无法扫描旧进程" return 1 fi printf '%s\n' "${process_table}" } is_managed_pid() { local pid="$1" if [[ -z "${pid}" ]] || ! kill -0 "${pid}" 2>/dev/null; then return 1 fi local process_table if ! process_table="$(get_process_table)"; then return 1 fi local line while IFS= read -r line; do set -- ${line} if [[ "${2:-}" == "${pid}" ]] && is_backend_process_line "${line}"; then return 0 fi done <<<"${process_table}" return 1 } is_backend_process_line() { local line="$1" [[ "${line}" != *""* ]] || return 1 [[ "${line}" == *" -jar ${JAR_PATH}"* ]] && return 0 [[ -n "${RELATIVE_JAR_PATH}" && "${line}" == *" -jar ${RELATIVE_JAR_PATH}"* ]] } collect_pids() { local all_pids="" local pid local process_table if ! process_table="$(get_process_table)"; then return 1 fi if [[ -f "${PID_FILE}" ]]; then pid="$(cat "${PID_FILE}" 2>/dev/null || true)" if is_managed_pid "${pid}"; then all_pids="${all_pids} ${pid}" fi fi local line while IFS= read -r line; do set -- ${line} pid="${2:-}" if [[ "${pid}" =~ ^[0-9]+$ ]] && is_backend_process_line "${line}"; then all_pids="${all_pids} ${pid}" fi done <<<"${process_table}" local unique_pids="" for pid in ${all_pids}; do case " ${unique_pids} " in *" ${pid} "*) ;; *) unique_pids="${unique_pids} ${pid}" ;; esac done xargs <<<"${unique_pids}" 2>/dev/null || true } start_backend() { resolve_java_cmd if [[ ! -f "${JAR_PATH}" ]]; then log_error "未找到后端 Jar: ${JAR_PATH}" exit 1 fi local running_pids running_pids="$(collect_pids)" if [[ -n "${running_pids}" ]]; then log_error "检测到后端已在运行: ${running_pids}" exit 1 fi mkdir -p "${LOG_DIR}" printf '\n===== %s start =====\n' "$(timestamp)" >>"${CONSOLE_LOG}" local profile_arg="" if [[ -n "${SPRING_PROFILES_ACTIVE}" ]]; then profile_arg="--spring.profiles.active=${SPRING_PROFILES_ACTIVE}" fi log_info "开始启动后端 Jar: ${JAR_PATH}" nohup "${JAVA_CMD}" "${APP_MARKER}" ${JAVA_OPTS} -jar "${JAR_PATH}" ${profile_arg} ${APP_ARGS} >>"${CONSOLE_LOG}" 2>&1 & echo $! >"${PID_FILE}" sleep 3 local starter_pid starter_pid="$(cat "${PID_FILE}" 2>/dev/null || true)" if [[ -z "${starter_pid}" ]] || ! kill -0 "${starter_pid}" 2>/dev/null; then log_error "启动命令未保持运行,请检查日志: ${CONSOLE_LOG}" exit 1 fi log_info "后端启动完成,PID: ${starter_pid}" } stop_backend() { local pids pids="$(collect_pids)" if [[ -z "${pids}" ]]; then log_info "未发现运行中的后端进程" rm -f "${PID_FILE}" return 0 fi log_info "准备停止后端进程: ${pids}" local pid for pid in ${pids}; do kill -TERM "${pid}" 2>/dev/null || true done local elapsed=0 local remaining_pids="${pids}" while [[ -n "${remaining_pids}" && "${elapsed}" -lt "${STOP_WAIT_SECONDS}" ]]; do sleep 1 elapsed=$((elapsed + 1)) remaining_pids="" for pid in ${pids}; do if kill -0 "${pid}" 2>/dev/null; then remaining_pids="${remaining_pids} ${pid}" fi done remaining_pids="$(xargs <<<"${remaining_pids}" 2>/dev/null || true)" done if [[ -n "${remaining_pids}" ]]; then log_info "仍有进程未退出,执行强制停止: ${remaining_pids}" for pid in ${remaining_pids}; do kill -KILL "${pid}" 2>/dev/null || true done fi rm -f "${PID_FILE}" log_info "后端停止完成" } status_backend() { local pids pids="$(collect_pids)" if [[ -n "${pids}" ]]; then log_info "后端正在运行,进程: ${pids}" return 0 fi log_info "后端未运行" } follow_logs() { mkdir -p "${LOG_DIR}" touch "${CONSOLE_LOG}" log_info "持续输出日志中,按 Ctrl+C 仅退出日志查看,不会停止后端进程" tail -n 200 -F "${CONSOLE_LOG}" } start_action() { stop_backend start_backend follow_logs } main() { local action="${1:-start}" case "${action}" in start) start_action ;; stop) stop_backend ;; restart) start_action ;; status) status_backend ;; logs) follow_logs ;; -h|--help|help) usage ;; *) usage exit 1 ;; esac } main "$@"