Linux

关注公众号 jb51net

关闭
首页 > 网站技巧 > 服务器 > Linux > Linux启动Java JAR Shell脚本

Linux启动Java JAR的Shell脚本的流程步骤

作者:Java小白笔记

本文以实际 run.sh 为依据,完整解释 Java JAR 的启动、停止、重启、JVM/代理参数和日志机制,并给出执行命令、分层验收标准与失败边界,需要的朋友可以参考下

内容摘要:本文以实际 run.sh 为依据,完整解释 Java JAR 的启动、停止、重启、JVM/代理参数和日志机制,并给出执行命令、分层验收标准与失败边界。重点提醒:打印“启动成功”只代表后台命令已提交,不代表端口监听或应用健康。

#!/bin/sh

##############################
# 【公共配置变量 - 只需修改这里】
##############################
PORT=8900                    # 服务端口
JAR_NAME="aiTools.jar"  # jar包名称
PROFILE="prod"                # 启动环境
LOG_FILE="_server.out"       # 日志文件名
XMS="512M"                   # 堆内存初始
XMX="1024M"                   # 堆内存最大
METASPACE="512M"             # 元空间最大

# 代理开关:false 不开启,true 开启
ENABLE_PROXY=true
PROXY_HOST="127.0.0.1"
PROXY_PORT="7890"
NON_PROXY_HOSTS="localhost|127.*|[::1]"

# 执行动作:start 启动、stop 停止、restart 重启、logs 查看实时日志、count 统计匹配记录数、search 查询上下文
# 不传参数时默认重启
ACTION="${1:-restart}"

##############################
# 日志相关操作
##############################
show_logs() {
    if [ ! -f "$LOG_FILE" ]; then
        echo "错误:日志文件不存在:$LOG_FILE"
        exit 1
    fi

    echo "===== 实时日志:$LOG_FILE ====="
    tail -f "$LOG_FILE"
}

count_log_lines() {
    if [ ! -f "$LOG_FILE" ]; then
        echo "错误:日志文件不存在:$LOG_FILE"
        exit 1
    fi

    if [ "$#" -eq 0 ]; then
        echo "用法:$0 count <关键字>"
        echo "示例:$0 count ERROR"
        exit 1
    fi

    KEYWORD="$*"
    MATCH_COUNT=$(grep -F -c -- "$KEYWORD" "$LOG_FILE")
    echo "日志文件:$LOG_FILE"
    echo "查询字符:$KEYWORD"
    echo "匹配记录数:$MATCH_COUNT"
}

search_log() {
    if [ ! -f "$LOG_FILE" ]; then
        echo "错误:日志文件不存在:$LOG_FILE"
        exit 1
    fi

    if [ "$#" -eq 0 ]; then
        echo "用法:$0 search <关键字>"
        echo "示例:$0 search ERROR"
        exit 1
    fi

    KEYWORD="$*"
    echo "===== 查询日志:$LOG_FILE(匹配记录前后各 5 行) ====="
    if ! grep -n -F -C 5 -- "$KEYWORD" "$LOG_FILE"; then
        echo "未找到包含字符:$KEYWORD"
    fi
}

show_usage() {
    echo "用法:$0 [start|stop|restart|logs|count <关键字>|search <关键字>]"
    echo "  start                  启动服务"
    echo "  stop                   停止服务"
    echo "  restart                重启服务(默认操作)"
    echo "  logs                   实时查看日志(等同 tail -f $LOG_FILE)"
    echo "  count <关键字>         统计日志中包含关键字的记录数"
    echo "  search <关键字>        查看匹配记录前后各 5 行(带行号)"
}

##############################
# 停止服务
##############################
stop_service() {
    echo "===== 停止服务 [端口:$PORT] ====="
    PID=$(netstat -nlp 2>/dev/null | grep -w "$PORT" | sed -r 's#.* (.*)/.*#\1#')
    if [ -n "$PID" ]; then
        kill -9 $PID
        echo "已停止进程:$PID"
        sleep 1
    else
        echo "未发现端口 $PORT 对应的运行进程。"
    fi
}

case "$ACTION" in
    logs|log|tail)
        show_logs
        exit 0
        ;;
    count|grep)
        shift
        count_log_lines "$@"
        exit 0
        ;;
    search)
        shift
        search_log "$@"
        exit 0
        ;;
    stop)
        stop_service
        exit 0
        ;;
    start)
        PID=$(netstat -nlp 2>/dev/null | grep -w "$PORT" | sed -r 's#.* (.*)/.*#\1#')
        if [ -n "$PID" ]; then
            echo "错误:端口 $PORT 已被进程 $PID 占用,请先执行:$0 stop"
            exit 1
        fi
        ;;
    restart)
        stop_service
        ;;
    *)
        show_usage
        exit 1
        ;;
