内置特性
# 内置特性
# 插件依赖
# 独立依赖三方组件
若插件因业务场景需引入主应用没有的第三方组件即独立依赖第三方组件,自动化构建打包,基本无需人工干预,详情请见[独立依赖第三方组件打包]{.underline}。
# plugin.order --- 简单优先级排序
值类型:整数。默认值:100000(未配置时)。值越小越先启动,批量停止时自动逆序。
# plugin-a/plugin.properties --- 先启动
plugin.order=100
# plugin-b/plugin.properties --- 后启动
plugin.order=200
启动顺序:plugin-a → plugin-b。停止时自动逆序:plugin-b → plugin-a。
适用场景:无需版本约束、无需显式声明依赖关系的简单排序。
# plugin.dependencies --- 拓扑依赖图
plugin.dependencies是 PF4J内置的依赖声明机制,支持可选版本约束。PF4J 将其解析为有向图并通过拓扑排序保证依赖插件先启动。
# plugin-b 依赖 plugin-a,要求版本 1.0 及以上
plugin.dependencies=plugin-a@1.0
# 可选依赖 --- 若 plugin-c 不存在则跳过
plugin.dependencies=plugin-c;optional
适用场景:需要精确版本约束或可选依赖时使用。
注意:两种机制不要同时配置产生冲突。两者共存时,plugin.order在拓扑解析后执行,可能覆盖依赖顺序。
# 会话信息
iotcenter.common包中的SessionContenxt类且通过构造函数注入来获取每一请求上下文中用户相关信息,例如获取当前操作账号、角色名,是否为管理员,访问IP和端口信息。


# 序列化&反列化
平台使用Jackson开源组件作为序列化&反序列化工具,禁止使用其他工具。插件通过Lombok的@RequiredArgsConstructor注解注入ObjectMapper:
@RequiredArgsConstructor
public class MyService {
private final ObjectMapper objectMapper;
}
若需指定日期格式,使用 @JsonFormat 注解。
# 读取配置
构造函数注入IoTPluginJsonConfigurationBinder接口,读取约定{ganweisoft.iotcenter.module.equiplist.json}配置
# 链式调用
# 直接绑定

# 简单字符串
# 本地事件总线
引入事件总线设施从而降低代码耦合或削峰等等,所有事件总线定义以及订阅处理规范约定放于integrationevents目录,events目录为事件定义,eventhandlers目录为事件处理程序即订阅事件,同时我们约定规范事件名称以点号隔开。
一般来讲,事件发布和订阅所属事件类型应为相同类型,但在插件化中存在事件发布和订阅处于不同包场景,此时发布事件和订阅事件类型不同,但属性一致,为避免代码耦合即解耦,底层设施也支持此场景。发布和订阅既支持一对一关系,也支持一对多关系即一处发布多处订阅
注意事项:事件总线订阅处理程序自行处理好幂等性设计,所谓幂等性即执行一次或多次,其影响是相同的,换言之,重复执行也不影响实际业务
# 定义事件
通过@EventName注解定义事件名称

# 订阅事件
事件处理类实现IoTPluginLocalEventListener<>泛型接口,泛型类为上述事件类
事件处理类名称命名规范:场景用途+EventListener

# 发布事件
构造函数注入IoTPluginLocalEventBus,发布事件既支持同步也支持异步

# Excel导入&导出
使用alibaba的easyexcel开源组件进行二次封装(包含国际化翻译等等),开箱即用
# 注入导入&导出接口
@Bean
public IExportManager exportManager(ApplicationContext applicationContext) {
return new ExportManager(applicationContext);
}
@Bean
public IImportManager importManager(ApplicationContext applicationContext) {
return new ImportManager(applicationContext);
}

# 国际化表头翻译支持
- 通过注解@ExcelProperty定义表头翻译键,[翻译键约定:excel.类名.字段名(当然类名或字段名若很长,可以自行进行简写,但excel前缀必须)]{.mark}

Excel sheet名称翻译键约定:[excel.sheet.name.类名(当然类名若很长,可以自行进行简写);翻译键对应指定的翻译文件配置,例如如下示例中文和英文翻译配置]{.mark}


