Linux启动Java JAR的Shell脚本的流程步骤
作者:Java小白笔记
内容摘要:本文以实际 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}"
这些变量形成启动契约:
| 变量 | 作用 | 示例 | 注意事项 |
|---|---|---|---|
PORT | Spring Boot 服务端口 | 8900 | 必须与停服检测的端口一致 |
JAR_NAME | JAR 文件名或路径 | aiTools.jar | 相对路径以执行脚本时的当前目录为准 |
PROFILE | Spring profile | prod | 通过应用参数传入 |
LOG_FILE | 标准输出和错误输出文件 | _server.out | 启动时会被清空 |
XMS/XMX | JVM 初始/最大堆 | 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 进程存在、端口已监听,并且健康接口返回成功,才能判断应用基本就绪。
失败路径、风险与改造建议
netstat不存在或无权限:查询结果为空,脚本可能误报“未发现进程”,随后重复启动。现代发行版可优先使用ss,或在部署前明确安装net-tools。- PID 解析不可靠:
sed -r依赖扩展正则语法,且脚本假设netstat -nlp输出包含PID/程序名。不同系统或容器环境格式变化时,停止逻辑可能失效。 - 强制杀进程:
kill -9跳过优雅关闭。更稳妥的顺序是先发送TERM,等待有限时间,再对仍存活的 PID 使用KILL。 - 日志被清空:
> "$LOG_FILE"会在每次启动前截断历史日志。若需要审计或故障回溯,应改用按日期轮转、追加写入或交给 journald。 - 没有启动失败回滚:即使 JAR 不存在、端口启动失败或配置错误,脚本仍会先打印“启动成功!”,随后
tail -f继续等待。应检查后台 PID、短暂等待端口,并设置超时和失败退出码。 - 相对路径和多实例限制:脚本依赖当前工作目录,且固定一个端口、一个日志文件、一个 JAR,不适合并行多实例;多实例应为每个实例拆分配置或引入服务管理器。
取舍与结论
当前脚本适合“人工登录服务器后快速重启单个 Java 服务”的场景:配置集中、命令短、日志即时可见。它的代价是进程识别依赖端口文本解析,停止采用不可恢复的强杀,启动结果没有健康确认,日志也会被覆盖。
如果目标只是开发机或低频人工运维,可以保留现状并执行分层验证;如果目标是生产自动恢复、开机自启、优雅停机和可观测性,应迁移到 systemd/Supervisor,并把健康检查、超时、日志轮转和权限隔离纳入服务契约。无论是否改造,不能仅凭“启动成功!”这行输出宣称应用已经可用。
以上就是Linux启动Java JAR的Shell脚本的流程步骤的详细内容,更多关于Linux启动Java JAR Shell脚本的资料请关注脚本之家其它相关文章!