esac

##############################
# 组装 JVM 参数
##############################
set -- \
    "-Dserver.port=$PORT" \
    "-Xms$XMS" \
    "-Xmx$XMX" \
    "-XX:MaxMetaspaceSize=$METASPACE" \
    "-XX:+UseG1GC"

if [ "$ENABLE_PROXY" = "true" ]; then
    set -- "$@" \
        "-Dhttp.proxyHost=$PROXY_HOST" \
        "-Dhttp.proxyPort=$PROXY_PORT" \
        "-Dhttps.proxyHost=$PROXY_HOST" \
        "-Dhttps.proxyPort=$PROXY_PORT" \
        "-Dhttp.nonProxyHosts=$NON_PROXY_HOSTS"
fi

##############################
# 清空日志并启动
##############################
echo "===== 启动新服务 ====="
> "$LOG_FILE"

nohup java "$@" -jar "$JAR_NAME" \
    "--spring.profiles.active=$PROFILE" \
    > "$LOG_FILE" 2>&1 &

echo "启动成功!"
echo "===== 实时日志 ====="
tail -f "$LOG_FILE"

读者问题与适用范围

这份脚本解决的是单机、单实例 Java 服务的进程管理问题:通过一个命令停止占用指定端口的进程,或者启动指定名称的 JAR,并把输出写入日志文件。它适合部署目录中已经存在 JAR 的 Linux 主机,不等同于 systemd、Supervisor 或容器编排方案。

本文讨论的源码是:/Users/xiaoxiangyuan/Documents/快客达/linux启动脚本/改进版/run.sh。脚本中的变量、命令和输出均按当前文件记录;未在该文件中出现的 Java 版本、Spring Boot 版本、健康检查接口和生产部署结果,不作推断。

核心判断与证据边界

脚本的关键价值不只是“执行 java -jar”,而是把端口占用检查、JVM 参数、代理参数和日志重定向集中成一个可重复入口。不过它的“启动成功”只表示 nohup 命令已提交到后台,脚本没有等待端口监听,也没有调用健康检查接口。因此,进程启动、端口监听和应用就绪必须分开验证。

判断源码证据当前能证明什么未覆盖的边界
默认执行重启ACTION="${1:-restart}"(第 21 行)不传参数时先停再启未判断旧进程是否确实退出
按端口停止进程netstat ... grep -w "$PORT" ...(第 28 行)可尝试取得端口对应 PID解析格式依赖 netstat 输出,可能误匹配或取不到 PID
参数集中配置第 6~18 行与第 62~76 行端口、内存、环境、代理可在脚本顶部修改没有校验参数格式和取值范围
后台写日志第 84~86 行Java 输出进入 $LOG_FILE未验证 JAR 是否存在、Java 是否成功启动
启动后跟踪日志第 90 行 tail -f当前终端可持续观察日志脚本会保持前台,需 Ctrl+C 结束跟踪

脚本如何把配置变成一次启动

配置层:只修改顶部变量

PORT=8900
JAR_NAME="aiTools.jar"
PROFILE="prod"
LOG_FILE="_server.out"
XMS="512M"
XMX="1024M"
METASPACE="512M"

ENABLE_PROXY=true
PROXY_HOST="127.0.0.1"
PROXY_PORT="7890"
NON_PROXY_HOSTS="localhost|127.*|[::1]"

ACTION="${1:-restart}"

这些变量形成启动契约:

变量作用示例注意事项
PORTSpring Boot 服务端口8900必须与停服检测的端口一致
JAR_NAMEJAR 文件名或路径aiTools.jar相对路径以执行脚本时的当前目录为准
PROFILESpring profileprod通过应用参数传入
LOG_FILE标准输出和错误输出文件_server.out启动时会被清空
XMS/XMXJVM 初始/最大堆512M/1024M受主机可用内存约束
METASPACE元空间上限512M过小可能导致类加载失败
ENABLE_PROXY是否附加代理参数true/false只有严格等于 true 才启用

JAR_NAME 和 LOG_FILE 没有转换为脚本所在目录的绝对路径,所以从错误目录执行 ./run.sh 可能出现“找不到 JAR”或日志写到意外位置。这是相对路径语义,不是 Java 本身的问题。

