Ubuntu 24.04 生产环境 Java 程序

我爱海鲸 2026-01-09 19:23:56 暂无标签

简介乌班图

Ubuntu 24.04 生产环境 Java 程序(zoc)部署文档

一、环境准备

1. 系统依赖与源配置

确保系统源可用(已切换国内源并配置 IPv4),更新系统:
sudo apt update && sudo apt upgrade -y

2. 安装 JDK

# 安装 OpenJDK 8
sudo apt install openjdk-8-jdk -y


# 安装 OpenJDK 17(推荐开源版本)
sudo apt install -y openjdk-17-jdk

# 验证安装
java -version



# 列出系统中所有已安装的 JDK 路径
ls /usr/lib/jvm/
# 查看 java 命令的备选配置(安装 JDK 17 后不会自动添加到这里)
sudo update-alternatives --list java



# 配置 java 命令的默认版本
sudo update-alternatives --config java


有 2 个候选项可用于替换 java (提供 /usr/bin/java)。

  选择    路径                                            优先级  状态
------------------------------------------------------------
* 0            /usr/lib/jvm/java-17-openjdk-amd64/bin/java   1711      自动模式
  1            /usr/lib/jvm/java-11-openjdk-amd64/bin/java   1111      手动模式
  2            /usr/lib/jvm/java-8-openjdk-amd64/bin/java    1081      手动模式

要维持当前值[*]请按<回车>,或者键入选择的编号:



# 删除编号为1的备选项(根据你的实际编号调整)
sudo update-alternatives --remove java /usr/lib/jvm/java-17-openjdk-amd64/bin/java


# 注册JDK 8(优先级1081)
sudo update-alternatives --install /usr/bin/java java /usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java 1081

# 注册JDK 17(优先级1711)
sudo update-alternatives --install /usr/bin/java java /usr/lib/jvm/java-17-openjdk-amd64/bin/java 1711


# 查看所有已安装的 openjdk 包
dpkg --list | grep -i openjdk


# 卸载 JDK 17(包名需与实际匹配)
sudo apt remove --purge -y openjdk-17-jdk

# 清理残留依赖
sudo apt autoremove -y


# 查看当前 java 备选配置
sudo update-alternatives --list java

# 删除指定路径的备选(替换为实际残留路径)
sudo update-alternatives --remove java /usr/lib/jvm/java-17-openjdk-amd64/bin/java

二、创建专属运行用户

# 创建用户组
sudo groupadd -r zoc-app

# 创建无登录权限的运行用户
sudo useradd -r -s /sbin/nologin -g zoc-app zoc-app

三、部署目录与权限配置

1. 部署目录结构

# 创建程序主目录
sudo mkdir -p /opt/java-app/zoc/{bin,logs,conf}
 
目录说明:
  • /opt/java-app/zoc:程序根目录
  • bin/:启停脚本目录
  • logs/:日志目录
  • conf/:配置文件目录

2. 权限配置

 
# 递归设置目录属主为 zoc-app
sudo chown -R zoc-app:zoc-app /opt/java-app/zoc

# 设置目录基础权限(运行用户可读写,同组只读)
sudo chmod -R 750 /opt/java-app/zoc
 

四、上传程序文件

将 Java 程序包(zoc.jar)及配置文件(application-prod.yml)上传至对应目录:
# 上传 jar 包到程序根目录
sudo cp /path/to/local/zoc.jar /opt/java-app/zoc/

# 上传配置文件到 conf 目录
sudo cp /path/to/local/application-prod.yml /opt/java-app/zoc/conf/

# 再次设置文件属主
sudo chown zoc-app:zoc-app /opt/java-app/zoc/zoc.jar
sudo chown zoc-app:zoc-app /opt/java-app/zoc/conf/application-prod.yml
 

五、编写启停脚本

