Wujies 无界平台
插件开发

跟着 hello 读源码

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

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

这一页不是教程,是索引:把"我该照哪个文件写"这个问题,指向一个真实可运行的文件。

范例应用:wujies-plugins/wujies-hello(后端插件)+ wujies-plugins/frontend/hello-app(前端页面)。 装一个 hello,前后端 + 演示页面一起到位 —— 这是刻意设计的(见下面"为什么合并成一个应用")。

为什么值得先读它

文档讲的是"应该怎么做",而 hello 是"已经这么做并且能跑"的最小实例。 两者的差别在细节上:注解写在哪一行、方法签名长什么样、pom 里的 scope 是哪个 —— 这些抄错一个就跑不起来,而文档通常不会把每个字节都写出来。

因此本页的用法是两栏对照:左边是你脑子里的问题,右边是可以直接打开的文件。


一、五个演示点:问题 → 源码 → 页面

你想知道后端源码前端页面打什么端点验证
插件怎么调宿主的系统服务cn.wujies.hello.controller.HelloDemoHostController「插件开发范例」分区 1GET /hello/demo/host/whoami
插件怎么调另一个插件cn.wujies.hello.controller.HelloDemoCrossPluginController分区 2GET /hello/demo/cross/service?userId=1
怎么从插件里开放接口给别人用cn.wujies.hello.open.HelloDemoOpenController分区 3GET /open/hello/greet
定时任务怎么写(两种写法)cn.wujies.hello.task.HelloDemoScheduledTask / cn.wujies.hello.task.HelloDemoSnailJobTask分区 4(无端点,看源码)服务端日志 / SnailJob 控制台
清单 app.json 怎么写wujies-hello/app.json—菜单能否点开
表结构与种子数据怎么随包走wujies-hello/migrations/01..03-*.sql—装完列表里有数据

前端页面在**「Hello 示例 → 插件开发范例」**这个菜单下。


二、重点:怎么从一个插件里把接口开放出去

这是被问得最多的一个问题:接口写在插件里、宿主管不着它,那 C 端 / 第三方怎么调?