# 导入举例说明
假设已通过平台下载导入模板,表头是根据平台当前语言已自动进行翻译,所以模板表头请勿随意修改。我们只需根据模板填充数据,如下我们建立2个sheet,一个是用户,一个是产品,导入后将获取到的数据进行回显。


# 导出举例说明
如下仅为参考示例,具体请以实际业务为准

# 数据转换对象(DTO)
对数据进行DTO,基础设施统一规范使用https://github.com/modelmapper/modelmapper (opens new window)来进行映射,若不熟悉,请自行查阅相关资料并了解
# 隐式映射
IoTModelMapperUtils中 的mapTo和mapToList隐式自动映射


# 显式映射
命名规范:插件后缀名称+ModelMapperConfig
添加@Extension注解
实现IModelMapperConfig接口
IoTTypeMapConfig.of配置映射(底层本质使用modelmapper的TypeMap)


# 国际化语言
前端页面内容和相关操作等提示都由后台统一返回,键默认为中文,所以后端中文翻译目录无需配置。翻译配置以如下对象格式输出,已废弃兼容json格式
# zh-CN目录
- Front:前端UI中文翻译
# en-US目录
Back:后台插件英文翻译
Front:前端UI英文翻译
注意事项1:Json翻译文件命名约定:英文且小写
注意事项2:前端文件命名约定【前端插件名.子菜单名.json】
注意事项3:后台插件命名约定【插件名.json】
# 国际化时间转换
客户端转服务端(场景:获取接口请求转换)
@JsonDeserialize(using = IoTClientLocalDateTimeDeserializer.class)服务端转客户端(场景:数据库返回日期转换)
@JsonSerialize(using = IoTClientLocalDateTimeSerializer.class)
# 插件翻译说明
翻译目录置于插件resources目录

构造函数注入


[插件打包]{.underline}自动化拷贝国际化目录,无需人工干预
# 实时消息通信
基于https://github.com/mrniko/netty-socketio (opens new window)二次封装为.NET Core SignalR Hub模式,开箱即用
# 规范目录
非必须但建议将hub实现统一放到hubs目录下
# Hub实现
类命名约定以Hub后缀结尾
类继承IoTHub
添加Component注解

# Hub建链
和前端沟通对齐需连接映射的Hub名称,比如为test_iot_hub,则将其名称固化

# 推送消息
# 指定客户端推送消息
调用方法:getClients().caller().sendAsync("方法名称","具体消息")
举个栗子:客户端连接上后,需立即推送消息,调用如下


# 指定组推送消息
getGroups().addToGroup加入组getClients().group获取指定组

{width="5.763194444444444in" height="4.46875in"}
二进制推送
推送二进制数据(如音频、文件),使用 sendBinaryAsync:
getClients().caller().sendBinaryAsync("事件名", byte[]数据);
# 接收消息
若客户端主动给服务端发送消息,则在Hub类定义公共方法以及增加客户端发送的方法名称注解即如下
添加@IoTHubMethod注解并和前端发送的方法名称对齐保持一致
接收对象Object实际对应jackson中的JsonNode,所以使用构造函数注入的 ObjectMapper进行转换为最终实际对象。
例如,最终需要转换的对象为TestHubMap,若前端传的消息为对象则
若前端传的消息为数组则
比如客户端给test_iot_hub的report方法推送消息



二进制数据接收
若客户端发送二进制数据(如音频流、文件字节),在 Hub 类定义方法并添加 @IoTHubBinaryMethod 注解,事件名与前端发送的事件名对齐:
@IoTHubBinaryMethod("audio")
public void onAudio(byte[] data) {
// data 为剥离帧头后的真实二进制数据
}
前端帧头约定
所有二进制上行需前置 1 字节 0x01 帧头(规避 netty-socketio 2.0.13「首字节 0 误判二进制帧为字符串帧」的缺陷)。该帧头由服务端基础设施统一校验并剥离后,再将真实数据分发到插件方法——插件层零感知,无需自行处理。
前端加帧头示例(JavaScript):
const raw = new Uint8Array(int16.buffer);
const framed = new Uint8Array(raw.length + 1);
framed[0] = 0x01;
framed.set(raw, 1);
socket.emit('audio', framed.buffer);
若前端漏加帧头(首字节非 0x01),服务端会以「帧头校验失败」丢弃该数据,不会透传给插件(避免音频/数据错位)。
# 当前连接
获取当前连接Id:
getContext().getConnectionId()获取当前用户Id:
getContext().getUserId();获取插件注入bean处理业务逻辑

