Wujies 无界平台
插件开发后端工程

本地联调

本文是 1.x 的归档文档(冻结于 1.x)。该分支只接收勘误修正, 内容基本不再变动;文中的源码路径、配置项与版本号均以 1.x 为准,不建议用于新项目。

新项目请查看 5.x 最新文档。已在 1.x 上运行的项目,本文仍是可靠依据。

这一页解决一个问题:改了一行插件代码(或远程应用前端代码),怎么在真实宿主里最快看到它。 三条链路互相独立,先按「你改了什么」对号入座,再只看对应那一节。

你改了什么走哪条链路生效方式
插件后端 Java(cn.wujies.<app>.*)链路 1沙箱里即时;真实宿主里要重打 jar,热加载或重启
插件前端(wujies/wujies-plugins/frontend/<code>-app)链路 2重新构建产物 → 刷新浏览器
宿主前端(plus-ui/src/**、应用市场运行时)链路 3Vite 热更新

契约与 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 server80(plus-ui/.env.development 的 VITE_APP_PORT),/dev-api 代理到 http://127.0.0.1:8086
沙箱端口18080
默认账号admin / admin123,租户 000000
后端进程的工作目录必须是 wujies-platform/

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/start-host.ps1 现在会把工程根算错,先别直接跑

脚本在 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
改了插件代码却在宿主里没生效,先查三件事
  1. jar 真的重建了吗(看 target/ 里的时间戳,别只看 mvn 成功);
  2. 覆盖/落位的是当前生效那个文件名吗(对照 active-plugins.txt);
  3. 需要重启的改动做的是「重启」而不是「部署」吗。
别在别人正在跑的 JVM 上执行 mvn install

maven-compiler-plugin 会先清空输出目录再全编,正在运行、按需懒解析类的 JVM 会抛 NoClassDefFoundError。

2.4 范例应用 wujies-hello 的接口阶梯

后端装载问题按这个顺序打,能一步分开「装载」还是「数据」:

顺序接口验什么期望
1GET /hello/greet?name=无界路由挂上了、依赖注入通了(不碰库)code=200
2GET /hello/item/list建表脚本执行了、Mapper 装配了、SQL 真打到库上code=200,2 条种子数据
3GET /hello/demo/host/config、/whoami插件调用宿主系统服务(ConfigService / OssService / LoginHelper)code=200
4GET /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)
6GET /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
frontend-dir 与 dev server 读的目录不是同一处
  • 后端落位到 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。

想让「从市场装的应用」也在 dev 下直接可看

要么把 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 200401 说明宿主类上的 @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 valueplugins-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 时间戳、生效文件名、重启方式)

更多按症状查的入口:故障排查;任务步骤与日志的读法:应用任务中心。


相关阅读

目录