1. 启动脚本(bin/start.sh

cat > /opt/java-app/zoc/bin/start.sh << 'EOF'
#!/bin/bash
APP_NAME="zoc"
APP_BASE_DIR="/opt/java-app/zoc"
JAR_PATH="${APP_BASE_DIR}/zoc.jar"
CONF_PATH="${APP_BASE_DIR}/conf/application-prod.yml"
JVM_OPTS="-Xms512m -Xmx1024m -XX:+UseG1GC"

if [ $# -ne 1 ]; then
    echo "用法:$0 <端口号>(如:./start.sh 8080)"
    exit 1
fi
PORT=$1

# 检查端口占用
if netstat -tulpn | grep -q ":${PORT} "; then
    echo "端口 ${PORT} 已被占用"
    exit 1
fi

LOG_FILE="${APP_BASE_DIR}/logs/${APP_NAME}-${PORT}.out"
PID_FILE="${APP_BASE_DIR}/logs/${APP_NAME}-${PORT}.pid"

# 启动实例
echo "启动实例(端口${PORT})..."
sudo -u zoc-app nohup java ${JVM_OPTS} \
  -jar ${JAR_PATH} \
  --spring.config.location=${CONF_PATH} \
  --server.port=${PORT} \
  > ${LOG_FILE} 2>&1 &

echo $! > ${PID_FILE}
echo "启动成功!日志:${LOG_FILE}"
EOF
指定jdk版本:
#!/bin/bash
# 核心配置:指定要使用的JDK版本路径
JAVA_HOME="/usr/lib/jvm/java-17-openjdk-amd64"

# 应用基础配置
APP_NAME="trafficfruit"
APP_BASE_DIR="/opt/java-app/tf"
JAR_PATH="${APP_BASE_DIR}/trafficfruit.jar"
CONF_PATH="${APP_BASE_DIR}/conf/application-prod.yml"
JVM_OPTS="-Xms512m -Xmx1024m -XX:+UseG1GC -Dfile.encoding=UTF-8"

# ###########################
# 前置检查:确保JDK路径有效
# ###########################
if [ ! -d "${JAVA_HOME}" ]; then
    echo "错误:指定的JDK路径不存在!路径:${JAVA_HOME}"
    exit 1
fi

# 验证JDK版本
echo "当前脚本检测的JDK版本:"
${JAVA_HOME}/bin/java -version  # 直接用绝对路径验证,避免环境变量干扰
echo "------------------------"

# ###########################
# 入参检查:必须传入端口号
# ###########################
if [ $# -ne 1 ]; then
    echo "用法:$0 <端口号>(如:./start.sh 8080)"
    exit 1
fi
PORT=$1

# 校验端口合法性
if ! [[ "${PORT}" =~ ^[0-9]+$ ]] || [ "${PORT}" -lt 1 ] || [ "${PORT}" -gt 65535 ]; then
    echo "错误:端口号必须是1-65535之间的数字!"
    exit 1
fi

# ###########################
# 目录/文件检查
# ###########################
if [ ! -d "${APP_BASE_DIR}" ]; then
    echo "错误:应用基础目录不存在!路径:${APP_BASE_DIR}"
    exit 1
fi

if [ ! -f "${JAR_PATH}" ]; then
    echo "错误:JAR包不存在!路径:${JAR_PATH}"
    exit 1
fi

if [ ! -f "${CONF_PATH}" ]; then
    echo "错误:配置文件不存在!路径:${CONF_PATH}"
    exit 1
fi

# 日志目录检查&权限设置
LOG_DIR="${APP_BASE_DIR}/logs"
if [ ! -d "${LOG_DIR}" ]; then
    mkdir -p "${LOG_DIR}"
    if id "zoc-app" >/dev/null 2>&1; then
        chown -R zoc-app:zoc-app "${LOG_DIR}"
        chmod 755 "${LOG_DIR}"
    fi
fi

LOG_FILE="${LOG_DIR}/${APP_NAME}-${PORT}.out"
PID_FILE="${LOG_DIR}/${APP_NAME}-${PORT}.pid"

# ###########################
# 启动应用(核心修复:用JDK 17绝对路径)
# ###########################
echo "启动${APP_NAME}实例(端口${PORT})..."
cd "${APP_BASE_DIR}" || exit 1

# 核心修复:直接调用JDK 17的java绝对路径,不受sudo环境变量影响
if id "zoc-app" >/dev/null 2>&1; then
    sudo -u zoc-app nohup ${JAVA_HOME}/bin/java ${JVM_OPTS} \
      -jar ${JAR_PATH} \
      --spring.config.location=${CONF_PATH} \
      --server.port=${PORT} > "${LOG_FILE}" 2>&1 &
else
    nohup ${JAVA_HOME}/bin/java ${JVM_OPTS} \
      -jar ${JAR_PATH} \
      --spring.config.location=${CONF_PATH} \
      --server.port=${PORT} > "${LOG_FILE}" 2>&1 &
fi

# 检查启动是否成功
START_PID=$!
sleep 3
if ps -p ${START_PID} >/dev/null 2>&1; then
    echo ${START_PID} > "${PID_FILE}"
    if id "zoc-app" >/dev/null 2>&1; then
        chown zoc-app:zoc-app ${PID_FILE}
    fi
    echo "启动成功!PID:${START_PID},日志:${LOG_FILE}"
else
    echo "启动失败!日志前10行:"
    head -n 10 "${LOG_FILE}"
    exit 1
fi

2. 停止脚本(bin/stop.sh

cat > /opt/java-app/zoc/bin/stop.sh << 'EOF'
#!/bin/bash
APP_NAME="zoc"
APP_BASE_DIR="/opt/java-app/zoc"

if [ $# -ne 1 ]; then
    echo "用法:$0 <端口号>(如:./stop.sh 8080)"
    exit 1
fi
PORT=$1

PID_FILE="${APP_BASE_DIR}/logs/${APP_NAME}-${PORT}.pid"
if [ ! -f ${PID_FILE} ]; then
    echo "实例未运行"
    exit 0
fi
PID=$(cat ${PID_FILE})

# 停止进程
kill ${PID}
sleep 10
if ps -p ${PID} > /dev/null; then
    kill -9 ${PID}
fi

rm -f ${PID_FILE}
echo "实例已停止"
EOF
指定jdk版本:
#!/bin/bash
# 核心配置:与启动脚本保持一致的JDK版本路径
JAVA_HOME="/usr/lib/jvm/java-17-openjdk-amd64"

# 应用基础配置(与start.sh保持一致)
APP_NAME="trafficfruit"
APP_BASE_DIR="/opt/java-app/tf"

# ###########################
# 前置检查:确保JDK路径有效(可选)
# ###########################
if [ ! -d "${JAVA_HOME}" ]; then
    echo "警告:指定的JDK路径不存在!路径:${JAVA_HOME}"
    echo "(停止进程不依赖JDK,将继续执行停止操作)"
fi

# 临时设置PATH(仅为脚本完整性)
export PATH="${JAVA_HOME}/bin:${PATH}"

# ###########################
# 入参检查:必须传入端口号
# ###########################
if [ $# -ne 1 ]; then
    echo "用法:$0 <端口号>(如:./stop.sh 8080)"
    exit 1
fi
PORT=$1

# 校验端口合法性(新增:和启动脚本保持一致)
if ! [[ "${PORT}" =~ ^[0-9]+$ ]] || [ "${PORT}" -lt 1 ] || [ "${PORT}" -gt 65535 ]; then
    echo "错误:端口号必须是1-65535之间的数字!你输入的是:${PORT}"
    exit 1
fi

# ###########################
# 进程文件检查与验证(生产环境优化)
# ###########################
PID_FILE="${APP_BASE_DIR}/logs/${APP_NAME}-${PORT}.pid"
if [ ! -f ${PID_FILE} ]; then
    # 优化:PID文件不存在时,尝试通过端口查找进程
    echo "未找到PID文件:${PID_FILE}"
    # 查找占用指定端口的Java进程(优化:兼容ss/netstat,且用JDK绝对路径过滤更精准)
    PID=""
    if command -v ss >/dev/null 2>&1; then
        PID=$(ss -tulpn 2>/dev/null | grep -E ":${PORT}\s+" | grep -E "${JAVA_HOME}/bin/java|java" | awk '{print $7}' | cut -d'=' -f2 | cut -d',' -f1)
    elif command -v netstat >/dev/null 2>&1; then
        PID=$(netstat -tulpn 2>/dev/null | grep -E ":${PORT}\s+" | grep -E "${JAVA_HOME}/bin/java|java" | awk '{print $7}' | cut -d'=' -f2 | cut -d',' -f1)
    fi
    
    if [ -z "${PID}" ]; then
        echo "实例(端口${PORT})未运行"
        exit 0
    else
        echo "通过端口找到进程PID:${PID}"
    fi
else
    PID=$(cat ${PID_FILE})
    # 验证PID是否为有效进程
    if ! ps -p ${PID} > /dev/null 2>&1; then
        echo "PID文件中的进程(${PID})已不存在,清理无效PID文件"
        rm -f ${PID_FILE}
        exit 0
    fi
fi

# ###########################
# 优雅停止进程(生产环境标准流程)
# ###########################
echo "停止${APP_NAME}实例(端口${PORT},PID:${PID})..."

# 第一步:发送优雅停止信号(SIGTERM)
kill ${PID} > /dev/null 2>&1

# 等待进程退出(最多30秒,避免强制杀进程)
WAIT_TIME=0
MAX_WAIT=30
while ps -p ${PID} > /dev/null 2>&1; do
    if [ ${WAIT_TIME} -ge ${MAX_WAIT} ]; then
        echo "等待${MAX_WAIT}秒后进程仍未退出,强制终止(kill -9)"
        kill -9 ${PID} > /dev/null 2>&1
        break
    fi
    echo "等待进程退出...(已等待${WAIT_TIME}秒)"
    sleep 1
    WAIT_TIME=$((WAIT_TIME + 1))
done

# 清理PID文件
if [ -f ${PID_FILE} ]; then
    rm -f ${PID_FILE}
    echo "已清理PID文件:${PID_FILE}"
fi

# 最终验证
if ps -p ${PID} > /dev/null 2>&1; then
    echo "停止失败!进程${PID}仍在运行"
    exit 1
else
    echo "实例(端口${PORT})已成功停止"
    exit 0
fi

3.状态脚本(bin/status.sh)

#!/bin/bash
# 查看所有运行中的实例状态

APP_NAME="zoc"
APP_BASE_DIR="/opt/java-app/zoc"
PID_FILES="${APP_BASE_DIR}/logs/${APP_NAME}-*.pid"

echo "${APP_NAME} 运行中实例:"
echo "----------------------------------------"
for pid_file in ${PID_FILES}; do
    if [ -f ${pid_file} ]; then
        # 提取端口号
        PORT=$(echo ${pid_file} | grep -oP "(?<=-)\d+(?=\.pid)")
        PID=$(cat ${pid_file})
        # 检查进程是否存活
        if ps -p ${PID} > /dev/null 2>&1; then
            echo "端口:${PORT} | PID:${PID} | 状态:运行中"
        else
            echo "端口:${PORT} | PID:${PID} | 状态:已停止(清理无效PID)"
            rm -f ${pid_file}
        fi
    fi
done
echo "----------------------------------------"

指定jdk:

#!/bin/bash
# 核心配置:与启动/停止脚本保持一致的JDK版本路径(仅为配置统一)
JAVA_HOME="/usr/lib/jvm/java-17-openjdk-amd64"

# 应用基础配置(关键修改:对齐trafficfruit项目)
APP_NAME="trafficfruit"  # 原zoc → 改为trafficfruit
APP_BASE_DIR="/opt/java-app/tf"  # 原/opt/java-app/zoc → 改为/opt/java-app/tf
PID_FILES="${APP_BASE_DIR}/logs/${APP_NAME}-*.pid"

# ###########################
# 前置检查:JDK路径验证(仅为配置完整性)
# ###########################
if [ ! -d "${JAVA_HOME}" ]; then
    echo "⚠️  警告:指定的JDK路径不存在!路径:${JAVA_HOME}"
    echo "(状态检查不依赖JDK,将继续执行)"
    echo "----------------------------------------"
else
    # 临时设置PATH(保持配置统一)
    export PATH="${JAVA_HOME}/bin:${PATH}"
    # 可选:显示当前配置的JDK版本(改用绝对路径,避免sudo环境干扰)
    echo "当前配置的JDK版本:"
    ${JAVA_HOME}/bin/java -version 2>&1 | head -n 1
    echo "----------------------------------------"
fi

# ###########################
# 实例状态检查(生产环境优化)
# ###########################
echo "${APP_NAME} 运行中实例:"
echo "----------------------------------------"

# 标记是否有运行中的实例
has_running=0

# 遍历所有PID文件
for pid_file in ${PID_FILES}; do
    # 跳过非文件类型(如无匹配的情况)
    [ -f "${pid_file}" ] || continue
    
    # 提取端口号(兼容不同命名格式,增强鲁棒性)
    PORT=$(basename "${pid_file}" | sed -n "s/${APP_NAME}-\([0-9]\+\)\.pid/\1/p")
    if [ -z "${PORT}" ]; then
        echo "❌ 无效PID文件:${pid_file}(无法提取端口号),已清理"
        rm -f "${pid_file}"
        continue
    fi

    PID=$(cat "${pid_file}" 2>/dev/null)
    # 检查PID是否有效
    if [ -z "${PID}" ] || ! [[ "${PID}" =~ ^[0-9]+$ ]]; then
        echo "❌ 端口:${PORT} | PID:无效 | 状态:PID文件损坏(清理无效文件)"
        rm -f "${pid_file}"
        continue
    fi

    # 检查进程是否存活
    if ps -p "${PID}" > /dev/null 2>&1; then
        # 额外优化:精准匹配JDK 17的Java进程(避免误判)
        if ps -p "${PID}" -o cmd= | grep -E "${JAVA_HOME}/bin/java|java" > /dev/null 2>&1; then
            echo "✅ 端口:${PORT} | PID:${PID} | 状态:运行中"
            has_running=1
        else
            echo "❌ 端口:${PORT} | PID:${PID} | 状态:非目标Java进程(清理无效PID)"
            rm -f "${pid_file}"
        fi
    else
        echo "❌ 端口:${PORT} | PID:${PID} | 状态:已停止(清理无效PID)"
        rm -f "${pid_file}"
    fi
done

# 无实例时的友好提示
if [ ${has_running} -eq 0 ]; then
    echo "ℹ️  暂无运行中的${APP_NAME}实例"
fi

echo "----------------------------------------"

4. 脚本权限配置

# 赋予执行权限
sudo chmod 750 /opt/java-app/zoc/bin/{start.sh,stop.sh,status.sh}
sudo chown zoc-app:zoc-app /opt/java-app/zoc/bin/{start.sh,stop.sh,status.sh}

六、启动与管理程序

1. 启动多实例

# 启动端口8080的实例
sudo /opt/java-app/zoc/bin/start.sh 8080

# 启动端口8081的实例
sudo /opt/java-app/zoc/bin/start.sh 8081

2. 停止实例

# 停止端口8080的实例
sudo /opt/java-app/zoc/bin/stop.sh 8080

3. 查看运行状态

ps -ef | grep zoc.jar | grep -v grep

七、生产环境补充

  1. 日志切割:配置 logrotate 定期切割日志;
  2. 进程监控:搭配 supervisor 实现进程自动重启;
  3. 负载均衡:通过 Nginx 反向代理多实例。

你好:我的2025