# 接口调用
比如在控制器或接口实现里面需要通过对应Hub主动给客户端推送消息
注入插件上下文
上下文获取IoTHubManager的Bean
通过IoTHubManager获取Hub的上下文中的已连接的客户端并向指定客户端推送消息

为方便演示,我们直接在Hub里面模拟接口实现调用Hub上下文给当前已连接的客户端推送消息


# springdoc-openapi
springdoc-openapi采用和主应用springboot匹配版本,插件只需给控制器或控制器方法打上对应标签即可,更多用法请见官方文档:https://springdoc.org (opens new window)

# 控制器&方法打标签

# 访问swagger-ui
若已启用swagger-ui访问:/swagger-ui/index.html

# 匿名访问控制
插件接口无需登录认证即可访问(如公开查询接口、回调接口等),可通过@AllowAnonymous注解将控制器类或方法标记为匿名访问。
@AllowAnonymous 可标注在类上(整个Controller所有方法匿名),也可标注在单个方法上。方法级优先于类级。reason字段必填,用于运维审计。
// 类级:整个控制器匿名
@AllowAnonymous(reason = "SSO端点由第三方身份提供商回调,无法携带登录态")
@RestController
@RequestMapping("/api/v3/sso")
public class SsoCallbackController {
@PostMapping("/login")
public Result handleLogin(@RequestBody SsoLoginRequest req) {
return ssoService.processLogin(req);
}
@PostMapping("/logout")
public Result handleLogout(@RequestBody SsoLogoutRequest req) {
return ssoService.processLogout(req);
}
}
// 方法级:仅指定接口匿名
@RestController
@RequestMapping("/api/v3/user")
public class UserController {
// 匿名:SSO 回调由身份提供商调用
@AllowAnonymous
@PostMapping("/sso/callback")
public Result handleSsoCallback(@RequestBody SsoCallbackRequest req) {
return ssoService.handleCallback(req);
}
// 需鉴权
@GetMapping("/{id}")
public Result getUser(@PathVariable String id) {
return userService.getById(id);
}
}
# 数据库自动迁移
# 实体自动迁移
框架为插件实体提供数据库Schema 自动迁移能力。插件启动时,自动扫描@TableName(MyBatis-Plus)实体并与当前数据库结构对比,缺失的表和字段会被自动创建,无需手写 SQL 迁移脚本,无需任何额外配置,开箱即用
支持 6 种数据库:MySQL、PostgreSQL、GaussDB、KingbaseES、DM(达梦)、SQLite,仅做增量添加(建新表、加新列),不删除/不修改已有表结构,保证生产环境数据的完整性
# 脚本自动迁移
如果插件需要预置数据或创建特殊索引等无法通过实体建模的DDL,可在 src/main/resources/db/下放置 SQL 文件:
系统会自动识别当前数据库类型并执行对应目录下的 .sql 文件。
# 定时任务
基于Quartz (opens new window)提供插件定时任务调度能力。插件只需实现IPluginJob接口并标注@PluginJob注解,框架会在插件启动后自动扫描并注册到 Quartz调度器。
# 创建定时任务
在插件中创建实现IPluginJob的Bean,用@PluginJob注解标记:
@Slf4j
@Component
@PluginJob(name = "cleanExpiredTokens", intervalSeconds = 3600)
public class TokenCleanupJob implements IPluginJob {
@Override
public void execute() {
log.info("Cleaning expired tokens...");
// 业务逻辑
}
}
# @PluginJob参数说明
参数 类型 默认值 说明
name String 必填 任务唯一标识,全局唯一
intervalSeconds long -1 固定间隔(秒),与 cronExpression 二选一
cronExpression String "" Cron 表达式,与 intervalSeconds 二选一
runOnce boolean false 是否仅执行一次
# disallowConcurrentExecution boolean true 是否禁止并发执行
# SQL关键字自动引用
插件实体表的字段名若恰好是数据库的保留关键字(如 order、group、comment 等),直接执行SQL会抛出异常。
框架内置 MyBatis-Plus InnerInterceptor,运行时自动检测数据库类型,当列名与数据库保留关键字(如order、comment、context)冲突时,自动包裹正确的引号字符。JPA/Hibernate 通过自身dialect 或 hibernate.auto_quote_keyword 配置处理关键字引号。系统自动识别当前数据库类型(MySQL/PostgreSQL/GaussDB/KingbaseES/DM/SQLite)
@Component
public class UserKeywords implements TableKeywordProvider {
@Override
public Map<String, Set<String>> getTableKeywords() {
return Map.of(
"user_table", Set.of("level", "comment", "type")
);
}
}
# 插件认证&鉴权
插件只需实现鉴权接口即可对本插件接口进行自定义认证。框架负责路由分发,开发者只需写认证逻辑。
# 鉴权模式
核心规则:@PluginAuthenticated决定鉴权模式
注解 模式 Session 用户的处理
无 @PluginAuthenticated 全量排他鉴权 忽略宿主 Session,完全由插件 authenticate()接管
有 @PluginAuthenticated OR 鉴权 Session 优先放行,无 Session 才走 authenticate()
# 两层 API
层级 方式 适用场景
简单层 继承 一套鉴权策略,只需写authenticate(),框架自动处理路由 AbstractPluginAuthenticationProvider
高级层 实现 IPluginAuthenticationProvider 多套鉴权策略,需要自定义 supports() 做精细路由
# 注解
注解 作用 标注位置
@PluginAuthenticated 标记该接口走插件自定义鉴权 Controller 类/方法
@AllowAnonymous 匿名访问,跳过认证链 Controller 类/方法
@AllowAnonymous 优先级高于 @PluginAuthenticated。
# 使用场景
# 场景 1:插件所有接口全量排他鉴权
Provider 继承 AbstractPluginAuthenticationProvider,Controller 不加任何注解。插件自定义鉴权完全替代宿主 Session 认证,所有请求必过authenticate()。
@Component
public class PaymentJwtProvider extends AbstractPluginAuthenticationProvider {
@Override
public Authentication authenticate(HttpServletRequest request) {
String token = request.getHeader("Authorization");
if (token == null) {
return null; // 无凭证 → 401
}
// 校验 token...
return new UsernamePasswordAuthenticationToken("user", null, List.of());
}
}
框架自动将该插件所有接口注入 Provider,supports() 无需重写。
# 场景 2:部分接口 OR 鉴权
Provider 同上,Controller 在需要鉴权的接口上标注 @PluginAuthenticated。
Session 用户直接放行,外部 API 走authenticate()
@PluginAuthenticated
@PostMapping("/refund")
public Result refund() { ... } // → OR:Session 放行 / 插件 Token
@GetMapping("/order/{id}")
public Result getOrder() { ... } // → 宿主 session(未标注,不触发插件鉴权)
# 场景 3:混合鉴权 + 匿名
组合使用@PluginAuthenticated和@AllowAnonymous。
@PluginAuthenticated
@PostMapping("/refund")
public Result refund() { ... } // → 插件自定义鉴权
@AllowAnonymous(reason = "支付回调")
@PostMapping("/callback")
public Result callback() { ... } // → 匿名放行
@GetMapping("/order/{id}")
public Result getOrder() { ... } // → 宿主 session
# 场景 4:多套鉴权策略(高级层)
直接实现IPluginAuthenticationProvider,各自写supports()区分接口。
@Order(100)
@Component
public class TokenAuthProvider implements IPluginAuthenticationProvider {
@Override
public boolean supports(HttpServletRequest request) {
return request.getRequestURI().startsWith("/api/payment/refund");
}
@Override
public Authentication authenticate(HttpServletRequest request) { /* Token */ }
}
@Order(200)
@Component
public class ApiKeyAuthProvider implements IPluginAuthenticationProvider {
@Override
public boolean supports(HttpServletRequest request) {
return request.getRequestURI().startsWith("/api/payment/reconciliation");
}
@Override
public Authentication authenticate(HttpServletRequest request) { /* ApiKey */ }
}
Controller不加注解,由 Provider各自的supports()路由。
# 场景 5:简单层 + 高级层共存
大部分接口 JWT(简单层),个别接口用 ApiKey(高级层)。
约束:共存时简单层必须配合@PluginAuthenticated标注范围,否则框架 WARN 并跳过简单层。
// 简单层
@Component
public class PaymentJwtProvider extends AbstractPluginAuthenticationProvider { ... }
// 高级层
@Order(100)
@Component
public class CallbackApiKeyProvider implements IPluginAuthenticationProvider {
@Override
public boolean supports(HttpServletRequest request) {
return request.getRequestURI().startsWith("/api/payment/callback");
}
@Override
public Authentication authenticate(HttpServletRequest request) { /* ApiKey */ }
}
Controller 示例:
@PluginAuthenticated
@PostMapping("/refund")
public Result refund() { ... } // → 简单层 PaymentJwtProvider
@PostMapping("/callback")
public Result callback() { ... } // → 高级层 CallbackApiKeyProvider (order=100 先到)
# 快速对照
场景 Provider Controller 注解 Session 用户
全量排他 extends 不加 忽略,必过 AbstractPluginAuthenticationProvider authenticate()
OR 鉴权 extends @PluginAuthenticated 放行 AbstractPluginAuthenticationProvider
混合匿名 extends @PluginAuthenticated + 放行 AbstractPluginAuthenticationProvider @AllowAnonymous
多套策略 N × implements 不加 由 supports() IPluginAuthenticationProvider 决定
# 简单+高级共存 1 × 必须 放行
AbstractPluginAuthenticationProvider + N @PluginAuthenticated
× IPluginAuthenticationProvider
# 插件过滤器扩展
插件可向宿主 Spring Security 过滤链的指定位置注入过滤器(jakarta.servlet.Filter),统一承载多步认证流程、授权决策、请求拦截等横切能力。插件只需实现 IPluginFilterExtension 接口,声明挂载位置并返回过滤器即可,框架自动完成扫描、排序、挂载与热卸载。
# 扩展接口
public interface IPluginFilterExtension {
PluginFilterPosition position();
jakarta.servlet.Filter getFilter();
default int getOrder() { return 0; }
}
# 六个固定位置
public enum PluginFilterPosition {
FIRST, // SecurityContextHolderFilter 之前
SESSION_RESTORE, // SecurityContextHolderFilter 之后
AUTHENTICATION, // 认证入口(UsernamePasswordAuthenticationFilter 位置)
ANONYMOUS, // AnonymousAuthenticationFilter 之后
PRE_AUTHORIZE, // AuthorizationFilter 之前(授权决策)
LAST // AuthorizationFilter 之后
}
# 使用示例
以多步认证流程为例,插件实现过滤器扩展并注册到 AUTHENTICATION 位置:
@Component
public class CustomLoginFilterExtension implements IPluginFilterExtension {
@Override
public PluginFilterPosition position() {
return PluginFilterPosition.AUTHENTICATION;
}
@Override
public jakarta.servlet.Filter getFilter() {
return new CustomLoginFilter();
}
}
public class CustomLoginFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain)
throws ServletException, IOException {
if (!request.getRequestURI().startsWith("/sso/")) {
chain.doFilter(request, response);
return;
}
// 多步认证逻辑
chain.doFilter(request, response);
}
}
# 3.16.4. 与插件认证鉴权的关系
认证决策(单步凭证校验)走 IPluginAuthenticationProvider(见「插件认证 & 鉴权」章节);多步认证流程、授权决策、通用请求拦截走本机制 IPluginFilterExtension。二者正交并存。