#!/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 <}:${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 </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