让 Spring Boot Starter 天然支持 AI:Zeka Stack 接入 SkillsJars 的一次探索
让 Spring Boot Starter 天然支持 AI:Zeka Stack 接入 SkillsJars 的一次探索
dong4j背景
最近我一直在重新思考一个问题:在 AI 已经可以大量生成代码的今天,Spring Boot Starter 这类工程脚手架的价值还剩下什么?
过去做 Starter,主要是为了减少重复配置。开发者引入一个依赖,就能获得自动配置、默认 Bean、统一异常处理、统一响应模型、消息队列封装、接口文档集成等能力。它解决的是“少写样板代码”和“统一工程规范”的问题。
但现在 AI 已经能很快生成 Controller、Service、DTO、Mapper,甚至配置文件和测试代码。单纯减少样板代码,已经不再是框架最核心的竞争力。
真正的问题变成了:AI 写出来的代码,是否理解你的工程体系?
它知道项目为什么要用统一响应模型吗?
它知道某个 Starter 已经自动装配了哪些 Bean 吗?
它知道哪些 Bean 可以覆盖,哪些不应该绕过吗?
它知道同一个组件下 Servlet Starter 和 Reactive Starter 应该怎么选吗?
它知道团队约定的异常处理、配置方式、日志方式和扩展边界吗?
如果 AI 不知道这些,它写得越快,偏离工程规范的速度也越快。
这也是我在思考 Zeka Stack 3.0.0 时,一个越来越明确的判断:未来的 Starter 不应该只给人和 JVM 使用,还应该给 AI 使用。
从 README 到 AI 可消费的组件说明
传统 README 是给人看的。它解释组件是什么、怎么引入、有哪些配置、如何运行示例。
但 AI Agent 在真实编码时,并不一定会稳定地读取 README,也不一定知道某个模块里的 docs/、示例工程和自动配置类之间是什么关系。更重要的是,README 往往面向“阅读理解”,而不是面向“编码决策”。
AI 真正需要的是一种更直接的上下文:
- 这个 Starter 适合什么场景
- 它提供了哪些能力
- 它自动装配了哪些 Bean
- 使用方应该如何写业务代码
- 哪些配置可以调整
- 如何覆盖默认 Bean
- 哪些写法是错误的
- 同组件下其他 Starter 应该如何选型
也就是说,Starter 需要一份专门给 AI 看的说明书。
这正好和 SkillsJars 的思路契合:把 SKILL.md 打进 JAR,让 Skill 随 Maven 依赖一起分发。使用方项目依赖了某个 Starter,就可以把这个 Starter 内置的 Skill 解包到 AI 工具目录里,让 AI 在写代码时读取这些组件规则。
SkillsJars 是什么
在设计 Zeka Stack 的 Starter Skill 之前,需要先理解 SkillsJars 解决的问题。
现在不少 AI 编程工具都开始支持 Agent Skills。一个 Skill 通常就是一个带有 SKILL.md 的目录,用来告诉 AI:这个能力什么时候使用、应该遵守什么流程、有哪些约束、不要做什么。
但传统 Skill 的分发方式比较“文件夹化”:复制到 .cursor/skills、.kiro/skills、.claude/skills、.qoder/skills 这类目录中。它适合个人使用,但放到 Java 工程体系里,就会遇到几个问题:
- Skill 怎么跟随依赖版本升级?
- Skill 怎么和组件一起发布?
- 多个项目如何复用同一份 Skill?
- AI 工具目录里的 Skill 怎么保证和当前项目依赖版本一致?
- 一个 Starter 升级后,对应的 AI 使用说明如何同步升级?
SkillsJars 的思路就是把 Agent Skills 纳入 JVM 生态最熟悉的分发体系:Maven / Gradle / JAR。
它把 SKILL.md 和相关文件打包进 JAR,并放在约定目录下:
1 | META-INF/skills/<org>/<repo>/<skill>/SKILL.md |
这样 Skill 就可以像普通 Java 依赖一样被管理:
- 可以发布到 Maven Central
- 可以通过 Maven / Gradle 引入
- 可以跟随依赖版本升级
- 可以作为传递依赖分发
- 可以被构建插件解包到 AI 工具目录
- 也可以被 Spring AI 这类 JVM Agent Runtime 直接从 classpath 读取
SkillsJars 主要有两个动作。
第一个是 Packaging SkillsJars,也就是发布方把 Skill 打进 JAR。项目中维护:
1 | skills/<skill-name>/SKILL.md |
Maven package 时,SkillsJars 插件会把它复制到 JAR 的 META-INF/skills/... 目录。
第二个是 Extracting SkillsJars,也就是使用方把依赖里的 Skill 解包出来。比如业务项目依赖了某个带 Skill 的 Starter 后,可以执行:
1 | ./mvnw com.skillsjars:maven-plugin:0.0.7:extract -Ddir=.cursor/skills |
插件会扫描当前 Maven 项目的依赖图,找到依赖 JAR 中的 META-INF/skills,再解包到 AI 工具能读取的目录。
这对 Zeka Stack 很关键。因为 Starter 本来就是工程规范的入口:它不只是引入依赖,还统一了自动配置、默认 Bean、异常处理、响应模型、消息模型和扩展边界。过去这些规则主要服务于 Spring Boot;现在它们也应该被打包成 AI 可以读取的上下文。
换句话说,SkillsJars 让 Starter 从“代码依赖”进一步变成“代码依赖 + AI 使用说明依赖”。开发者引入一个 Starter,AI 也能同步理解这个 Starter 的正确用法。
为什么 Skill 应该放在具体 Starter 下
一开始我也考虑过在组件根目录放一份 Skill,比如:
1 | cubo-rest-spring-boot/skills/cubo-rest-spring-boot/SKILL.md |
但很快发现这个设计不够准确。
因为 Zeka Stack 的很多组件下面不止一个 Starter。比如 REST 组件下有:
1 | cubo-rest-servlet-spring-boot-starter |
消息组件未来也可能有:
1 | cubo-messaging-kafka-spring-boot-starter |
这些 Starter 面向不同技术栈,使用方式、推荐代码、自动配置 Bean、风险边界都不一样。使用方依赖的是具体 Starter,而不是上层聚合组件。
所以最终规则是:每个直接暴露给使用方依赖的 Starter,都应该维护自己的 Skill。
目录结构如下:
1 | cubo-rest-servlet-spring-boot-starter/ |
这样打包后,Skill 会进入对应 Starter JAR。使用方只依赖 Servlet Starter,就只会解出 Servlet Skill;依赖 Reactive Starter,就只会解出 Reactive Skill。
这个行为正好符合 Maven 依赖的语义。
一个关键约束:Skill 必须自包含
这里还有一个很容易忽略的问题:SKILL.md 会被打包进 JAR,但源码仓库里的 README.md、docs/、示例工程通常不会一起打进去。
所以 Skill 里不能写:
1 | 参考 docs/detail-xxx.md |
因为使用方解包后根本看不到这些文件。
这意味着 Skill 不能只是一个索引,它必须自包含。必要的规则、最佳实践、配置说明、Bean 覆盖方式、选型建议,都要直接写进 SKILL.md。
这和普通文档的写法不同。普通文档可以分层引用,Skill 更像是一个可携带的最小上下文包。
Packaging 和 Extracting 的分工
SkillsJars 里有两个动作。
一个是 Packaging。
这是框架发布方做的事情。Zeka Stack 在 Starter 模块里维护:
1 | skills/<starter-artifact-id>/SKILL.md |
Maven 打包时,插件会把它放进 JAR 的:
1 | META-INF/skills/... |
另一个是 Extracting。
这是使用方项目做的事情。业务项目依赖了 Zeka Stack Starter 后,可以执行:
1 | ./mvnw com.skillsjars:maven-plugin:0.0.7:extract -Ddir=.cursor/skills |
插件会扫描当前 Maven 项目的依赖图,找到依赖 JAR 里的 META-INF/skills,再解包到 AI 工具目录。
这里有一个重要细节:extract 不会扫描本地 .m2 中所有已经安装的 JAR,也不会扫描整个 reactor 的所有模块。它只解析当前项目依赖图里的 JAR,以及插件自己的 dependencies。
所以如果当前项目只依赖了 Servlet Starter,就只会解出 Servlet Skill。这是合理的。
Zeka Stack 里的落地方式
为了避免每个 Starter 都重复配置插件,我把 SkillsJars 的 package 能力放到了组件父 POM 中统一管理。
Starter 模块只负责维护自己的:
1 | skills/<starter-artifact-id>/SKILL.md |
插件配置统一使用:
1 | <skillsDir>${project.basedir}/skills</skillsDir> |
这样每个具体 Starter 都会读取自己模块下的 skills/ 目录。
同时,我还补了一个仓库级 Skill:
1 | supports/skills/zeka-starter-skill-builder/SKILL.md |
它的作用是指导后续为新的 Starter 生成符合规范的 SKILL.md。这个 Skill 固化了几条规则:
- Skill 写到具体 Starter 模块下
- 内容必须自包含
- 生成前必须读取整个组件上下文,而不是只看 Starter POM
- 同组件多个 Starter 必须分别生成 Skill
- 每份 Skill 都要包含同组件其他 Starter 的选型说明
- 不默认添加
allowed-tools - 解包验证必须使用完整 Maven 插件坐标
我希望 Starter Skill 最终提供什么
一个好的 Starter Skill,不应该只是“这个组件怎么引入”。
它应该告诉 AI:
- 这个组件解决什么问题
- 提供了哪些能力
- 推荐怎么写业务代码
- 哪些配置项会影响行为
- 自动装配了哪些关键 Bean
- 哪些 Bean 可以由使用方覆盖
- 覆盖时有什么风险
- 哪些写法会绕过框架约定
- 同类 Starter 应该如何选型
例如 REST Servlet Starter 的 Skill 应该明确告诉 AI:这是 Spring MVC / Servlet 场景,不要生成 WebFlux 的 Mono、Flux;异常处理应该走统一异常模型,不要在 Controller 里临时拼 JSON 错误结构。
Reactive Starter 的 Skill 则应该强调响应式边界,不要在 Reactive 链路里直接混入阻塞式调用,也不要使用 Servlet 专用类型。
这才是 AI 真正需要的工程上下文。
结语
这次接入 SkillsJars,对我来说不是简单地“给项目加几个 Skill 文件”。
它更像是 Zeka Stack 3.0.0 AI Native 方向上的一个基础动作:让 Starter 从“代码依赖”升级为“代码依赖 + AI 上下文依赖”。
过去,Starter 帮开发者少写配置。
现在,Starter 还应该帮助 AI 少走弯路。
当一个业务项目引入 Zeka Stack 的 Starter 时,它不仅获得自动配置和默认 Bean,也应该获得一份可以被 AI 读取的工程规则。这样 AI 生成代码时,才不会只是泛泛地写 Spring Boot,而是沿着 Zeka Stack 定义好的工程轨道前进。
这可能才是 AI 时代下脚手架和 Starter 新的价值:不只是生成代码,而是把工程约束、架构边界和最佳实践,持续地交给 AI 使用。