动作层:start、stop、restart的分支

停止函数的核心代码如下:

stop_service() {
    echo "===== 停止服务 [端口:$PORT] ====="
    PID=$(netstat -nlp 2>/dev/null | grep -w "$PORT" | sed -r 's#.* (.*)/.*#\1#')
    if [ -n "$PID" ]; then
        kill -9 $PID
        echo "已停止进程:$PID"
        sleep 1
    else
        echo "未发现端口 $PORT 对应的运行进程。"
    fi
}

动作契约如下:

命令行为退出条件
./run.sh stop查询 $PORT,找到 PID 后执行 kill -9,然后退出停止分支显式 exit 0
./run.sh start先检查端口;已占用则报错并退出,否则继续启动只检查端口,不检查 JAR 或 Java
./run.sh restart先调用 stop_service,再继续组装参数并启动停止后仅等待 1 秒
./run.sh因默认值等同于 restart与 restart 相同
其他参数打印用法并退出exit 1

这里的设计取舍很明确:按端口找进程比记录 PID 文件更容易接管已有进程,但依赖 netstat 文本格式;kill -9 能快速终止进程,却不给应用执行优雅关闭、刷盘和释放资源的机会。

JVM 与代理层:用set --构造参数数组

set -- \
    "-Dserver.port=$PORT" \
    "-Xms$XMS" \
    "-Xmx$XMX" \
    "-XX:MaxMetaspaceSize=$METASPACE" \
    "-XX:+UseG1GC"

if [ "$ENABLE_PROXY" = "true" ]; then
    set -- "$@" \
        "-Dhttp.proxyHost=$PROXY_HOST" \
        "-Dhttp.proxyPort=$PROXY_PORT" \
        "-Dhttps.proxyHost=$PROXY_HOST" \
        "-Dhttps.proxyPort=$PROXY_PORT" \
        "-Dhttp.nonProxyHosts=$NON_PROXY_HOSTS"
fi

set -- 把当前脚本的位置参数替换成 JVM 参数;"$@" 再把已有参数逐项追加,避免把整串参数错误地合并成一个参数。最终命令中,-D...、-X... 位于 -jar 之前,属于 JVM 参数;--spring.profiles.active=prod 位于 JAR 之后,属于 Spring Boot 应用参数。

代理只配置了 HTTP、HTTPS 和 HTTP 非代理主机,未配置 SOCKS 代理。NON_PROXY_HOSTS 使用 Java 系统属性的匹配格式,实际是否覆盖业务域名仍需结合应用发起请求的客户端实现验证。

完整源码:当前run.sh

以下代码与源码文件保持一致,未把建议改动混入其中。

#!/bin/sh

##############################
# 【公共配置变量 - 只需修改这里】
##############################
PORT=8900                    # 服务端口
JAR_NAME="aiTools.jar"  # jar包名称
PROFILE="prod"                # 启动环境
LOG_FILE="_server.out"       # 日志文件名
XMS="512M"                   # 堆内存初始
XMX="1024M"                   # 堆内存最大
METASPACE="512M"             # 元空间最大

# 代理开关:false 不开启,true 开启
ENABLE_PROXY=true
PROXY_HOST="127.0.0.1"
PROXY_PORT="7890"
NON_PROXY_HOSTS="localhost|127.*|[::1]"

# 执行动作:start 启动、stop 停止、restart 重启;不传参数时默认重启
ACTION="${1:-restart}"

##############################
# 停止服务
##############################
stop_service() {
    echo "===== 停止服务 [端口:$PORT] ====="
    PID=$(netstat -nlp 2>/dev/null | grep -w "$PORT" | sed -r 's#.* (.*)/.*#\1#')
    if [ -n "$PID" ]; then
        kill -9 $PID
        echo "已停止进程:$PID"
        sleep 1
    else
        echo "未发现端口 $PORT 对应的运行进程。"
    fi
}

case "$ACTION" in
    stop)
        stop_service
        exit 0
        ;;
    start)
        PID=$(netstat -nlp 2>/dev/null | grep -w "$PORT" | sed -r 's#.* (.*)/.*#\1#')
        if [ -n "$PID" ]; then
            echo "错误:端口 $PORT 已被进程 $PID 占用,请先执行:$0 stop"
            exit 1
        fi
        ;;
    restart)
        stop_service
        ;;
    *)
        echo "用法:$0 [start|stop|restart]"
        exit 1
        ;;
