本地联调
这一页解决一个问题:改了一行插件代码(或远程应用前端代码),怎么在真实宿主里最快看到它。 三条链路互相独立,先按「你改了什么」对号入座,再只看对应那一节。
| 你改了什么 | 走哪条链路 | 生效方式 |
|---|---|---|
插件后端 Java(cn.wujies.<app>.*) | 链路 1 | 沙箱里即时;真实宿主里要重打 jar,热加载或重启 |
插件前端(wujies/wujies-plugins/frontend/<code>-app) | 链路 2 | 重新构建产物 → 刷新浏览器 |
宿主前端(plus-ui/src/**、应用市场运行时) | 链路 3 | Vite 热更新 |
契约与 Mapper XML 都在插件工程自己的 src/ 下(平台里没有 *-api 契约模块),
所以三条链路都不需要碰平台代码 —— 只有改了宿主自己的代码时才要重建 wujies-admin.jar。
一、前置(一次性)
三个仓库,没有共同 git 根,各自提交:
| 目录 | 是什么 | 判据 |
|---|---|---|
wujies-platform/ | 后端(Java / Spring Boot)+ SQL | 有 pom.xml、wujies-admin/target/wujies-admin.jar |
wujies/wujies-plugins/ | 插件本体 + 插件前端(pnpm workspace)+ 打包器 + sandbox/ | 根 pom.xml 的 <modules> 里是 wujies-<app> |
plus-ui/ | 宿主前端(Vue 3 / Vite) | 它的 package.json 里 name 写的是 wujies-admin,别被名字骗了 |
# 1) 平台构件先装进本地仓库 —— 插件以已发布构件的形式依赖平台(cn.wujies:*:5.6.2)
cd wujies-platform
& $mvn -B install -DskipTests
# 2) 插件前端的构建期依赖(一次即可)
cd ..\wujies\wujies-plugins
pnpm install| 项 | 值 |
|---|---|
| 后端端口 | 8086(wujies-admin/src/main/resources/application.yml 的 server.port) |
| 前端 dev server | 80(plus-ui/.env.development 的 VITE_APP_PORT),/dev-api 代理到 http://127.0.0.1:8086 |
| 沙箱端口 | 18080 |
| 默认账号 | admin / admin123,租户 000000 |
app-market.deploy 下两个路径都是相对路径,见 wujies-admin/src/main/resources/application.yml:
plugin-dir: plugins-dist
frontend-dir: ../wujies-admin/app-market-dist/remote-apps在别的目录启动,插件 jar 与远程应用前端产物会落到错误的位置 —— 表现是「装成功了但功能打不开」。
新起一个宿主实例(手工命令,-D 参数的引号不能省 —— PowerShell 会把 -Dloader.path=x 拆成 /path=x):
cd wujies-platform
java "-Dloader.path=plugins-dist" "-Dspring.profiles.active=dev" -jar "wujies-admin\target\wujies-admin.jar"脚本在 script/prune/ 下,而它用 $root = Split-Path $PSScriptRoot -Parent 推工程根,
推出来的是 wujies-platform/script,不是仓库根 —— 于是它在 script\wujies-admin\target\... 找不到 jar,
转去打包时又因为那个目录没有 pom.xml 而失败,最后以「打包失败,已中止启动」退出。
同目录的 prune-plugins.ps1 单独跑也有同类问题:不带 -PluginDir 时它去 script\plugins-dist 找,
那里不存在,于是它静默什么都不做(打印「目录不存在,无需清理」)。
- 启动宿主:用上面的手工命令;
- 清残留 jar:在
wujies-platform下执行powershell -NoProfile -ExecutionPolicy Bypass -File script\prune\prune-plugins.ps1 -PluginDir plugins-dist(先加-DryRun看它准备删什么)。
这两条错位记在 wujies-platform/docs/开发问题与解决方案.md 的遗留清单里,状态是「未改」;
脚本一旦修好,这一条就可以删掉。
app-market.plugin-runtime.enabled 默认 true(每插件独立类加载器)。要退回旧的兼容机制,
必须用 JVM 参数,因为启动器在 Spring 之前执行、读不到 yml:
"-Dapp-market.plugin-runtime.enabled=false"。只在 yml 里改的表现是「插件完全不加载」。
二、链路 1:插件后端(Java)
2.1 最快回路:沙箱(H2,不连 MySQL / Redis / RabbitMQ)
wujies-plugins/sandbox 是一个独立的 Spring Boot 应用,它把一个插件 jar 挂起来跑,
用来在还没有完整平台环境时看自己的接口。
cd wujies\wujies-plugins
powershell -NoProfile -ExecutionPolicy Bypass -File sandbox\run.ps1 -Plugin wujies-hello脚本会先 mvn -pl wujies-hello,sandbox package -DskipTests(-SkipBuild 可跳过),
再用扁平 classpath 启动,端口 18080:
| 地址 | 作用 |
|---|---|
http://localhost:18080/sandbox | 自检:插件装上没有、挂了几条路由 |
http://localhost:18080/sandbox/beans | 插件上下文全部 bean —— 排查「注入不进来」最有用 |
http://localhost:18080/h2-console | 直接查沙箱内存库(用户 sa,空密码) |
插件的 migrations/*.sql 会在沙箱里按文件名顺序自动执行(MySQL 方言会被改写成 H2 能跑的语句)。
-ApiPath 是给已废弃的 *-api 契约模块准备的,当前用不到;插件自己的类都在插件 jar 里。
它不做安装、签名、菜单同步、迁移记账;它的 H2 schema 是开发用最小集;
它刻意复现「宿主看不到插件」这个边界(沙箱里不能用注入拿插件服务)。
能力与已知限制见 wujies-plugins/sandbox/README.md;边界场景(鉴权链、租户、跨插件、SnailJob)
必须回到真实宿主验。
2.2 真实宿主:重新打 jar
插件不在平台 reactor 里,构建在插件仓库里做:
cd wujies\wujies-plugins
# 只构建一个插件(-am 会一并构建它依赖的兄弟插件构件,例如 hello 依赖 wujies-member-plugin)
& $mvn -B -pl wujies-hello -am package -DskipTests产物是 wujies-hello/target/wujies-hello-plugin-1.0.0.jar。
2.3 让真实宿主用上新 jar(两种)
| 方式 | 怎么做 | 生效时机 |
|---|---|---|
| 走市场(推荐) | .\package.ps1 -Plugin wujies-hello -SigningKey keys\private.pem → 上传版本 → 控制台「部署」或「我的应用」里升级 | 热加载,不重启(响应里 hotLoaded: true) |
| 本地偷懒 | 把新 jar 覆盖 wujies-platform/plugins-dist/ 下的同名文件,然后重启宿主 | 启动时加载 |
判断依据是 plugins-dist/active-plugins.txt(每行 appCode=jar 文件名):
- 运行期(启动加载与热加载都走
PluginRuntimeManager)以它为准:同一个appCode名下有多个 jar 时, 只加载清单里登记的那个;不在清单、也不属于任何已登记应用的 jar 被当作「手工放置」, 按文件名推导appCode加载(该appCode已有生效版本时不覆盖)。 - 启动期那条
loader.path(PluginClassPathLoader,Spring 之前执行)现在也只挂清单里的 jar, 其余会打印「跳过 N 个不在 active-plugins.txt 里的 jar(刻意不挂载)」;清单文件缺失/为空时退回「全挂」。
所以:覆盖同名文件最省事,清单不用改;换文件名就必须同步改 active-plugins.txt,
否则容易出现「看着有 jar、其实没生效」。
⚠️ 手工放进去的 jar 只解决「后端产物在不在」:它没有安装记录,也不会建菜单
(菜单只在安装/升级时按 app.json 幂等 upsert)。菜单要出现,还是得走一次安装或升级。
# 重启前清掉同一 appCode 的旧版本 jar(按 active-plugins.txt,只删不在用的那些)
cd wujies-platform
powershell -NoProfile -ExecutionPolicy Bypass -File script\prune\prune-plugins.ps1 -PluginDir plugins-dist- jar 真的重建了吗(看
target/里的时间戳,别只看mvn成功); - 覆盖/落位的是当前生效那个文件名吗(对照
active-plugins.txt); - 需要重启的改动做的是「重启」而不是「部署」吗。
maven-compiler-plugin 会先清空输出目录再全编,正在运行、按需懒解析类的 JVM 会抛 NoClassDefFoundError。
2.4 范例应用 wujies-hello 的接口阶梯
后端装载问题按这个顺序打,能一步分开「装载」还是「数据」:
| 顺序 | 接口 | 验什么 | 期望 |
|---|---|---|---|
| 1 | GET /hello/greet?name=无界 | 路由挂上了、依赖注入通了(不碰库) | code=200 |
| 2 | GET /hello/item/list | 建表脚本执行了、Mapper 装配了、SQL 真打到库上 | code=200,2 条种子数据 |
| 3 | GET /hello/demo/host/config、/whoami | 插件调用宿主系统服务(ConfigService / OssService / LoginHelper) | code=200 |
| 4 | GET /hello/demo/cross/mapper?id=1、/cross/service?userId=1 | 跨插件调用:直接注入兄弟 Mapper vs 走 SDK 定位器 | 会员没装时 /service 仍 200,返回 memberAppAvailable=false |
| 5 | 匿名 GET /open/hello/greet | 从插件里开放 /open 接口 | 200(不带 token) |
| 6 | GET /open/hello/config | 站点配置由宿主的泛化端点读出(插件不用自己写接口) | 200,settings 里是 hello.* 的值 |
| 7 | 插件里的两个定时任务 | @Scheduled(cron 在代码里)与 SnailJob @JobExecutor(cron 在调度中心) | 前者看日志,后者要在调度中心建任务 |
/hello/greet 与 /hello/item/list 通、/open/** 通、跨插件那两条按「装没装会员」给不同结果 ——
这七条的真实源码与逐行解释见 跟着 hello 读源码。
三、链路 2:插件前端(远程 ESM)
远程应用是独立构建的 ESM 产物,跑在宿主同源上下文里。两个环境变量决定它能不能被 dev server 提供:
cd wujies\wujies-plugins
# 宿主仓库根(含 package.json、src/、src/utils、src/enums 的那一层)
$env:WUJIES_UI_DIR = 'C:\Users\TX\Desktop\ruoyi\plus-ui'
# 产物落点:宿主的 app-market-dist\remote-apps(工厂会自动追加 <appCode> 子目录)
$env:WUJIES_FRONTEND_DIST = 'C:\Users\TX\Desktop\ruoyi\plus-ui\app-market-dist\remote-apps'
pnpm --filter ./frontend/hello-app run build| 项 | 说明 |
|---|---|
| 产物目录 | <宿主根>/app-market-dist/remote-apps/<appCode>/,目录名必须等于 appCode |
| 入口文件 | app.json 的 assets.frontend 的文件名(hello 是 index.js) |
不设 WUJIES_FRONTEND_DIST | 产物落在工程自己的 dist/,dev server 取不到 |
不设 WUJIES_UI_DIR | 会沿祖先目录做结构性探测;探测不到时明确报错并告诉你该设什么 |
| 生效方式 | dev server 直接读磁盘(响应带 Cache-Control: no-cache),刷新浏览器即可 |
发布前跑一遍守门脚本(都在 plus-ui 下):
cd plus-ui
npm run check:remote # 组件能不能解析 + 产物里有没有 process.env 残留
npm run check:globals # 宿主自动导入宇宙里的名字有没有变成裸全局
npm run check:runtime # 运行时垫片的导出面覆盖不覆盖产物里的 bare import- 后端落位到
app-market.deploy.frontend-dir,按wujies-platform/工作目录解析,是工作区顶层的wujies-admin/app-market-dist/remote-apps/<appCode>/; - 宿主 dev server 提供
/remote-apps/**时读的是plus-ui/app-market-dist/(plus-ui/vite/plugins/appMarketStatic.ts的sourceDir,不可配置)。
自己构建的产物要落到后者才会被 dev server 提供(就是上面那条 WUJIES_FRONTEND_DIST)。
两边对不上的现象是菜单点开「远程视图不可用」,控制台报
Failed to fetch dynamically imported module: /remote-apps/<code>/index.js。
要么把 application.yml 的 frontend-dir 改成 ../plus-ui/app-market-dist/remote-apps
(改完必须重建 wujies-admin.jar —— yml 在 jar 里),要么把 wujies-admin/app-market-dist/remote-apps
下的产物手工同步到 plus-ui/app-market-dist/remote-apps。
3.1 让宿主知道「去哪拿这份 ESM」
正常路径是运行期问服务端:登录后 GET /app/runtime/remote-apps 返回本租户已启用应用的入口,
形如 {"appCode":"hello","entry":"/remote-apps/hello/index.js?v=1.0.0"}(带 ?v= 是为了升级后不吃旧缓存)。
纯后端应用(清单没写 assets.frontend)不会出现在里面,这是正常的。
临时指向别的产物时,用静态清单(优先级更高):
VITE_APP_MARKET_REMOTE_APPS = hello=/remote-apps/hello/index.js格式是 code=url,code2=url2;只写 url 时只能预加载、不能按需装载。写进 plus-ui/.env.development
后要重启 dev server(环境变量在启动时读取)。开发环境另有内置的 demo-app / gallery 两条静态声明
(其中 demo-app 已从应用目录去掉,那条已失效)。
解析器与优先级见 plus-ui/src/plugins/appmarket/bootstrap.ts。
四、链路 3:宿主前端壳
cd plus-ui
npm run dev # 80 端口;predev 会先跑 gen:runtime 生成运行时垫片改了 plus-ui/src/**(宿主自己的页面、应用市场运行时、菜单与权限)走常规热更新即可。
| 什么时候必须重建,而不是只靠热更新 | 原因 |
|---|---|
改了 scripts/gen-runtime-shims.mjs 的 staticNames,或 src/plugins/appmarket/runtime.ts 里对应 slot 挂的名字 | 垫片的导出面要覆盖远程产物真正 import 的名字,改一处要同时改两处 |
| 改了插件前端工程的构建配置 / 组件白名单 | 远程产物是预构建的,app-market-dist/ 不进版本库 |
改了 plus-ui/src/plugins/appmarket/** 的加载逻辑 | 用 npm run test:market 跑一遍单测 |
五、怎么确认插件真的加载了
后端侧(从上到下,越往下越接近「业务真的通了」):
| 看什么 | 通过的样子 | 不通过说明 |
|---|---|---|
| 启动日志 | 插件加载完成,已挂载 N 个,接口 M 个;每个插件一行 插件 <code> 已加载:组件 a 个,接口 b 个,Mapper c 个,类加载器 ... | 没有该应用那行 → jar 没落位,或不在 active-plugins.txt 里 |
| 部署响应 | hotLoaded: true,hotLoadNote 就是上面那句摘要 | hotLoaded: false 且 restartRequired: true → 这次没热加载成功(产物已落位,重启会生效);staleJars 非空 → 旧 jar 删不掉,看同响应里的 warning |
| 任务中心 | 「我的应用 → 应用任务」里每步与日志(下载校验 → 落位 → 迁移脚本 → 后端产物) | 失败原因先看这里,比翻服务端日志快 |
| 直接打接口 | /hello/greet 200,/hello/item/list 200(见 2.4 的阶梯) | 两条都不通 = 装载侧;只有第二条不通 = 数据侧(建表脚本、表名、Mapper) |
| 鉴权链 | 不带 token 打 /hello/greet 返回 {"code":401} | 返回 200 说明插件路由没挂上宿主拦截器链 —— 那是平台侧的问题,不是「我的插件没问题」 |
| 开放接口 | 匿名 /open/hello/greet 200 | 401 说明宿主类上的 @SaIgnore 被去掉,或 open-api.tenant.path-patterns 里的 /open/** 被删了 |
| 接口在应用停用后 | 返回 404 且 body 里的业务码是 4610 | 这是「应用不可用」的刻意提示,不是 bug |
前端侧:
| 看什么 | 通过的样子 |
|---|---|
GET /app/runtime/remote-apps | 有该应用的 {appCode, version, entry},entry 带 ?v=<版本号> |
| 浏览器控制台 | [app-market] 远程应用入口(后端): hello, ... |
| 菜单 | 安装/升级/启用/停用/卸载后前端菜单树会刷新(plugins/appmarket/menuRefresh.ts);看不到变化先手动刷新页面 |
六、常见现象对照
| 现象 | 多半是 |
|---|---|
菜单点开「远程视图不可用: <viewKey>(应用 X 未安装或入口地址未配置)」 | 后端清单里没有该应用(没装 / 被停用),或静态清单里没声明 |
控制台 Failed to fetch dynamically imported module: /remote-apps/<code>/index.js | 产物不在 dev server 读的目录(WUJIES_FRONTEND_DIST 没设/指错),或目录名与 appCode 不一致 |
| 页面能开但整站没样式 | 工程 src/index.ts 的 styles 路径与产物 CSS 文件名不一致 —— 跑 npm run check:remote |
装载即 ReferenceError: useStorage is not defined | 工程 AutoImport 的 imports 没与宿主逐项一致 —— 跑 npm run check:globals |
SyntaxError: ... does not provide an export named 'globalHeaders' | 宿主运行时垫片少了这个具名导出(要同时改 gen-runtime-shims.mjs 与 runtime.ts)—— 跑 npm run check:runtime |
装载即 ReferenceError: process is not defined | 产物里残留 process.env(每个工程都要 define: { 'process.env.NODE_ENV': ... }) |
| 页面渲染了但没有侧边栏和顶栏 | 远程清单里声明了 routes:它会被注册成顶级路由、顶掉 Layout —— 清单不要写 routes |
| 按钮变成纯文字 | 宿主组件没进该工程的 HOST_SDK_COMPONENTS 白名单;加之前先确认它在宿主 SHARED_COMPONENTS 里 |
启动即 Result Maps collection already contains value | plugins-dist 里同一 appCode 有两个版本的 jar(两个都在清单里时才会同时挂上)—— 清残留后再启动 |
Invalid bound statement (not found): ...selectList | 自定义 SQL 的 Mapper XML 没随插件 jar 走(XML 由 PluginMapperRegistrar 在插件加载时注册,不要再往平台模块里放) |
| A 应用被「彻底删除」后,B 应用或宿主接口一查 A 的表就报「表不存在」 | 表随 A 的包走、也随 A 的卸载删掉;替别人兜底的宿主侧代码要先判「那个插件装了没」,把「没装」当正常状态 |
| 后端起不来且报缺类 | 平台依赖没打成 provided(平台类由宿主类加载器提供),或被 loader.path 同时挂了两份 |
| 改了后端代码没生效 | 只跑了 mvn compile:它不会更新 wujies-admin.jar,跑的仍是旧代码 |
| 改了插件代码没生效 | 见 2.3 的三件事(jar 时间戳、生效文件名、重启方式) |
更多按症状查的入口:故障排查;任务步骤与日志的读法:应用任务中心。
相关阅读
- 本地开发与联调方案 —— 本页的补充:对标 Halo / NiuCloud 的机制,给出「远程宿主 + 本地代码」四条路径与一条命令的自动化脚本
- 后端工程 —— 插件模块结构、
provided依赖、Mapper 与 XML 的位置 - 前端远程应用 —— 构建配置工厂、组件白名单、运行时垫片
- 跟着 hello 读源码 —— 四个演示点的真实源码
- 应用清单
app.json· 数据库迁移 · 种子数据与站点配置 - 必须遵守的不变量 —— 类可见 vs bean 可见、两套加载机制、拦截器链
- 15 分钟跑起来 —— 从零把平台和后端跑起来