跟着 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 | 「插件开发范例」分区 1 | GET /hello/demo/host/whoami |
| 插件怎么调另一个插件 | cn.wujies.hello.controller.HelloDemoCrossPluginController | 分区 2 | GET /hello/demo/cross/service?userId=1 |
| 怎么从插件里开放接口给别人用 | cn.wujies.hello.open.HelloDemoOpenController | 分区 3 | GET /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 更省事。
三条只有真做过才知道的事:
@Scheduled在插件里能用,因为宿主替插件子上下文注册了ScheduledAnnotationBeanPostProcessor(PluginRuntimeManager里,源码注释写明 "刻意不注册带@EnableScheduling的@Configuration类")。你不需要自己写@EnableScheduling。@JobExecutor光有注解不会跑 —— 还要在调度中心建一条任务,执行器名必须与@JobExecutor(name=...)完全一致。只把包传上去、不建任务,是不会有任何执行的。 排查"任务没跑"的第一件事是这个,不是权限也不是插件没装。- SnailJob 的日志要用
SnailJobLog.REMOTE—— 普通log.info只进本机文件, 在控制台的「任务日志」里看不到。"控制台里没日志"经常只是因为用了普通 logger。
五、清单 app.json:三个必须写对的地方
以 wujies-hello/app.json 为例:
assets—— 前后端都写。只写backend的话,前端产物不会随包发 (页面上菜单点开是"远程视图不可用")。menus里的按钮(type: "F")必须写进清单。不写的两个后果: ① 全新部署装完没有按钮(前端v-hasPermi过不了); ② 彻底删除也删不掉它们(归属映射里没有,平台不知道它们是这个应用的)。component里的视图键必须与前端注册的 view key 一致(remote:<appCode>/<viewKey>)。 不一致就是悬空菜单 —— 由清单声明,所以全新安装也会照建, 是最难发现的一类坏数据。hello自己历史上就踩过:原来写remote:hello/hello/list, 而前端从来没有注册过hello/list。
顺带一条容易忽略的:
hello的前端工程里还有一份app.json(frontend/hello-app/app.json)。它是一个近重复,且曾经与插件清单漂移过 (version0.1.0vs1.0.0、少 F 按钮)。改清单时两份一起改, 否则就会出现"装完版本号对不上 / 按钮没有"这类现象。
六、为什么把示例做成一个应用
早期是拆成两个的:hello(后端插件)+ demo-app(纯前端远程应用)。
两者已合并成 hello 一个应用,并且 demo-app 已经从应用目录里去掉
(生成器 EXCLUDED_APPS + 存量清理脚本 app_market_demo_app_remove.sql)。理由是:
- 一次安装看完链路 —— 装一个应用就能看到前端页面 + 后端接口 + 开放接口 + 定时任务;
assets本来就支持同时写backend与frontend,没有特例;- 拆两个会让学习者先踩"跨应用依赖"这个额外的坑;
- 讲"怎么开放接口"时,
app.json声明、controller、前端调用能在同一处对照; - 纯前端应用的范例已经有
finance,不需要再占一个示例位置。