esac

##############################
# 组装 JVM 参数
##############################
set -- \
    "-Dserver.port=$PORT" \
    "-Xms$XMS" \
    "-Xmx$XMX" \
    "-XX:MaxMetaspaceSize=$METASPACE" \
    "-XX:+UseG1GC"

if [ "$ENABLE_PROXY" = "true" ]; then
    set -- "$@" \
        "-Dhttp.proxyHost=$PROXY_HOST" \
        "-Dhttp.proxyPort=$PROXY_PORT" \
        "-Dhttps.proxyHost=$PROXY_HOST" \
        "-Dhttps.proxyPort=$PROXY_PORT" \
        "-Dhttp.nonProxyHosts=$NON_PROXY_HOSTS"
fi

##############################
# 清空日志并启动
##############################
echo "===== 启动新服务 ====="
> "$LOG_FILE"

nohup java "$@" -jar "$JAR_NAME" \
    "--spring.profiles.active=$PROFILE" \
    > "$LOG_FILE" 2>&1 &

echo "启动成功!"
echo "===== 实时日志 ====="
tail -f "$LOG_FILE"

可复现的执行与验证闭环

1. 准备目录和权限

把 run.sh 与 aiTools.jar 放在同一目录,或把 JAR_NAME 改成可访问路径;然后执行:

cd /path/to/app
chmod +x run.sh
java -version
ls -l run.sh aiTools.jar

java -version 证明 Java 命令可用,ls 证明脚本依赖的文件存在;二者都通过后,才有资格进入启动验证。

2. 先做无副作用的语法检查

sh -n run.sh

无输出且退出码为 0,只能说明 Shell 语法可被解析,不能证明 netstat、JAR 或应用配置正确。

3. 执行动作

./run.sh start
./run.sh stop
./run.sh restart

启动或重启后,当前终端会进入 tail -f。按 Ctrl+C 只会结束日志跟踪,不会自动停止后台 Java 进程;需要停服时另开终端执行 ./run.sh stop。

4. 分层确认服务状态

ss -lntp | grep ':8900'
ps -ef | grep '[a]iTools.jar'
tail -n 100 _server.out
curl -f http://127.0.0.1:8900/实际健康检查路径

脚本源码没有提供健康检查路径,因此最后一条命令中的路径必须替换为应用真实接口。只有日志无致命异常、Java 进程存在、端口已监听,并且健康接口返回成功,才能判断应用基本就绪。

失败路径、风险与改造建议

  1. netstat 不存在或无权限:查询结果为空,脚本可能误报“未发现进程”,随后重复启动。现代发行版可优先使用 ss,或在部署前明确安装 net-tools。
  2. PID 解析不可靠:sed -r 依赖扩展正则语法,且脚本假设 netstat -nlp 输出包含 PID/程序名。不同系统或容器环境格式变化时,停止逻辑可能失效。
  3. 强制杀进程:kill -9 跳过优雅关闭。更稳妥的顺序是先发送 TERM,等待有限时间,再对仍存活的 PID 使用 KILL。
  4. 日志被清空:> "$LOG_FILE" 会在每次启动前截断历史日志。若需要审计或故障回溯,应改用按日期轮转、追加写入或交给 journald。
  5. 没有启动失败回滚:即使 JAR 不存在、端口启动失败或配置错误,脚本仍会先打印“启动成功!”,随后 tail -f 继续等待。应检查后台 PID、短暂等待端口,并设置超时和失败退出码。
  6. 相对路径和多实例限制:脚本依赖当前工作目录,且固定一个端口、一个日志文件、一个 JAR,不适合并行多实例;多实例应为每个实例拆分配置或引入服务管理器。

取舍与结论

当前脚本适合“人工登录服务器后快速重启单个 Java 服务”的场景:配置集中、命令短、日志即时可见。它的代价是进程识别依赖端口文本解析,停止采用不可恢复的强杀,启动结果没有健康确认,日志也会被覆盖。

如果目标只是开发机或低频人工运维,可以保留现状并执行分层验证;如果目标是生产自动恢复、开机自启、优雅停机和可观测性,应迁移到 systemd/Supervisor,并把健康检查、超时、日志轮转和权限隔离纳入服务契约。无论是否改造,不能仅凭“启动成功!”这行输出宣称应用已经可用。

以上就是Linux启动Java JAR的Shell脚本的流程步骤的详细内容,更多关于Linux启动Java JAR Shell脚本的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文