# 动态服务
动态服务用于把一段动态逻辑发布成 HTTP 接口,供前端页面、移动端或第三方系统调用。服务的逻辑在运行时定义和修改,无需重新编译和部署。
升级注意(1.0.0-beta18)
beta18(#768)起,Enable Anonymous(enableAnonymous)为关闭或未设置的动态服务会拒绝未登录的请求,返回 errorCode: 11004(AnonymousInvocation)。
此前这个开关约一年没有生效(2025-09 平台改用匿名身份对象表示未登录用户之后,判断 auth == null 一直不成立),未登录也能调用任意动态服务。所以升级到 beta18 后,一直被匿名调用的服务会突然失败。
升级前请逐个排查:
- 确实需要匿名访问的服务(例如公开页面调用的接口),把
Enable Anonymous打开; - 其余服务的调用方改为携带 token 调用,见 调用方式与认证。
对开启了 Enable Log 的服务,可以在数据库中用下面的 SQL 找出曾被匿名调用、升级后会被拒绝的服务(没有开启 Enable Log 的服务不会留下执行记录,需要从调用方排查):
select s.name, s.enable_anonymous, count(*) as anonymous_calls, max(r.start_time) as last_call
from dynamic_service_exec_record r
join dynamic_service s on s.id = r.provider_id
where coalesce(s.enable_anonymous, false) = false
and (r.exec_auth is null or r.exec_auth::text like '%Anonymous%')
group by 1, 2 order by 3 desc;
2
3
4
5
6
注意:被拒绝时 HTTP 状态码当前是 500(不是 403),客户端请按响应体中的 errorCode 判断,见 错误码。
# 目标读者
本文档的目标读者为:本系统的开发和实施人员
# 定义动态服务
菜单位置:开发定制 > 系统集成 > 动态服务

截图中的“设备状态查询”是本页示例用的服务(Active、Enable Log 开启,Enable Anonymous 关闭);“Test Echo”是平台项目模板自带的示例服务(来自模板的 data/csv/DynamicService_example.csv),新建的系统里都会有。
| 界面显示 | 属性名 | 类型 | 说明 |
|---|---|---|---|
| Name | name | String | 服务名称,租户内唯一,创建后不可修改;调用地址中使用这个名称 |
| Logic | logic | tech.muyan.dynamic.DynamicLogic | 服务的执行逻辑,逻辑类型为 DYNAMIC_SERVICE_CORE_LOGIC |
| Active | active | Boolean | 是否激活 |
| Enable Anonymous | enableAnonymous | Boolean | 是否允许未登录调用,默认关闭 |
| Enable Log | enableLog | Boolean | 是否记录执行记录,默认关闭;只有开启后才会生成 执行记录 |
| Body Type | bodyType | 枚举 | 请求体的解析方式:JSON、XML、INPUT_STREAM;不填时按 JSON 处理 |
| Exec Records | execRecords | 该服务的执行记录 |
新建动态服务:
- 在
开发定制 > 动态逻辑 > 动态逻辑中创建一个类型为DYNAMIC_SERVICE_CORE_LOGIC(动态服务核心逻辑)的动态逻辑。 - 在动态服务列表页点击
创建,填写名称,选择上一步的逻辑,打开Active,按需设置其他开关后保存。

截图中的服务名称、逻辑与下文示例一致。弹窗标题显示为领域类名 DynamicService,这是当前版本的界面问题。
# 注入变量
服务逻辑执行时,可以使用以下注入变量:
| 变量名称 | 变量类型 | 描述 |
|---|---|---|
user | tech.muyan.api.security.MuyanAuthentication | 调用者。未登录调用时是匿名身份对象,不是 null |
method | String | 请求的 HTTP 方法,例如 GET、POST |
params | Map<String, Object> | URL 参数。只有一个值的参数直接是字符串,有多个值的参数是列表 |
body | Object | 请求体,按 Body Type 解析:JSON 为 JSON 对象,XML 为 XML 对象,INPUT_STREAM 为原始输入流 |
log | Closure<?> | 用于打印执行日志的 log 闭包,开启 Enable Log 后日志写进执行记录 |
提示
URL 里的 access_token 参数也会出现在 params 中。
# 返回结果
逻辑应返回一个包含 status 和 body 两个键的 Map:
status:返回给调用方的 HTTP 状态码,必须是整数,不填时为 200;body:返回给调用方的内容,会被转成 JSON。不填,或者是空列表、空字符串、0、false时返回{}。
注意
平台只取返回值中的 status 和 body,其他键会被丢弃;status 的值不是数字时调用会失败(HTTP 500,For input string: ...)。不要把业务上的状态字段直接放在返回值的顶层,而要放进 body。
示例:按设备编号返回设备运行状态。
log("收到请求:method=${method}, params=${params}")
String code = params.equipmentCode ?: body?.equipmentCode
if (!code) {
return [status: 400, body: [message: '缺少参数 equipmentCode']]
}
return [
status: 200,
body : [
equipmentCode: code,
runningStatus: '运行中',
caller : user.name,
method : method,
]
]
2
3
4
5
6
7
8
9
10
11
12
13
14
# 调用方式与认证
调用地址为:
http://<服务器地址>/api/service/<服务名>
- 服务名如果含中文、空格等字符,需要进行 URL 编码。
- 不限制 HTTP 方法,
GET、POST、PUT、DELETE都会执行同一个逻辑,逻辑中可以通过method区分。 - 认证方式与平台其他 REST API 相同:先 登录 获取
access_token,再通过请求头Authorization: Bearer <access_token>、URL 参数access_token或 Cookie 携带,详见 在请求中携带 token。旧版的用户令牌和X-MY-Token请求头 1.0 起已移除。 Enable Anonymous开启后,不带 token 也可以调用,此时user为匿名身份。
以示例服务“设备状态查询”为例(Enable Anonymous 关闭,与上面列表截图一致):
# 带 token 调用(推荐使用请求头)
curl 'http://<服务器地址>/api/service/%E8%AE%BE%E5%A4%87%E7%8A%B6%E6%80%81%E6%9F%A5%E8%AF%A2?equipmentCode=EQ-001' \
-H 'Authorization: Bearer <access_token>'
# 返回 HTTP 200
# {"caller":"超级管理员","method":"GET","equipmentCode":"EQ-001","runningStatus":"运行中"}
# POST JSON 请求体
curl -X POST 'http://<服务器地址>/api/service/%E8%AE%BE%E5%A4%87%E7%8A%B6%E6%80%81%E6%9F%A5%E8%AF%A2' \
-H 'Authorization: Bearer <access_token>' -H 'Content-Type: application/json' \
-d '{"equipmentCode":"EQ-002"}'
# 不带 token,且服务未开启 Enable Anonymous
curl 'http://<服务器地址>/api/service/%E8%AE%BE%E5%A4%87%E7%8A%B6%E6%80%81%E6%9F%A5%E8%AF%A2?equipmentCode=EQ-001'
# 返回 HTTP 500
# {"msg":"ErrorCode: 11004, ErrorMsg: AnonymousInvocation","errorCode":11004}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
平台内部(其他动态逻辑、插件)不再通过动态服务互相调用;旧版的 DynamicServiceConsumerService、服务发现、jvm:// 调用方式 1.0 起已移除(插件之间可改用 动态 RPC)。
# 错误码
平台拦截请求时,响应体格式为 {"msg": "...", "errorCode": <错误码>}:
| errorCode | 名称 | 场景 |
|---|---|---|
| 11001 | ProviderNotFound | 找不到该名称的服务 |
| 11003 | ProviderInactivated | 服务未激活(Active 关闭) |
| 11004 | AnonymousInvocation | 未登录调用,且服务未开启 Enable Anonymous |
平台按上表从上到下依次检查,例如服务未激活时,不论是否登录都返回 11003。逻辑执行中抛出普通异常时,响应体为 {"msg": "<异常信息>"};抛出的是平台异常(例如逻辑限流时的 1007 RateLimitExceed)时,同样带 errorCode。
HTTP 状态码
当前版本中,以上所有错误的 HTTP 状态码都是 500,不区分“找不到”“未授权”等情况。请按响应体中的 errorCode 判断错误类型,不要依赖 HTTP 状态码。
# 配置的生效时间
平台会缓存动态服务的定义(Active、Enable Anonymous、Enable Log、Body Type 等配置)。按设计,修改服务后缓存应当立即失效;但当前版本有一个已知缺陷:插件导入完成时平台会清掉缓存的变更监听登记,而动态服务仍在使用导入前拿到的那个缓存,此后修改配置不会让它失效,只能等它自然过期。这个缓存在连续 10 分钟没有被调用后才会过期,所以修改这些配置后,只要服务一直有人调用,新配置就不会生效。实测:修改 Enable Anonymous、Active 后立即调用,仍按旧配置执行;停止调用 10 分钟后再调用,新配置才生效。
注意
这意味着关闭 Enable Anonymous 或 Active 并不能立即阻止调用。需要紧急停用某个服务时,可以把它的逻辑改为直接返回错误(逻辑修改立即生效),例如 return [status: 403, body: [message: '服务已停用']]。
逻辑代码的修改不受这个缓存影响,保存后下一次调用即生效。
# 执行记录
对开启了 Enable Log 的服务,每次进入逻辑执行的调用都会生成一条执行记录(被 错误码 中的检查拒绝的调用不记录),在 执行记录 > 服务 中查看,也可以在动态服务列表中点击 Exec Records 列的按钮查看某个服务的记录。

截图中的记录按时间倒序排列:下面两条来自上文 调用方式与认证 中带 token 的 GET、POST 调用;最上面一条是一次不带 equipmentCode 的调用,逻辑返回了 400,但逻辑本身执行成功,所以状态为 Success。未登录的那次调用被 11004 拒绝,没有记录。
执行记录的列较多,界面上要横向滚动才能看到 执行结果、执行日志、错误堆栈 等列(截图为了一屏显示全部列,加宽了窗口并截短了长文本列)。错误堆栈 列点其中的按钮查看完整内容。执行结果 以 JSON 保存,其中的中文被转义成 \uXXXX 的形式。当前版本点击行末的 详情,弹窗会一直处于加载状态(平台没有为动态服务执行记录配置详情表单),这是当前版本的界面问题。
| 列名 | 说明 |
|---|---|
| 服务提供者 | 被调用的动态服务 |
| 状态 | Success 或 Failed |
| Exec user | 调用者,未登录调用时为匿名身份 |
| 开始时间 | 开始执行的时间 |
| 结束时间 | 执行结束的时间 |
| 执行参数 | 请求的方法、URL 参数和请求体 |
| 执行结果 | 逻辑的返回值 |
| 执行日志 | 逻辑中通过 log 打印的日志 |
| 错误堆栈 | 出错时的异常信息 |
注意
通过 URL 参数传递的 access_token 会原样记录在“执行参数”中。调用开启了 Enable Log 的服务时,请使用 Authorization 请求头传递 token。
← 🌐 Webhook 集成 ⏲️ 定时任务 →