答案:写一个 /open/<app>/** 的控制器,宿主一行都不用改。四个动作:

#做什么为什么 / 谁负责
1控制器放 /open/<你的app>/**,类上加 @SaIgnore@SaIgnore 告诉 Sa-Token「这条不要登录态」;/open/** 前缀本身也在宿主 security.excludes 白名单里(双保险:只看代码也能一眼看出它是公开接口)
2什么都不用注册⚠️ 插件不需要登记路由清单。宿主的 AllUrlHandler 会同时收集「宿主 mapping + 插件 mapping」,PluginRuntimeManager 在 load / unload 之后调 refresh() —— 装了就进清单、卸了就出清单
3租户自动就有宿主 open-api.tenant.pathPatterns 的默认值就含 /open/**,按请求头把租户设进上下文。不设的话,带租户过滤的表会查不到数据
4站点配置不用自己写接口把 hello.* 写进 sys_config(config_category = 'hello',见 migrations/03-*.sql),宿主的泛化端点 GET /open/<code>/config 立刻能读 —— 宿主零改动,你之后新加的应用也不用改宿主

第 4 条值得多说一句

GET /open/<code>/config 是宿主的一个泛化端点(/open/{code}/config), 它只认 config_category,不认识任何具体应用。

所以"新应用要有一个公开的站点配置接口"这件事,你什么都不用做: 写好 sys_config 里的行,它就自动可用。返回里 8 个基础字段 (logo / favicon / name / nameEn / backgroundImage / description / copyright / icp)各占一个属性,其余 hello.* 全部原样放进 settings 透传给前端 —— 前端各页面自己决定读哪些开关,宿主完全不需要认识它们。

⚠️ 反面教材:HelloDemoOpenController 里有一个 GET /open/hello/config-via-plugin 端点,它把同样的数据用"插件自己读 sys_config 再包一层"的方式又实现了一遍。 别照它写 —— 它的存在只是为了让你看到"这件事宿主已经替所有应用做完了"。

验证(三条,逐级定位)

# ① 匿名可访问?(走 /open/ 不带 token)
curl -i http://<host>/open/hello/greet?name=test

# ② 宿主的泛化端点能读到你的配置?(站点配置走这条)
curl -s http://<host>/open/hello/config

# ③ 对照:管理端接口应当**要求**登录态
curl -i http://<host>/hello/item/list

① 返回 401 而不是 200 → 检查 @SaIgnore 是否漏了、或白名单是否被改动。 ② 返回空对象 → 检查 sys_config 里有没有 config_category = 'hello' 的行 (migrations/03-*.sql 是否执行过;迁移记录见 app_schema_history)。


三、最容易做错的一处:调用宿主 vs 跨插件调用

这两件事方向不同、做法也不同,混起来一定会写出"注入了却注入不进来"的代码。

调用宿主(DemoHostController):插件是宿主的子上下文, 所以插件能注入宿主 bean,反之不行。直接用宿主的能力接口就行 —— 比如 ConfigService / OssService(都在 wujies-common-core)/ LoginHelper。

跨插件调用(DemoCrossPluginController):这里有个看着矛盾的表述,两句都对:

不变式说「插件只能看见父上下文(宿主),看不见兄弟插件」 契约规则说「插件之间互相要对方的契约时,依赖兄弟插件构件」

因为说的不是同一层:

层面能不能跨插件为什么
类(编译期 / 类加载期)能PluginClassLoader.findClass 在"从自己的 URL 定义"之前,先按包名把类委托给拥有它的那个插件加载器(cn.wujies.member.* 一律交给 member 的加载器)。所以 JVM 里同一个 FQCN 只有一个 Class 对象,instanceof 成立
Spring bean(运行期)不能每个插件有自己的子上下文(parent 是宿主)。你的上下文里没有兄弟的 bean

⇒ 所以"依赖兄弟插件构件"买到的是类型可见,不是 bean 可见。落地成两条规则:

要用的东西能直接注入吗怎么做
兄弟的 Mapper 接口✅ 能PluginMapperRegistrar 把它注册进宿主的 bean factory ⇒ 宿主 bean 对子上下文可见
兄弟的 Service bean❌ 不能用 SDK 的 XxxServiceLocator(每次调用都反查,装卸/升级都跟得上),并且先 available(...) 判在不在
兄弟的 DTO / event 类—(不是 bean)provided 依赖引用类型即可

什么时候用哪种:可选依赖 → 必须用定位器 + available 降级; 强依赖 → 也用定位器,但让它的「请到应用市场安装 XX」抛出去(比 NPE 清楚得多); 只想读对方数据 → 直接注入 Mapper 最省事,但要意识到你从此隐式依赖对方的数据表。


四、定时任务:两种写法,怎么选

hello 两个都写了,因为两种都是真实存在的做法(不是二选一的历史遗留):

@Scheduled(DemoScheduledTask)SnailJob @JobExecutor(DemoSnailJobTask)
cron 写在哪代码里控制台里(/snail-job → 任务管理)
改了 cron 要重启吗要不要
执行历史 / 重试 / 告警只有日志有(控制台能看到每次执行)
多实例部署每台都跑(要自己加分布式锁)由调度中心分片 / 路由
额外依赖不需要需要 SnailJob 客户端

经验法则:这个任务出错了要不要有人知道? 要 → SnailJob;只是清理/预热、失败重跑就行 → @Scheduled 更省事。

三条只有真做过才知道的事:

  1. @Scheduled 在插件里能用,因为宿主替插件子上下文注册了 ScheduledAnnotationBeanPostProcessor(PluginRuntimeManager 里,源码注释写明 "刻意不注册带 @EnableScheduling 的 @Configuration 类")。你不需要自己写 @EnableScheduling。
  2. @JobExecutor 光有注解不会跑 —— 还要在调度中心建一条任务,执行器名必须与 @JobExecutor(name=...) 完全一致。只把包传上去、不建任务,是不会有任何执行的。 排查"任务没跑"的第一件事是这个,不是权限也不是插件没装。
  3. SnailJob 的日志要用 SnailJobLog.REMOTE —— 普通 log.info 只进本机文件, 在控制台的「任务日志」里看不到。"控制台里没日志"经常只是因为用了普通 logger。

五、清单 app.json:三个必须写对的地方

以 wujies-hello/app.json 为例:

  1. assets —— 前后端都写。只写 backend 的话,前端产物不会随包发 (页面上菜单点开是"远程视图不可用")。
  2. menus 里的按钮(type: "F")必须写进清单。不写的两个后果: ① 全新部署装完没有按钮(前端 v-hasPermi 过不了); ② 彻底删除也删不掉它们(归属映射里没有,平台不知道它们是这个应用的)。
  3. component 里的视图键必须与前端注册的 view key 一致(remote:<appCode>/<viewKey>)。 不一致就是悬空菜单 —— 由清单声明,所以全新安装也会照建, 是最难发现的一类坏数据。hello 自己历史上就踩过:原来写 remote:hello/hello/list, 而前端从来没有注册过 hello/list。

顺带一条容易忽略的:hello 的前端工程里还有一份 app.json (frontend/hello-app/app.json)。它是一个近重复,且曾经与插件清单漂移过 (version 0.1.0 vs 1.0.0、少 F 按钮)。改清单时两份一起改, 否则就会出现"装完版本号对不上 / 按钮没有"这类现象。


六、为什么把示例做成一个应用

早期是拆成两个的:hello(后端插件)+ demo-app(纯前端远程应用)。 两者已合并成 hello 一个应用,并且 demo-app 已经从应用目录里去掉 (生成器 EXCLUDED_APPS + 存量清理脚本 app_market_demo_app_remove.sql)。理由是:

  1. 一次安装看完链路 —— 装一个应用就能看到前端页面 + 后端接口 + 开放接口 + 定时任务;
  2. assets 本来就支持同时写 backend 与 frontend,没有特例;
  3. 拆两个会让学习者先踩"跨应用依赖"这个额外的坑;
  4. 讲"怎么开放接口"时,app.json 声明、controller、前端调用能在同一处对照;
  5. 纯前端应用的范例已经有 finance,不需要再占一个示例位置。

相关阅读

目录