Ooder A2UI 核心架构深度解析:WEB 拦截层的设计与实现

0 阅读18分钟

从 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 实现了:

  1. 实时响应上下文变化:无需重新生成代码,即可根据用户、模块、数据上下文动态调整 UI
  2. 成本可控:一次性开发技能,无限次复用,避免频繁调用 AI 模型的高昂成本
  3. 质量可控:统一的架构约束和代码规范,确保生成 UI 的一致性和可维护性
  4. 扩展性强:基于 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 拦截层承担着以下核心职责:

  1. 请求路由与分发:智能识别请求类型,分发到对应的处理器
  2. 技能驱动:基于 Skills 架构,实现组件的动态加载和生成
  3. SPI 扩展:通过 Java SPI 机制,支持第三方技能的无缝集成
  4. 性能优化:缓存、懒加载、并发控制等多重优化策略

通过本文的深度解析,您将全面了解:

  • 如何从传统的硬编码拦截器演进到 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 IDComponentCategory
treegridTREEGRIDdata-display
formFORMLAYOUTdata-input
treeTREEVIEWdata-display
galleryGALLERYdata-display
chartECHARTSdata-visualization
navtreeTREEBARnavigation
navigalleryGALLERYnavigation
navtabsTABSnavigation
navmenubarBARnavigation
tabscontainerTABSlayout
layoutLAYOUTlayout

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 技能设计原则

  1. 单一职责:每个技能只负责一种组件类型的生成
  2. 无状态设计:技能类应该是无状态的,支持并发调用
  3. 模板复用:使用 FreeMarker 模板提高代码复用性
  4. 错误处理:在 buildGenJson 中做好参数校验和异常处理
  5. 关键词优化:提供丰富的关键词以支持 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 架构设计通过以下几个关键点实现了框架的现代化重构:

  1. 注解驱动:使用 @A2uiSkill 注解声明式定义技能元数据,简化配置
  2. SPI 扩展:通过 Java SPI 机制实现技能的动态加载和热插拔
  3. 统一拦截SkillDrivenInterceptor 替代多个硬编码拦截器,降低耦合
  4. 服务分层:清晰的服务层职责划分(管理、发布、桥接)
  5. 模板方法AbstractA2uiSkill 提供通用实现,子类专注业务逻辑
  6. 项目桥接SkillProjectBridge 实现技能与项目的松耦合关联

这套架构不仅解决了传统拦截机制的痛点,更为未来的扩展提供了坚实的基础。通过 SPI 机制,第三方开发者可以轻松开发自定义技能,丰富 A2UI 的组件生态。

© 2026 Ooder 架构团队 | 保留所有权利