从 CLI 到增强型 UI:A2UI 构建自主 UI 体系与 CLI 的 Bridger 层核心逻辑
0. A2UI 的诞生背景与意义
0.1 从 CLI 到 A2UI:人机交互的演进
在软件开发的历史长河中,命令行界面(CLI)曾是开发者与系统交互的唯一方式。虽然 CLI 以其高效、灵活的特点深受技术人员的喜爱,但随着软件系统复杂度的指数级增长,纯文本的交互方式逐渐显露出其局限性:
- 学习曲线陡峭:需要记忆大量命令和参数
- 可视化能力弱:难以直观展示复杂的数据结构和关系
- 操作效率受限:对于非技术用户,CLI 几乎是不可逾越的鸿沟
正是在这样的背景下,A2UI(Augmented & Autonomous UI,增强型自主 UI) 应运而生。A2UI 不仅仅是传统 GUI 的升级版,它代表了人机交互的下一个范式:在保持 CLI 高效性的同时,通过智能化的 UI 组件提供更直观、更强大的交互体验。
0.2 为什么在 AICoding 时代仍需要 A2UI?⭐ 核心技术洞察
在 AICoding(AI 辅助编程)技术日趋成熟的今天,特别是在前端领域,AI 已经能够根据需求自动生成高质量的全栈代码。这引发了一个核心问题:为什么还需要引入 A2UI 这样的技术体系?
0.2.1 AICoding 的局限性
虽然 AICoding 在代码生成方面表现出色,但在实际企业级应用场景中,它面临着以下挑战:
1. 生成成本高昂
- 每次生成都需要调用 AI 模型,消耗大量计算资源
- 对于高频、重复的 UI 需求,成本不可控
- 生成代码的质量参差不齐,需要人工审核和调整
2. 代码维护困难
- AI 生成的代码风格不统一,难以维护
- 业务逻辑与 UI 代码耦合度高,修改成本大
- 缺乏统一的架构约束,容易产生技术债
3. 无法实时响应上下文变化
- 用户上下文(User Context)、模块上下文(Module Context)、数据模块上下文(Data Module Context)的变化需要重新生成
- 无法根据运行时环境动态调整 UI 展现形式
- 缺乏对实时数据的响应能力
0.2.2 A2UI 的核心技术难点
A2UI 的设计面临着独特的技术挑战,这些挑战是 AICoding 难以直接解决的:
核心矛盾:CLI 数据的固定性 vs 前端展现的动态性
图 0-3:CLI 数据固定性与前端展现动态性的矛盾
0.2.3 动态拦截机制:A2UI 的技术核心
为了解决上述矛盾,A2UI 引入了动态拦截机制,这是本文的技术重点:
为什么选择动态拦截而非 AICoding?
| 对比维度 | AICoding 方案 | A2UI 动态拦截机制 |
|---|---|---|
| 响应速度 | 秒级(需调用 AI 模型) | 毫秒级(本地计算) |
| 成本控制 | 每次生成都消耗资源 | 一次性开发,无限次使用 |
| 上下文感知 | 需要重新生成 | 实时动态调整 |
| 代码质量 | 参差不齐,需审核 | 统一标准,可控可测 |
| 维护成本 | 高(每次修改需重新生成) | 低(修改配置即可) |
| 扩展性 | 依赖 AI 模型能力 | 基于 SPI 机制,无限扩展 |
动态拦截机制的核心价值:
1. 实时上下文感知
// 根据用户上下文动态选择展现方式
if (userContext.hasPermission("admin")) {
skill = new DetailedViewSkill(); // 管理员看详细视图
} else {
skill = new SimpleViewSkill(); // 普通用户看简洁视图
}
2. 智能 UI 组件选择
// 根据数据量自动选择展现形式
if (dataCount > 1000) {
componentType = ComponentType.PAGINATED_GRID; // 大数据量用分页表格
} else if (hasHierarchy(data)) {
componentType = ComponentType.TREE_GRID; // 层级数据用树形表格
} else {
componentType = ComponentType.SIMPLE_GRID; // 简单数据用普通表格
}
3. 动态字段配置
// 根据模块上下文动态配置表单字段
List<Field> fields = new ArrayList<>();
if (moduleContext.isCreateMode()) {
fields.addAll(getRequiredFields()); // 创建模式显示必填字段
} else if (moduleContext.isViewMode()) {
fields.addAll(getAllFields()); // 查看模式显示所有字段
}
4. 运行时决策引擎
图 0-4:A2UI 动态决策引擎架构
0.2.4 A2UI 与 AICoding 的协同关系
A2UI 并不排斥 AICoding,而是与之形成互补:
图 0-5:A2UI 与 AICoding 的协同关系
总结:
A2UI 的引入不是为了替代 AICoding,而是为了解决 AICoding 在运行时动态性方面的不足。通过动态拦截机制,A2UI 实现了:
- 实时响应上下文变化:无需重新生成代码,即可根据用户、模块、数据上下文动态调整 UI
- 成本可控:一次性开发技能,无限次复用,避免频繁调用 AI 模型的高昂成本
- 质量可控:统一的架构约束和代码规范,确保生成 UI 的一致性和可维护性
- 扩展性强:基于 SPI 机制,可以灵活扩展新的 UI 组件和交互方式
这正是本文要深入探讨的技术重点:如何设计和实现一个高效、灵活、可扩展的动态拦截机制。
0.3 A2UI 的核心意义:WEB 前端技术的演进方向
Ooder A2UI 的设计理念可以概括为以下三个核心价值:
1. 完全自主的 UI 基础设施
传统的 UI 框架往往依赖于第三方组件库,导致定制化困难、版本依赖复杂。Ooder A2UI 从底层开始构建,实现了:
- 零外部依赖:核心组件完全自主研发
- 高度可定制:每个组件都可以根据业务需求深度定制
- 版本控制友好:组件与业务逻辑解耦,升级无负担
2. CLI 与 UI 的无缝桥接
这是 Ooder A2UI 最具创新性的设计目标。通过建立一层智能的 Bridge 层,实现:
图 0-1:CLI ↔ A2UI Bridge 层架构 - 双向同步的智能桥接机制
这种设计使得:
- 开发者可以继续使用熟悉的 CLI 工作流,同时享受 UI 的可视化优势
- 运维人员可以通过 UI 界面操作,系统自动生成对应的 CLI 脚本供审计和复用
- 产品经理和业务人员可以通过直观的 UI 界面参与系统配置,降低沟通成本
3. Web 技术的前沿探索
Ooder A2UI 紧跟现代 Web 技术的发展趋势,融合了多项前沿技术:
- 组件化架构:借鉴 React、Vue 等现代框架的设计理念
- 声明式 UI:通过注解和配置定义 UI,而非命令式编程
- 响应式设计:一套代码适配多种设备和屏幕尺寸
- 渐进式增强:从基础功能到高级特性,按需加载
0.4 Web 拦截层:A2UI 的核心枢纽
本文聚焦于 Ooder A2UI 架构中最关键的一环 —— Web 拦截层的设计与实现。作为连接用户请求与后端服务的桥梁,Web 拦截层承担着以下核心职责:
- 请求路由与分发:智能识别请求类型,分发到对应的处理器
- 技能驱动:基于 Skills 架构,实现组件的动态加载和生成
- SPI 扩展:通过 Java SPI 机制,支持第三方技能的无缝集成
- 性能优化:缓存、懒加载、并发控制等多重优化策略
通过本文的深度解析,您将全面了解:
- 如何从传统的硬编码拦截器演进到 Skills + SPI 架构
- 注解驱动开发如何简化组件定义和注册
- SPI 机制如何实现技能的热插拔和动态加载
- 服务层如何协作完成复杂的业务逻辑
- 如何设计和实现 RESTful API 供前端调用
0.5 适用读者
本文适合以下读者:
- 架构师:了解现代 Web 应用的架构设计思路
- 后端开发者:学习 Java 注解、SPI、拦截器等高级特性
- 前端开发者:理解后端 API 设计,更好地进行前后端协作
- 技术管理者:评估技术方案的可行性和扩展性
1. 架构演进背景
1.1 传统拦截机制的痛点
在 Ooder 框架的早期版本中,我们采用了基于硬编码的拦截器模式来处理特定后缀的请求(如 .jsx、.cls、.dyn 等)。这种设计存在以下问题:
图 1-1:传统拦截架构 - 多个独立拦截器导致代码重复和维护困难
核心问题:
- 耦合度高:拦截逻辑与具体的组件类型硬编码绑定
- 扩展困难:新增组件类型需要修改拦截器核心代码
- 维护成本高:多个拦截器类职责不清晰,代码重复
- 缺乏统一管理:组件注册和发现机制分散
1.2 Skills + SPI 架构的优势
为了解决上述问题,我们引入了 Skills + SPI 的架构设计:
图 1-2:Skills + SPI 拦截架构 - 统一入口和可扩展的技能注册机制
核心优势:
- 解耦:拦截逻辑与具体组件实现完全分离
- 可扩展:通过 SPI 机制动态加载新技能
- 统一管理:所有技能通过注册中心集中管理
- 注解驱动:使用注解声明式定义技能元数据
2. 核心架构设计
2.1 整体架构图
图 2-1:Ooder A2UI 六层架构设计 - 从 Web 层到注解层的完整体系
2.2 数据流图
图 2-2:请求处理数据流 - 从 HTTP 请求到 JSON 响应的完整链路
2.3 技能生命周期管理
图 2-3:Skill 生命周期 - 从定义、注册、发现到调用、发布、桥接、管理的完整流程
3. 注解层设计
3.1 @A2uiSkill 注解
注解是整个技能系统的元数据基础,用于声明式地定义技能的核心属性。
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface A2uiSkill {
String id(); // 技能唯一标识
String name() default ""; // 技能显示名称
String description() default ""; // 技能描述
String version() default "1.0.0"; // 版本号
String category() default "data-display"; // 分类
String[] capabilities() default {}; // 能力列表
ModuleViewType moduleViewType() default ModuleViewType.NONE;
ComponentType componentType() default ComponentType.MODULE;
int priority() default 100; // 优先级
}
设计要点:
- id 是必填项:确保每个技能有唯一标识
- 默认值策略:减少配置负担,常用字段提供合理默认值
- 类型安全:使用枚举类型(ModuleViewType、ComponentType)避免字符串错误
- 可扩展性:capabilities 数组支持声明技能的特殊能力
3.2 A2uiSkillBehavior 接口
行为接口定义了技能必须实现的核心方法:
public interface A2uiSkillBehavior {
String getSkillId(); // 获取技能 ID
String getComponentType(); // 获取组件类型
ModuleViewType getModuleViewType(); // 获取模块视图类型
String getCategory(); // 获取分类
String buildGenJson(String moduleName, String caption, // 构建生成 JSON
List<String> fields,
Map<String, Object> options);
Map<String, Object> buildFromNaturalLanguage(String query); // NLP 构建
List<String> getKeywords(); // 关键词列表
JSONObject toCardJson(); // 卡片 JSON
}
方法职责:
buildGenJson(): 核心方法,根据参数生成模块的 JSON 表示buildFromNaturalLanguage(): 支持自然语言输入,智能解析意图toCardJson(): 生成用于前端展示的卡片元数据
4. SPI 扩展机制
4.1 A2uiSkillRegistry 接口
SPI 接口定义了技能注册表的契约:
public interface A2uiSkillRegistry {
int getPriority(); // 优先级
String getSkillClassName(String skillId); // 获取类名
Class<?> getSkillClass(String skillId); // 获取 Class
Set<String> getSkillIds(); // 所有技能 ID
String getCategory(String skillId); // 获取分类
ModuleViewType getModuleViewType(String skillId); // 获取视图类型
ComponentType getComponentType(String skillId); // 获取组件类型
String findSkillIdByComponentType(String componentType);// 反向查找
}
4.2 A2uiSkillRegistrySPI 实现
SPI 加载器使用 Java 原生的 ServiceLoader 机制:
public class A2uiSkillRegistrySPI {
private static volatile A2uiSkillRegistrySPI instance;
private final List<A2uiSkillRegistry> registries = new ArrayList<>();
private final Map<String, String> classNameCache = new ConcurrentHashMap<>();
private final Map<String, Class<?>> classCache = new ConcurrentHashMap<>();
private final Map<String, String> componentTypeToSkillIdCache = new ConcurrentHashMap<>();
private void loadRegistries() {
ServiceLoader<A2uiSkillRegistry> loader = ServiceLoader.load(
A2uiSkillRegistry.class,
Thread.currentThread().getContextClassLoader());
for (A2uiSkillRegistry registry : loader) {
registries.add(registry);
}
// 按优先级排序
registries.sort(Comparator.comparingInt(A2uiSkillRegistry::getPriority));
}
}
设计亮点:
- 多重缓存:classNameCache、classCache、componentTypeToSkillIdCache 三层缓存
- 优先级排序:支持多个注册表,按优先级加载
- 线程安全:使用 volatile 和 ConcurrentHashMap 保证并发安全
- 热加载支持:提供 reload() 和 clearCache() 方法
4.3 SPI 配置文件
在 META-INF/services 下创建配置文件:
# 文件路径:src/main/resources/META-INF/services/net.ooder.annotation.spi.A2uiSkillRegistry
net.ooder.a2ui.nlp.skill.A2uiSkillRegistryImpl
5. 技能实现层
5.1 AbstractA2uiSkill 抽象基类
抽象基类使用模板方法模式提供通用实现:
public abstract class AbstractA2uiSkill implements A2uiSkillBehavior {
protected Configuration freemarkerCfg; // FreeMarker 模板引擎
public AbstractA2uiSkill() {
// 初始化 FreeMarker 配置
freemarkerCfg = new Configuration(Configuration.VERSION_2_3_31);
freemarkerCfg.setClassForTemplateLoading(this.getClass(), "/");
freemarkerCfg.setDefaultEncoding("UTF-8");
}
// 默认实现:通过注解获取元数据
@Override
public String getSkillId() {
A2uiSkill anno = getClass().getAnnotation(A2uiSkill.class);
return anno != null ? anno.id() : getClass().getSimpleName().toLowerCase();
}
// 模板方法:子类重写以提供具体组件的生成逻辑
protected abstract String getModuleTemplate();
// 通用方法:构建模块 JSON
protected String buildModuleFromTemplate(String className, String propertiesJson,
String childrenJson) {
String template = getModuleTemplate();
Template tpl = new Template("module", template, freemarkerCfg);
// ... 模板渲染逻辑
}
}
5.2 TreeGridSkill 实现示例
具体技能实现展示如何扩展抽象基类:
@Component
@A2uiSkill(
id = "treegrid",
name = "树形表格",
description = "用于展示树形结构数据的表格组件",
category = "data-display",
moduleViewType = ModuleViewType.GRIDCONFIG,
componentType = ComponentType.TREEGRID,
priority = 10
)
public class TreeGridSkill extends AbstractA2uiSkill {
@Override
public List<String> getKeywords() {
return Arrays.asList("树形表格", "tree", "grid", "层级数据", "树状结构");
}
@Override
public String buildGenJson(String moduleName, String caption,
List<String> fields, Map<String, Object> options) {
// 1. 创建基础属性
JSONObject properties = createBaseProperties(caption, moduleName);
// 2. 添加 TreeGrid 特有配置
properties.put("treeField", "name");
properties.put("showTreeLines", true);
properties.put("expandColumn", 0);
// 3. 构建字段配置
JSONArray columns = new JSONArray();
for (int i = 0; i < fields.size(); i++) {
JSONObject col = new JSONObject();
col.put("field", fields.get(i));
col.put("caption", fields.get(i));
col.put("width", 100);
columns.add(col);
}
properties.put("columns", columns);
// 4. 使用模板生成最终 JSON
return buildModuleFromTemplate(moduleName, properties.toJSONString(), "[]");
}
}
5.3 内置技能列表
| Skill ID | Component | Category |
|---|---|---|
| treegrid | TREEGRID | data-display |
| form | FORMLAYOUT | data-input |
| tree | TREEVIEW | data-display |
| gallery | GALLERY | data-display |
| chart | ECHARTS | data-visualization |
| navtree | TREEBAR | navigation |
| navigallery | GALLERY | navigation |
| navtabs | TABS | navigation |
| navmenubar | BAR | navigation |
| tabscontainer | TABS | layout |
| layout | LAYOUT | layout |
6. 拦截器重构
6.1 SkillDrivenInterceptor 核心逻辑
统一拦截器替代了原有的多个硬编码拦截器:
@Component
public class SkillDrivenInterceptor extends BaseInterceptor {
@Autowired
private List<ResourceResolver> resourceResolvers;
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
String url = request.getRequestURI();
SuffixType suffix = SuffixType.fromUrl(url);
// 1. 无后缀请求:数据请求处理
if (suffix == SuffixType.NONE) {
return handleDataRequest(handler, request, response);
}
// 2. 剥离后缀获取类名
String className = SuffixType.stripSuffix(url);
// 3. 根据后缀类型分发到不同处理器
switch (suffix) {
case JSX: return handleJsx(className, request, response);
case CLS: return handleCls(className, request, response);
case DYN: return handleDyn(handler, request, response);
case VIEW:
case JSA:
case JSAA: return handleView(className, request, response);
default: return true;
}
}
// JSX 处理:技能驱动的模块构建
private boolean handleJsx(String className, HttpServletRequest request,
HttpServletResponse response) {
// 1. 尝试静态资源解析
if (tryStaticResource(className, request, response)) {
return false;
}
// 2. 解析组件类型
String componentType = resolveComponentType(className);
// 3. 查找对应技能
A2uiSkillBehavior skill = findSkill(componentType);
if (skill != null) {
try {
// 4. 使用技能构建模块
UIModule uiModule = buildModuleFromSkill(skill, className, request);
if (uiModule != null) {
// 5. 填充数据并返回 JSON
MethodConfig methodConfig = getCurrMethodConfig(request);
if (methodConfig != null && uiModule.getComponent() != null) {
fillDataFromMethod(methodConfig, uiModule, request);
}
String json = EngineFactory.getAdminESDClient()
.genJSON(uiModule, null, true);
sendJSON(response, json.toString());
return false;
}
} catch (Exception e) {
log.error("Skill-driven module build failed", e);
}
}
// 6. 降级到传统模块处理
return handleLegacyModule(className, request, response);
}
// 技能查找:优先从缓存,其次从 SPI
private A2uiSkillBehavior findSkill(String componentType) {
if (componentType == null) return null;
// 1. 从配置工厂查找
A2uiSkillBehavior skill = C2UConfigFactory.getInstance()
.findSkillByComponentType(componentType);
if (skill != null) return skill;
// 2. 从 SPI 查找
A2uiSkillRegistrySPI skillSPI = A2uiSkillRegistrySPI.getInstance();
String skillId = skillSPI.findSkillIdByComponentType(componentType);
if (skillId != null) {
try {
Class<?> skillClass = skillSPI.getSkillClass(skillId);
if (skillClass != null &&
A2uiSkillBehavior.class.isAssignableFrom(skillClass)) {
return (A2uiSkillBehavior) skillClass
.getDeclaredConstructor().newInstance();
}
} catch (Exception e) {
log.error("Skill SPI lookup failed", e);
}
}
return null;
}
}
6.2 拦截器对比
图 6-1:拦截器架构对比 - 传统多拦截器 vs 统一技能驱动拦截器
7. 服务层架构
7.1 SkillManagementService
技能管理的核心服务,采用单例模式:
public class SkillManagementService {
private static volatile SkillManagementService instance;
private final Map<String, SkillModuleConfig> skillModuleCache = new ConcurrentHashMap<>();
public static SkillManagementService getInstance() {
if (instance == null) {
synchronized (SkillManagementService.class) {
if (instance == null) {
instance = new SkillManagementService();
}
}
}
return instance;
}
// 核心方法:通过技能创建模块
public UIModule createModuleFromSkill(String skillId, String moduleName,
String projectName, String caption,
List<String> fields,
Map<String, Object> options) {
// 1. 查找技能
A2uiSkillBehavior skill = findSkill(skillId);
if (skill == null) {
logger.error("Skill not found: " + skillId);
return null;
}
// 2. 生成模块配置
SkillModuleConfig config = SkillModuleConfig.fromSkill(
skill, moduleName, caption, fields, options);
skillModuleCache.put(moduleName, config);
// 3. 创建物理模块
ProjectCacheManager pcm = getProjectCacheManager(projectName);
INProject project = pcm.getProjectByName(projectName);
UIModule uiModule = pcm.createModule(version, moduleName);
// 4. 保存生成的 JSON 到文件系统
if (uiModule != null && config.getGenJson() != null) {
String physicalPath = version.getPath() + "/" +
moduleName.replace(".", "/") + ".cls";
CtVfsFactory.getCtVfsService().saveFileAsContent(
physicalPath, config.getGenJson(), "UTF-8");
}
return uiModule;
}
// 获取所有技能卡片
public Map<String, Object> getSkillCards() {
Map<String, Object> result = new LinkedHashMap<>();
A2uiSkillRegistrySPI skillSPI = A2uiSkillRegistrySPI.getInstance();
Set<String> skillIds = skillSPI.getSkillIds();
List<JSONObject> cards = new ArrayList<>();
for (String skillId : skillIds) {
A2uiSkillBehavior skill = findSkill(skillId);
if (skill != null) {
cards.add(skill.toCardJson());
}
}
result.put("skills", cards);
result.put("total", cards.size());
return result;
}
}
7.2 SkillPublishService
技能发布服务,负责生成可发布的技能包:
public class SkillPublishService {
public SkillPublishResult publish(A2uiSkillBehavior skill, String outputDir) {
String skillId = skill.getSkillId();
// 1. 创建技能目录结构
Path skillDir = Paths.get(outputDir, skillId + "-skill");
Files.createDirectories(skillDir);
// 2. 生成 Java 源码
Path javaDir = skillDir.resolve("src/main/java")
.resolve(packageName.replace(".", "/"));
Files.createDirectories(javaDir);
String javaSource = generateSkillJavaSource(skill, packageName);
Path javaFile = javaDir.resolve(skill.getClass().getSimpleName() + ".java");
Files.writeString(javaFile, javaSource);
// 3. 生成 SPI 配置文件
Path metaInfDir = skillDir.resolve("src/main/resources/META-INF/services");
Files.createDirectories(metaInfDir);
String spiContent = packageName + "." +
skill.getClass().getSimpleName() + "\n";
Path spiFile = metaInfDir.resolve("net.ooder.annotation.spi.A2uiSkillRegistry");
Files.writeString(spiFile, spiContent);
// 4. 返回发布结果
SkillPublishResult result = SkillPublishResult.success(skillId);
result.setJavaSource(javaSource);
result.setPublishedAt(new Date().toInstant().toString());
return result;
}
}
7.3 服务层协作关系
图 7-1:服务层协作关系 - SkillController 调用三个核心服务
8. 项目整合桥接
8.1 SkillProjectBridge
桥接服务负责技能与项目之间的关联管理:
public class SkillProjectBridge {
// 添加技能到项目
public SkillProjectBridgeResult addSkillToProject(
String projectName, String skillId,
String moduleName, String caption,
List<String> fields) {
// 1. 查找技能
SkillManagementService skillMgmt = SkillManagementService.getInstance();
A2uiSkillBehavior skill = skillMgmt.findSkill(skillId);
if (skill == null) {
return SkillProjectBridgeResult.fail("Skill not found: " + skillId);
}
// 2. 获取项目管理器
ProjectCacheManager pcm = getProjectCacheManager(projectName);
if (pcm == null) {
return SkillProjectBridgeResult.fail("Project not found: " + projectName);
}
// 3. 创建模块
UIModule module = pcm.createModuleFromSkill(projectName, moduleName,
skillId, caption, fields, new HashMap<>());
// 4. 记录技能依赖
ProjectConfig config = project.getConfig();
config.addSkillDependency(skillId);
pcm.updateProjectConfig(projectName, config);
// 5. 返回结果
SkillProjectBridgeResult result = SkillProjectBridgeResult.success();
result.setSkillId(skillId);
result.setModuleName(moduleName);
result.setProjectName(projectName);
result.setComponentType(skill.getComponentType());
return result;
}
// 列出项目的所有技能
public List<Map<String, String>> listProjectSkills(String projectName) {
List<Map<String, String>> skills = new ArrayList<>();
ProjectCacheManager pcm = getProjectCacheManager(projectName);
if (pcm != null) {
// 从项目配置中读取技能依赖
List<String> skillDeps = pcm.getSkillDependencies(projectName);
for (String skillId : skillDeps) {
Map<String, String> info = new LinkedHashMap<>();
info.put("skillId", skillId);
A2uiSkillBehavior skill = SkillManagementService
.getInstance().findSkill(skillId);
if (skill != null) {
info.put("name", skill.toCardJson().getString("name"));
info.put("componentType", skill.getComponentType());
info.put("category", skill.getCategory());
}
skills.add(info);
}
}
return skills;
}
// 从项目移除技能
public boolean removeSkillFromProject(String projectName, String skillId) {
ProjectCacheManager pcm = getProjectCacheManager(projectName);
if (pcm == null) return false;
INProject project = pcm.getProjectByName(projectName);
if (project != null && project.getConfig() != null) {
ProjectConfig config = project.getConfig();
// 移除技能依赖
config.removeSkillDependency(skillId);
pcm.updateProjectConfig(projectName, config);
return true;
}
return false;
}
}
8.2 项目与技能的关系
图 8-1:项目 - 技能关系 - 一对多的技能依赖管理
9. RESTful API 设计
9.1 SkillController API 列表
@RestController
@RequestMapping("/api/skill")
public class SkillController {
// 1. 获取所有技能列表
@GetMapping("/list")
public ResponseEntity<Map<String, Object>> listSkills() {
// GET /api/skill/list
// Response: { "skills": [...], "total": 11 }
}
// 2. 通过技能构建模块
@PostMapping("/build")
public ResponseEntity<Map<String, Object>> buildFromSkill(
@RequestBody JSONObject request) {
// POST /api/skill/build
// Body: { "skillId": "treegrid", "moduleName": "MyModule",
// "caption": "测试模块", "fields": ["id", "name"],
// "options": {...} }
}
// 3. 自然语言构建模块
@PostMapping("/nlp-build")
public ResponseEntity<Map<String, Object>> buildFromNaturalLanguage(
@RequestBody JSONObject request) {
// POST /api/skill/nlp-build
// Body: { "query": "创建一个包含 ID 和名称的树形表格" }
}
// 4. 添加技能到项目
@PostMapping("/project/add-skill")
public ResponseEntity<Map<String, Object>> addSkillToProject(
@RequestBody JSONObject request) {
// POST /api/skill/project/add-skill
}
// 5. 列出项目的技能
@GetMapping("/project/{projectName}/skills")
public ResponseEntity<Map<String, Object>> listProjectSkills(
@PathVariable String projectName) {
// GET /api/skill/project/ProjectA/skills
}
// 6. 从项目移除技能
@DeleteMapping("/project/{projectName}/skill/{skillId}")
public ResponseEntity<Map<String, Object>> removeSkillFromProject(
@PathVariable String projectName,
@PathVariable String skillId) {
// DELETE /api/skill/project/ProjectA/skill/treegrid
}
// 7. 发布技能
@PostMapping("/publish/{skillId}")
public ResponseEntity<Map<String, Object>> publishSkill(
@PathVariable String skillId,
@RequestParam(defaultValue = "./published-skills") String outputDir) {
// POST /api/skill/publish/treegrid?outputDir=./published
}
// 8. 健康检查
@GetMapping("/health")
public ResponseEntity<Map<String, Object>> health() {
// GET /api/skill/health
// Response: { "status": "UP", "registeredSkills": 11, ... }
}
}
9.2 API 响应示例
获取技能列表:
{
"skills": [
{
"skillId": "treegrid",
"name": "树形表格",
"description": "用于展示树形结构数据的表格组件",
"category": "data-display",
"componentType": "TREEGRID",
"moduleViewType": "GRIDCONFIG",
"version": "1.0.0",
"keywords": ["树形表格", "tree", "grid", "层级数据"]
},
{
"skillId": "form",
"name": "表单",
"description": "数据录入表单组件",
"category": "data-input",
"componentType": "FORMLAYOUT",
"moduleViewType": "FORMCONFIG",
"version": "1.0.0",
"keywords": ["表单", "form", "数据录入"]
}
],
"total": 11
}
添加技能到项目:
{
"success": true,
"message": "Skill added successfully",
"skillId": "treegrid",
"moduleName": "MyTreeGrid",
"componentType": "TREEGRID",
"projectName": "ProjectA"
}
10. 最佳实践
10.1 开发自定义技能
步骤 1:创建技能类
@Component
@A2uiSkill(
id = "mycustom",
name = "我的自定义组件",
description = "用于特定业务场景的自定义组件",
category = "custom",
componentType = ComponentType.CUSTOM,
priority = 50
)
public class MyCustomSkill extends AbstractA2uiSkill {
@Override
public List<String> getKeywords() {
return Arrays.asList("自定义", "custom", "业务组件");
}
@Override
public String buildGenJson(String moduleName, String caption,
List<String> fields,
Map<String, Object> options) {
// 1. 创建基础属性
JSONObject properties = createBaseProperties(caption, moduleName);
// 2. 添加自定义配置
properties.put("customProp1", options.getOrDefault("prop1", "default"));
properties.put("customProp2", options.get("prop2"));
// 3. 使用模板生成
return buildModuleFromTemplate(moduleName,
properties.toJSONString(), "[]");
}
}
步骤 2:注册到 SPI
public class MyCustomSkillRegistry implements A2uiSkillRegistry {
private static final Map<String, SkillEntry> SKILL_MAP = new HashMap<>();
static {
register("mycustom", "com.example.MyCustomSkill",
"custom", ModuleViewType.CUSTOMCONFIG, ComponentType.CUSTOM);
}
// 实现接口方法...
}
步骤 3:创建 SPI 配置文件
# src/main/resources/META-INF/services/net.ooder.annotation.spi.A2uiSkillRegistry
com.example.MyCustomSkillRegistry
10.2 技能设计原则
- 单一职责:每个技能只负责一种组件类型的生成
- 无状态设计:技能类应该是无状态的,支持并发调用
- 模板复用:使用 FreeMarker 模板提高代码复用性
- 错误处理:在 buildGenJson 中做好参数校验和异常处理
- 关键词优化:提供丰富的关键词以支持 NLP 意图识别
10.3 性能优化建议
图 10-1:性能优化四大策略 - 缓存、懒加载、并发控制、模板预热
10.4 调试技巧
启用详细日志:
# application.properties
logging.level.net.ooder.engine.core.skill=DEBUG
logging.level.net.ooder.annotation.spi=DEBUG
logging.level.net.ooder.web.interceptor=DEBUG
查看已注册技能:
curl http://localhost:8080/api/skill/health
测试技能构建:
curl -X POST http://localhost:8080/api/skill/build \
-H "Content-Type: application/json" \
-d '{
"skillId": "treegrid",
"moduleName": "TestModule",
"caption": "测试模块",
"fields": ["id", "name", "status"]
}'
总结
Ooder A2UI 的 Skills 架构设计通过以下几个关键点实现了框架的现代化重构:
- 注解驱动:使用
@A2uiSkill注解声明式定义技能元数据,简化配置 - SPI 扩展:通过 Java SPI 机制实现技能的动态加载和热插拔
- 统一拦截:
SkillDrivenInterceptor替代多个硬编码拦截器,降低耦合 - 服务分层:清晰的服务层职责划分(管理、发布、桥接)
- 模板方法:
AbstractA2uiSkill提供通用实现,子类专注业务逻辑 - 项目桥接:
SkillProjectBridge实现技能与项目的松耦合关联
这套架构不仅解决了传统拦截机制的痛点,更为未来的扩展提供了坚实的基础。通过 SPI 机制,第三方开发者可以轻松开发自定义技能,丰富 A2UI 的组件生态。
© 2026 Ooder 架构团队 | 保留所有权利