内容摘要:本文以实际 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脚本的资料请关注代码网其它相关文章!
发表评论