#!/usr/bin/env bash
|
set -euo pipefail
|
|
# ────────────────────────────────────────────────
|
# JNPF 微服务批量启动脚本
|
# 启动除 scheduletask 之外的所有服务(example 已于 2026-08-11 移除)
|
# 用法:
|
# ./start-all.sh 前台启动全部服务(实时显示就绪状态,Ctrl+C 统一停止)
|
# ./start-all.sh --build 先编译再前台启动
|
# ./start-all.sh stop 停止全部已启动的服务(用于其他终端)
|
# ./start-all.sh restart 停止 → 编译 → 前台启动
|
# ./start-all.sh status 查看各服务运行状态
|
#
|
# ./start-all.sh jnpf-dms 只启动 jnpf-dms
|
# ./start-all.sh jnpf-lims 只启动 jnpf-lims
|
# ./start-all.sh restart jnpf-lims 只重启 jnpf-lims(仅编译该模块)
|
# ./start-all.sh stop jnpf-lims 只停 jnpf-lims(不影响其他服务)
|
# ./start-all.sh status jnpf-lims 只看 jnpf-lims 状态
|
# ./start-all.sh restart jnpf-lims jnpf-platform 支持多服务
|
# ────────────────────────────────────────────────
|
|
BASE_DIR="$(cd "$(dirname "$0")" && pwd)"
|
PID_DIR="$BASE_DIR/.pids"
|
LOG_DIR="$BASE_DIR/log"
|
VERSION="6.1.0-RELEASE"
|
|
# 从仓库根 .env 加载全部 KEY=VALUE(宿主 java -jar 与 compose 共用同一份)。
|
# config/shared/*.yaml 的 ${VAR:默认} 靠进程环境解析;compose 侧由 docker 自动读 .env。
|
# 只按行取值不 source(.env 值可能含空格/特殊字符;与 create-app-roles.sh 同款)。
|
# .env 内同键以后出现的为准;启动前已在环境中的键不覆盖(允许 FOO=bar ./start-all.sh)。
|
if [ -f "$BASE_DIR/.env" ]; then
|
# 快照启动前已存在的环境变量名(bash 3.2 无关联数组,用 |name| 串匹配)
|
_preset="|"
|
while IFS= read -r _pk; do
|
[ -n "$_pk" ] && _preset="${_preset}${_pk}|"
|
done <<EOF
|
$(env | cut -d= -f1)
|
EOF
|
_loaded=0
|
while IFS= read -r _line || [ -n "$_line" ]; do
|
# 去 CR(Windows 换行)、跳过空行与注释
|
_line="${_line%$'\r'}"
|
case "$_line" in
|
''|\#*) continue ;;
|
esac
|
case "$_line" in
|
*=*) ;;
|
*) continue ;;
|
esac
|
_k="${_line%%=*}"
|
_v="${_line#*=}"
|
# 只接受合法标识符,避免把 "export FOO=bar" 这类行误当键
|
case "$_k" in
|
''|*[!A-Za-z0-9_]*|[0-9]*) continue ;;
|
esac
|
case "$_preset" in
|
*"|${_k}|"*) continue ;;
|
esac
|
# 未在启动前设定:直接 export;同键重复出现时后写覆盖前写
|
if [ -z "${!_k+x}" ]; then
|
_loaded=$((_loaded + 1))
|
fi
|
export "$_k=$_v"
|
done < "$BASE_DIR/.env"
|
echo "已从 .env 加载 ${_loaded} 个环境变量"
|
unset _line _k _v _pk _preset _loaded
|
fi
|
|
# 宿主 Java 栈与容器内 OnlyOffice 互访:容器中的 127.0.0.1 指向容器自身,
|
# 因此 DS 回源和回调必须走 Docker 提供的宿主地址;显式环境变量仍可覆盖。
|
export ONLYOFFICE_FILE_FETCH_BASE_URL="${ONLYOFFICE_FILE_FETCH_BASE_URL:-http://host.docker.internal:30000}"
|
export ONLYOFFICE_CALLBACK_BASE_URL="${ONLYOFFICE_CALLBACK_BASE_URL:-http://host.docker.internal:30000}"
|
export ONLYOFFICE_DS_INTERNAL_URL="${ONLYOFFICE_DS_INTERNAL_URL:-http://localhost:20000}"
|
|
# 「本机」档坐标翻译:.env 写的是容器视角 cx-postgres:5432,宿主进程要走
|
# 宿主发布端口 127.0.0.1:5433(compose 只发布到 5433,避免与系统 PG 撞车)。
|
# 其余基础设施(cx-redis/cx-flow-engine)发布端口与容器内一致,
|
# 由 /etc/hosts 的 cx-* → 127.0.0.1 条目解决(双侧解析约定),不在此翻译。
|
# 指向 CVM 客户实例(IP 坐标)时本块不生效。
|
if [ "${CUSTOMER_DB_HOST:-}" = "cx-postgres" ]; then
|
export CUSTOMER_DB_HOST=127.0.0.1
|
export CUSTOMER_DB_PORT=5433
|
echo "本机档:已翻译 PG 坐标 cx-postgres:5432 → 127.0.0.1:5433(宿主发布端口)"
|
fi
|
echo "客户坐标:${CUSTOMER_DB_HOST:-<未设,将回退 cx-postgres>}:${CUSTOMER_DB_PORT:-<未设,将回退 5432>}"
|
|
# JDK 9+ --add-opens 参数(与 Dockerfile 一致)
|
JVM_ADD_OPENS=(
|
--add-opens=java.base/java.lang=ALL-UNNAMED
|
--add-opens=java.base/java.lang.reflect=ALL-UNNAMED
|
--add-opens=java.base/java.lang.invoke=ALL-UNNAMED
|
--add-opens=java.base/java.math=ALL-UNNAMED
|
--add-opens=java.base/sun.net.util=ALL-UNNAMED
|
--add-opens=java.base/java.io=ALL-UNNAMED
|
--add-opens=java.base/java.net=ALL-UNNAMED
|
--add-opens=java.base/java.nio=ALL-UNNAMED
|
--add-opens=java.base/java.security=ALL-UNNAMED
|
--add-opens=java.base/java.text=ALL-UNNAMED
|
--add-opens=java.base/java.time=ALL-UNNAMED
|
--add-opens=java.base/java.util=ALL-UNNAMED
|
--add-opens=java.base/jdk.internal.module=ALL-UNNAMED
|
--add-opens=java.base/sun.security.util=ALL-UNNAMED
|
)
|
|
# 单个服务的最大 Java 堆内存。2026-08-11 平台聚合后常驻为 5 个 JVM
|
# (gateway / platform / biz-common / eln / lims),384m 的普通服务总堆上限约 1.5GB,
|
# platform 另用独立堆上限。
|
# 可按实际负载覆盖,例如:JNPF_SERVICE_MAX_HEAP=512m ./start-all.sh jnpf-lims
|
JNPF_SERVICE_MAX_HEAP="${JNPF_SERVICE_MAX_HEAP:-384m}"
|
# jnpf-platform 例外:九个模块的类元数据同处一个 JVM,384m 明显不够(实测堆 used 约 361MB
|
# 就是它的稳态下限)。单独给一档,与 compose 里 cx-platform 的 -Xmx1536m 对齐。
|
JNPF_PLATFORM_MAX_HEAP="${JNPF_PLATFORM_MAX_HEAP:-1536m}"
|
if [[ ! "$JNPF_SERVICE_MAX_HEAP" =~ ^[0-9]+[mMgG]$ ]]; then
|
echo "JNPF_SERVICE_MAX_HEAP 必须是类似 384m 或 1g 的内存大小"
|
exit 1
|
fi
|
if [[ ! "$JNPF_PLATFORM_MAX_HEAP" =~ ^[0-9]+[mMgG]$ ]]; then
|
echo "JNPF_PLATFORM_MAX_HEAP 必须是类似 1536m 或 2g 的内存大小"
|
exit 1
|
fi
|
|
JVM_OPTS=(
|
"${JVM_ADD_OPENS[@]}"
|
"-Xmx${JNPF_SERVICE_MAX_HEAP}"
|
-Dfile.encoding=utf8
|
-Djava.security.egd=file:/dev/./urandom
|
)
|
|
# 服务列表: "名称 JAR相对路径 端口"
|
# gateway 没有 -server 子模块,单独处理
|
SERVICES=(
|
"jnpf-gateway jnpf-gateway/target/jnpf-gateway-${VERSION}.jar 30000"
|
# 2026-08-11 平台聚合:下面这一行取代了原来的 oauth/system/visualdev/flowable/file/
|
# message/permission/visualdata/app 九行。服务名在 config/host-override/discovery.yaml
|
# 里全部别名到 127.0.0.1:30002,Feign 与网关路由一行未改。
|
# 回滚:mvn -P microservice 重新打出九个 jar,把下面注释的九行放回来 + 还原 discovery 表。
|
"jnpf-platform jnpf-platform/jnpf-platform-server/target/jnpf-platform-${VERSION}.jar 30002"
|
# "jnpf-oauth jnpf-oauth/jnpf-oauth-server/target/jnpf-oauth-${VERSION}.jar 30001"
|
# "jnpf-system jnpf-system/jnpf-system-server/target/jnpf-system-${VERSION}.jar 30002"
|
# "jnpf-visualdev jnpf-visualdev/jnpf-visualdev-server/target/jnpf-visualdev-${VERSION}.jar 30003"
|
# "jnpf-flowable jnpf-flowable/jnpf-flowable-server/target/jnpf-flowable-${VERSION}.jar 30004"
|
# "jnpf-file jnpf-file/jnpf-file-server/target/jnpf-file-${VERSION}.jar 30005"
|
# "jnpf-message jnpf-message/jnpf-message-server/target/jnpf-message-${VERSION}.jar 30008"
|
# "jnpf-permission jnpf-permission/jnpf-permission-server/target/jnpf-permission-${VERSION}.jar 30010"
|
# "jnpf-visualdata jnpf-visualdata/jnpf-visualdata-server/target/jnpf-visualdata-${VERSION}.jar 30011"
|
# "jnpf-app jnpf-app/jnpf-app-server/target/jnpf-app-${VERSION}.jar 30012"
|
# 2026-08-11 新增:审计搬入 jnpf-biz-common 后,**宿主栈不起它就等于没有审计能力**——
|
# host-override/discovery.yaml 把服务名 jnpf-audit 解析到 127.0.0.1:30015,
|
# 没有进程监听时 lims 的 audit-sdk 投递会全部失败、落 spool 等重放。
|
# (此前 audit/biz-common 都未登记本表,是 biz-common P1 的 YAGNI 决定;审计搬入后不再成立。)
|
# 放在 jnpf-lims 之前:lims 是审计事件的产生方,被投递方先就绪更顺。
|
"jnpf-biz-common jnpf-biz-common/jnpf-biz-common-server/target/jnpf-biz-common-${VERSION}.jar 30015"
|
"jnpf-eln jnpf-eln/jnpf-eln-server/target/jnpf-eln-${VERSION}.jar 30013"
|
"jnpf-lims jnpf-lims/jnpf-lims-server/target/jnpf-lims-${VERSION}.jar 30019"
|
"jnpf-dms jnpf-dms/jnpf-dms-server/target/jnpf-dms-${VERSION}.jar 30016"
|
)
|
|
# 补丁 #8:宿主栈直连坐标(覆盖 config/shared 里的 compose 默认值)
|
export JNPF_LOG_PROVIDER_URL="${JNPF_LOG_PROVIDER_URL:-dubbo://127.0.0.1:20880}"
|
SERVICE_READY_TIMEOUT="${SERVICE_READY_TIMEOUT:-360}"
|
|
mkdir -p "$PID_DIR"
|
# ⚠️ 变量必须写成 ${VAR} 形式:本行紧跟中文全角括号,而 macOS 自带 bash 3.2 按字节解析,
|
# 会把多字节字符的首字节吞进变量名(报 `JNPF_SERVICE_MAX_HEAP?: unbound variable`),
|
# 配合脚本头部的 set -u 直接终止。裸 $VAR 只有在其后是 ASCII 分隔符时才安全。
|
echo "JVM 单服务最大堆内存:${JNPF_SERVICE_MAX_HEAP}(jnpf-platform 单独用 ${JNPF_PLATFORM_MAX_HEAP})"
|
|
# ── 颜色 ──
|
RED='\033[0;31m'
|
GREEN='\033[0;32m'
|
YELLOW='\033[1;33m'
|
NC='\033[0m'
|
|
# 命令行传入的服务名过滤后的目标列表,与 SERVICES 同结构("name jar port" 字符串数组)。
|
# 默认未指定时由 resolve_target_services 填充为全部服务。
|
TARGET_SERVICES=()
|
# 仅记录当前脚本进程实际新启动的服务;跳过的既有服务不归本次 Ctrl+C 管理。
|
STARTED_SERVICES=()
|
STARTED_SERVICE_COUNT=0
|
|
# 根据传入的服务名列表过滤 SERVICES,结果写到 TARGET_SERVICES。
|
# 空入参 = 全部服务;不存在的服务名直接报错退出。
|
resolve_target_services() {
|
if [[ $# -eq 0 ]]; then
|
TARGET_SERVICES=("${SERVICES[@]}")
|
return 0
|
fi
|
TARGET_SERVICES=()
|
local requested=("$@")
|
for req in "${requested[@]}"; do
|
local matched=""
|
for entry in "${SERVICES[@]}"; do
|
# shellcheck disable=SC2086
|
set -- $entry
|
if [[ "$1" == "$req" ]]; then
|
matched="$entry"
|
break
|
fi
|
done
|
if [[ -z "$matched" ]]; then
|
echo -e "${RED}未知服务名: ${req}${NC}"
|
echo "可用服务:$(printf '%s ' "${SERVICES[@]}" | awk '{for(i=1;i<=NF;i+=3)printf "%s ",$i; print ""}')"
|
exit 1
|
fi
|
TARGET_SERVICES+=("$matched")
|
done
|
}
|
|
# 判断 TARGET_SERVICES 是否包含某个服务名
|
target_contains() {
|
local name=$1
|
for entry in "${TARGET_SERVICES[@]}"; do
|
# shellcheck disable=SC2086
|
set -- $entry
|
[[ "$1" == "$name" ]] && return 0
|
done
|
return 1
|
}
|
|
port_is_listening() {
|
(echo > "/dev/tcp/127.0.0.1/$1") >/dev/null 2>&1
|
}
|
|
port_listener_pid() {
|
if command -v lsof >/dev/null 2>&1; then
|
lsof -nP -tiTCP:"$1" -sTCP:LISTEN 2>/dev/null | head -n 1 || true
|
fi
|
}
|
|
wait_for_port() {
|
local name=$1 port=$2 timeout=${3:-60}
|
local elapsed=0
|
local start_ts=$(date +%s)
|
local pid_file="$PID_DIR/${name}.pid"
|
while (( elapsed < timeout )); do
|
if port_is_listening "$port"; then
|
local cost=$(( $(date +%s) - start_ts ))
|
echo -e " ${GREEN}[就绪]${NC} $name (端口 $port, ${cost}s)"
|
return 0
|
fi
|
if [[ -f "$pid_file" ]] && ! kill -0 "$(cat "$pid_file")" 2>/dev/null; then
|
echo -e " ${RED}[失败]${NC} $name (端口 $port) — 进程已退出,查看 $LOG_DIR/$name/startup.log"
|
return 1
|
fi
|
sleep 2
|
elapsed=$((elapsed + 2))
|
done
|
echo -e " ${YELLOW}[警告]${NC} $name (端口 $port) 在 ${timeout}s 内未就绪"
|
return 1
|
}
|
|
start_service() {
|
local name=$1 jar=$2 port=$3
|
local jar_path="$BASE_DIR/$jar"
|
local pid_file="$PID_DIR/${name}.pid"
|
local log_file="$LOG_DIR/${name}/startup.log"
|
|
if [[ -f "$pid_file" ]]; then
|
if kill -0 "$(cat "$pid_file")" 2>/dev/null; then
|
echo -e " ${YELLOW}[跳过]${NC} $name (端口 $port) — 已在运行 (PID $(cat "$pid_file"))"
|
return 0
|
fi
|
rm -f "$pid_file"
|
fi
|
|
local listener_pid
|
listener_pid=$(port_listener_pid "$port")
|
if [[ -n "$listener_pid" ]] || port_is_listening "$port"; then
|
echo -e " ${RED}[失败]${NC} $name — 端口 $port 已被未登记进程占用${listener_pid:+ (PID $listener_pid)}"
|
return 1
|
fi
|
|
if [[ ! -f "$jar_path" ]]; then
|
echo -e " ${RED}[失败]${NC} $name — JAR 不存在: $jar"
|
return 1
|
fi
|
|
mkdir -p "$(dirname "$log_file")"
|
|
# Dubbo LogProvider 的唯一 provider 固定端口;其余服务保持 -1 不绑。
|
# 2026-08-11:该角色随 jnpf-system-dubboservice 一并并入 jnpf-platform。
|
# 不绑 20880 → 每个请求的 RequestLogAspect 都调用失败。
|
local dubbo_port="-1"
|
[[ "$name" == "jnpf-platform" ]] && dubbo_port="20880"
|
|
# jnpf-platform 用单独的堆上限(九模块同 JVM,见 JNPF_PLATFORM_MAX_HEAP 处注释)
|
local -a jvm_opts=("${JVM_OPTS[@]}")
|
if [[ "$name" == "jnpf-platform" ]]; then
|
jvm_opts=("${JVM_ADD_OPENS[@]}" "-Xmx${JNPF_PLATFORM_MAX_HEAP}" -Dfile.encoding=utf8 -Djava.security.egd=file:/dev/./urandom)
|
fi
|
|
# 前台模式:后台启动子进程但不脱离会话(不用 nohup),脚本 Ctrl+C 时一并退出
|
DUBBO_PROTOCOL_PORT=$dubbo_port java "${jvm_opts[@]}" -jar "$jar_path" > "$log_file" 2>&1 &
|
local pid=$!
|
echo "$pid" > "$pid_file"
|
STARTED_SERVICES+=("$name $jar $port")
|
STARTED_SERVICE_COUNT=$((STARTED_SERVICE_COUNT + 1))
|
|
echo -e " ${GREEN}[启动]${NC} $name (端口 $port, PID $pid) — 日志: $log_file"
|
}
|
|
# 并行等待一批服务就绪,实时打印每个服务的就绪/失败/超时状态
|
# 参数: timeout(整体超时秒数) + 服务条目数组("name jar port")
|
wait_services_ready() {
|
local timeout=$1; shift
|
local -a names ports statuses
|
local n=0
|
for entry in "$@"; do
|
# shellcheck disable=SC2086
|
set -- $entry
|
names[n]=$1
|
ports[n]=$3
|
statuses[n]="pending"
|
n=$((n + 1))
|
done
|
|
local total=$n
|
local start_ts=$(date +%s)
|
local done_cnt=0
|
local elapsed=0
|
|
while (( elapsed < timeout )) && (( done_cnt < total )); do
|
local i=0
|
while (( i < total )); do
|
if [[ "${statuses[i]}" == "pending" ]]; then
|
local name=${names[i]} port=${ports[i]}
|
local pid_file="$PID_DIR/${name}.pid"
|
local pid=""
|
[[ -f "$pid_file" ]] && pid=$(cat "$pid_file")
|
|
if [[ -z "$pid" ]] || ! kill -0 "$pid" 2>/dev/null; then
|
statuses[i]="failed"
|
done_cnt=$((done_cnt + 1))
|
echo -e " ${RED}[失败]${NC} $name (端口 $port) — 进程已退出,查看 $LOG_DIR/$name/startup.log"
|
elif port_is_listening "$port"; then
|
statuses[i]="ready"
|
done_cnt=$((done_cnt + 1))
|
local cost=$(( $(date +%s) - start_ts ))
|
echo -e " ${GREEN}[就绪]${NC} $name (端口 $port, PID $pid, ${cost}s) [${done_cnt}/${total}]"
|
fi
|
fi
|
i=$((i + 1))
|
done
|
|
if (( done_cnt < total )); then
|
sleep 2
|
elapsed=$(( $(date +%s) - start_ts ))
|
fi
|
done
|
|
local ready_cnt=0 failed_cnt=0 timeout_cnt=0
|
local i=0
|
while (( i < total )); do
|
case "${statuses[i]}" in
|
ready) ready_cnt=$((ready_cnt + 1)) ;;
|
failed) failed_cnt=$((failed_cnt + 1)) ;;
|
pending)
|
timeout_cnt=$((timeout_cnt + 1))
|
echo -e " ${YELLOW}[超时]${NC} ${names[i]} (端口 ${ports[i]}) — ${timeout}s 内未监听端口"
|
;;
|
esac
|
i=$((i + 1))
|
done
|
|
echo ""
|
echo -e "── 启动结果: ${GREEN}就绪 ${ready_cnt}${NC} ${RED}失败 ${failed_cnt}${NC} ${YELLOW}超时 ${timeout_cnt}${NC} / 合计 ${total} ──"
|
|
(( failed_cnt == 0 && timeout_cnt == 0 ))
|
}
|
|
# 前台等待:脚本保持不退出,收到 INT/TERM 时统一终止所有子进程
|
foreground_wait() {
|
if (( STARTED_SERVICE_COUNT == 0 )); then
|
echo ""
|
echo -e "${YELLOW}本次没有新启动的服务,脚本结束。${NC}"
|
return 0
|
fi
|
|
echo ""
|
echo -e "${GREEN}所有服务以前台模式运行,按 Ctrl+C 统一停止。${NC}"
|
echo ""
|
|
trap 'foreground_shutdown' INT TERM
|
|
# 仅监控本次启动的子进程;其他终端启动的服务保持不动
|
while true; do
|
local any_alive=false
|
for entry in "${STARTED_SERVICES[@]}"; do
|
# shellcheck disable=SC2086
|
set -- $entry
|
local pid_file="$PID_DIR/$1.pid"
|
if [[ -f "$pid_file" ]] && kill -0 "$(cat "$pid_file")" 2>/dev/null; then
|
any_alive=true
|
break
|
fi
|
done
|
if [[ "$any_alive" != "true" ]]; then
|
echo -e "${YELLOW}所有子进程均已退出,脚本结束。${NC}"
|
break
|
fi
|
sleep 5
|
done
|
}
|
|
foreground_shutdown() {
|
echo ""
|
echo "── 收到中断信号,停止本次启动的服务 ──"
|
stop_started_services
|
echo "========================================"
|
exit 0
|
}
|
|
stop_started_services() {
|
local entry
|
if (( STARTED_SERVICE_COUNT > 0 )); then
|
for entry in "${STARTED_SERVICES[@]}"; do
|
# shellcheck disable=SC2086
|
stop_service $entry
|
done
|
fi
|
STARTED_SERVICES=()
|
STARTED_SERVICE_COUNT=0
|
}
|
|
startup_failed() {
|
echo ""
|
echo -e "${RED}启动未完成,清理本次已启动的服务。${NC}"
|
stop_started_services
|
return 1
|
}
|
|
stop_service() {
|
local name=$1 port=$3
|
local pid_file="$PID_DIR/${name}.pid"
|
|
if [[ ! -f "$pid_file" ]]; then
|
local listener_pid
|
listener_pid=$(port_listener_pid "$port")
|
if [[ -n "$listener_pid" ]] || port_is_listening "$port"; then
|
echo -e " ${YELLOW}[跳过]${NC} $name — 端口 $port 被未登记进程占用${listener_pid:+ (PID $listener_pid)},未自动停止"
|
else
|
echo -e " ${YELLOW}[跳过]${NC} $name — 无 PID 文件"
|
fi
|
return 0
|
fi
|
|
local pid
|
pid=$(cat "$pid_file")
|
|
if kill -0 "$pid" 2>/dev/null; then
|
kill "$pid"
|
echo -e " ${GREEN}[停止]${NC} $name (端口 $port, PID $pid)"
|
else
|
echo -e " ${YELLOW}[跳过]${NC} $name — 进程已不存在 (PID $pid)"
|
fi
|
|
rm -f "$pid_file"
|
}
|
|
show_status() {
|
local name=$1 port=$3
|
local pid_file="$PID_DIR/${name}.pid"
|
|
if [[ -f "$pid_file" ]] && kill -0 "$(cat "$pid_file")" 2>/dev/null; then
|
if port_is_listening "$port"; then
|
echo -e " ${GREEN}[运行中]${NC} $name 端口=$port PID=$(cat "$pid_file")"
|
else
|
echo -e " ${YELLOW}[启动中]${NC} $name 端口=$port PID=$(cat "$pid_file")"
|
fi
|
else
|
if [[ -f "$pid_file" ]]; then rm -f "$pid_file"; fi
|
local listener_pid
|
listener_pid=$(port_listener_pid "$port")
|
if [[ -n "$listener_pid" ]] || port_is_listening "$port"; then
|
echo -e " ${YELLOW}[未登记]${NC} $name 端口=$port${listener_pid:+ PID=$listener_pid}"
|
else
|
echo -e " ${RED}[未运行]${NC} $name 端口=$port"
|
fi
|
fi
|
return 0
|
}
|
|
show_help() {
|
cat <<EOF
|
JNPF 微服务批量启动脚本
|
|
用法: $0 [命令] [选项] [服务名...]
|
|
命令:
|
start 启动服务(默认,前台运行,Ctrl+C 统一停止)
|
stop 停止服务(用于其他终端)
|
restart 停止 → 编译 → 启动(前台)
|
status 查看运行状态
|
help 显示此帮助信息
|
|
选项:
|
--build, -b 启动前先编译项目
|
|
服务名(可选,省略 = 全部):
|
jnpf-gateway jnpf-platform jnpf-biz-common jnpf-lims jnpf-dms
|
|
注:jnpf-biz-common(30015) 含审计能力(2026-08-11 由原 jnpf-audit:30014 搬入)。
|
只起 jnpf-lims 而不起它,审计事件会投递失败并落 spool。
|
|
环境变量:
|
仓库根 .env 启动时自动加载全部 KEY=VALUE(可被命令行同名变量覆盖)
|
SERVICE_READY_TIMEOUT 等待业务服务全部就绪的最长秒数(默认 360)
|
JNPF_SERVICE_MAX_HEAP 单服务最大堆(默认 384m)
|
LOAD_GATE 分波启动负载闸门(默认 9)
|
|
示例:
|
$0 启动全部(缺失的 JAR 自动编译)
|
$0 --build 编译后启动全部
|
$0 restart 全量重启(自动全量编译)
|
$0 stop 停止全部
|
$0 status 查看全部状态
|
|
$0 jnpf-dms 只启动 dms
|
$0 restart jnpf-dms 只重启 dms(仅编译该模块)
|
$0 jnpf-lims 只启动 lims
|
$0 restart jnpf-lims 只重启 lims(仅编译该模块,快很多)
|
$0 stop jnpf-lims 只停 lims
|
$0 status jnpf-lims 只看 lims 状态
|
$0 restart jnpf-lims jnpf-platform 多服务一起重启
|
EOF
|
}
|
|
BUILD=false
|
ACTION=""
|
TARGET_NAMES=()
|
|
while [[ $# -gt 0 ]]; do
|
case "$1" in
|
--build|-b) BUILD=true; shift ;;
|
--help|-h|help) show_help; exit 0 ;;
|
start|stop|restart|status)
|
if [[ -n "$ACTION" ]]; then TARGET_NAMES+=("$1"); else ACTION="$1"; fi
|
shift ;;
|
*)
|
# 第一个非选项位置参数若不是 action 关键字,按服务名收集
|
TARGET_NAMES+=("$1"); shift ;;
|
esac
|
done
|
|
[[ -z "$ACTION" ]] && ACTION="start"
|
# bash 3.2(macOS 自带)+ set -u 下,空数组用 "${arr[@]}" 展开会报 unbound;
|
# 用 "${arr[@]+...}" 这种兼容写法在数组为空时整段不展开。
|
resolve_target_services ${TARGET_NAMES[@]+"${TARGET_NAMES[@]}"}
|
|
build_service_entries() {
|
local modules=()
|
local entry jar
|
for entry in "$@"; do
|
# shellcheck disable=SC2086
|
set -- $entry
|
jar=$2
|
# 把 jnpf-dms/jnpf-dms-server/target/xxx.jar 反推出 Maven 模块坐标。
|
modules+=("$(dirname "$(dirname "$jar")")")
|
done
|
local pl
|
pl=$(IFS=,; echo "${modules[*]}")
|
echo "── 编译项目(仅 ${pl})──"
|
if ! mvn -f "$BASE_DIR/pom.xml" -pl "$pl" -am clean package -DskipTests -q; then
|
echo -e "${RED}编译失败,中止。${NC}"
|
exit 1
|
fi
|
echo -e "${GREEN}编译完成${NC}"
|
}
|
|
do_build() {
|
# 全量启动 → 编译整个项目;指定子集 → 用 -pl/-am 只编译相关模块(快很多)
|
if [[ ${#TARGET_SERVICES[@]} -eq ${#SERVICES[@]} ]]; then
|
echo "── 编译项目(全量)──"
|
if ! mvn -f "$BASE_DIR/pom.xml" clean package -DskipTests -q; then
|
echo -e "${RED}编译失败,中止。${NC}"
|
exit 1
|
fi
|
echo -e "${GREEN}编译完成${NC}"
|
else
|
build_service_entries "${TARGET_SERVICES[@]}"
|
fi
|
}
|
|
# 普通 start 复用现有 JAR,仅为首次检出或新服务自动补编缺失产物。
|
ensure_target_jars() {
|
local missing=()
|
local entry jar name
|
for entry in "${TARGET_SERVICES[@]}"; do
|
# shellcheck disable=SC2086
|
set -- $entry
|
name=$1
|
jar=$2
|
if [[ ! -f "$BASE_DIR/$jar" ]]; then
|
echo -e " ${YELLOW}[缺 JAR]${NC} $name — 将自动编译"
|
missing+=("$entry")
|
fi
|
done
|
if [[ ${#missing[@]} -gt 0 ]]; then
|
build_service_entries "${missing[@]}"
|
fi
|
}
|
|
# ── 分波启动(2026-08-06,对齐 docker/up-staged.sh)──
|
# 全量启动包含 5 个 JVM,其中 4 个业务 JVM 同启会瞬间打满 CPU
|
# (Druid 建连超时风险;容器栈同款问题的宿主版)。
|
# 按依赖重要度分波、波间过负载闸门;指定服务子集(≤4 个)时保持直启不分波。
|
LOAD_GATE="${LOAD_GATE:-9}"
|
|
load_now() {
|
if sysctl -n vm.loadavg >/dev/null 2>&1; then
|
sysctl -n vm.loadavg | awk '{print int($2)}'
|
else
|
awk '{print int($1)}' /proc/loadavg
|
fi
|
}
|
|
wait_load_gate() {
|
local l
|
while :; do
|
l=$(load_now)
|
(( l < LOAD_GATE )) && return 0
|
echo " 负载 ${l} ≥ 闸门 ${LOAD_GATE},等 20s 再放下一波…(可 LOAD_GATE=N 调整)"
|
sleep 20
|
done
|
}
|
|
# 波次成员表:未列出的服务名自动归收尾波(新增服务不用改这里)
|
wave_of() {
|
case "$1" in
|
jnpf-platform) echo 1 ;;
|
jnpf-biz-common) echo 2 ;;
|
jnpf-lims|jnpf-dms) echo 3 ;;
|
*) echo 4 ;;
|
esac
|
}
|
|
# 消费 do_start 的 biz_services(bash 动态作用域),逐波启动并等就绪
|
start_in_waves() {
|
local w entry
|
for w in 1 2 3 4; do
|
local wave_entries=()
|
for entry in "${biz_services[@]}"; do
|
# shellcheck disable=SC2086
|
set -- $entry
|
[[ "$(wave_of "$1")" == "$w" ]] && wave_entries+=("$entry")
|
done
|
[[ ${#wave_entries[@]} -eq 0 ]] && continue
|
echo ""
|
echo "── 波次 ${w}/4: $(printf '%s ' "${wave_entries[@]}" | awk '{for(i=1;i<=NF;i+=3)printf "%s ",$i}')──"
|
wait_load_gate
|
for entry in "${wave_entries[@]}"; do
|
# shellcheck disable=SC2086
|
start_service $entry || return 1
|
done
|
wait_services_ready "$SERVICE_READY_TIMEOUT" "${wave_entries[@]}" || return 1
|
done
|
}
|
|
do_start() {
|
echo "========================================"
|
echo " 启动 JNPF 微服务 (本次 ${#TARGET_SERVICES[@]} 个 / 共 ${#SERVICES[@]} 个)"
|
echo "========================================"
|
|
if [[ ! -f "$BASE_DIR/config/shared/datasource.yaml" ]]; then
|
echo -e "${RED}缺 config/shared/datasource.yaml(补丁 #8 后配置在仓库内),中止启动。${NC}"
|
exit 1
|
fi
|
|
# 阶段 1: 仅在目标包含网关时启动并等待
|
local biz_services=()
|
if target_contains "jnpf-gateway"; then
|
echo "── 阶段 1: 启动网关 ──"
|
for entry in "${TARGET_SERVICES[@]}"; do
|
# shellcheck disable=SC2086
|
set -- $entry
|
if [[ "$1" == "jnpf-gateway" ]]; then
|
# shellcheck disable=SC2086
|
start_service $entry || {
|
startup_failed
|
return 1
|
}
|
wait_for_port "jnpf-gateway" 30000 "$SERVICE_READY_TIMEOUT" || {
|
startup_failed
|
return 1
|
}
|
else
|
biz_services+=("$entry")
|
fi
|
done
|
else
|
biz_services=("${TARGET_SERVICES[@]}")
|
fi
|
|
if [[ ${#biz_services[@]} -gt 0 ]]; then
|
if [[ ${#TARGET_SERVICES[@]} -lt ${#SERVICES[@]} && ${#biz_services[@]} -le 4 ]]; then
|
# 小子集直启(保持既有行为,重启单服务时不受波次拖慢)
|
echo ""
|
echo "── 阶段 2: 启动业务服务 ──"
|
for entry in "${biz_services[@]}"; do
|
# shellcheck disable=SC2086
|
start_service $entry || {
|
startup_failed
|
return 1
|
}
|
done
|
echo ""
|
echo "── 阶段 3: 等待业务服务就绪 (最多 ${SERVICE_READY_TIMEOUT}s) ──"
|
wait_services_ready "$SERVICE_READY_TIMEOUT" "${biz_services[@]}" || {
|
startup_failed
|
return 1
|
}
|
else
|
echo ""
|
echo "── 阶段 2: 分波启动业务服务(负载闸门 ${LOAD_GATE})──"
|
start_in_waves || {
|
startup_failed
|
return 1
|
}
|
fi
|
fi
|
|
echo "========================================"
|
echo " 日志目录: $LOG_DIR/<服务名>/startup.log"
|
echo " 跨终端停止: $0 stop 跨终端查看状态: $0 status"
|
echo "========================================"
|
|
foreground_wait
|
}
|
|
case "$ACTION" in
|
restart)
|
echo "========================================"
|
echo " 重启 JNPF 微服务 (stop → build → start) (本次 ${#TARGET_SERVICES[@]} 个)"
|
echo "========================================"
|
for entry in "${TARGET_SERVICES[@]}"; do
|
# shellcheck disable=SC2086
|
stop_service $entry
|
done
|
do_build
|
do_start
|
;;
|
start)
|
if [[ "$BUILD" == true ]]; then
|
do_build
|
else
|
ensure_target_jars
|
fi
|
do_start
|
;;
|
stop)
|
echo "========================================"
|
echo " 停止 JNPF 微服务 (本次 ${#TARGET_SERVICES[@]} 个)"
|
echo "========================================"
|
for entry in "${TARGET_SERVICES[@]}"; do
|
# shellcheck disable=SC2086
|
stop_service $entry
|
done
|
echo "========================================"
|
;;
|
status)
|
echo "========================================"
|
echo " JNPF 微服务状态 (本次 ${#TARGET_SERVICES[@]} 个)"
|
echo "========================================"
|
for entry in "${TARGET_SERVICES[@]}"; do
|
# shellcheck disable=SC2086
|
show_status $entry
|
done
|
echo "========================================"
|
;;
|
*)
|
echo "未知命令: $ACTION"
|
echo ""
|
show_help
|
exit 1
|
;;
|
esac
|