1. 为什么我最终把项目里的 Swagger 换成了 JApiDocs
先交代一下背景:我在某公司的后端组负责几个 Spring Boot 微服务,之前一直用 Swagger(springfox 那套)生成 API 文档。刚开始觉得挺好,控制器方法一写,注解一标,文档就能自动出来,前端同事也不用老来问参数格式。但用了一年后,问题越来越明显——启动速度越来越慢,一个中等规模的服务启动要 30 多秒,其中一大半时间花在 Swagger 的文档扫描和模式解析上;内存占用也高,本地开发经常因为 PermGen 不够报 OutOfMemory;更麻烦的是注解写起来啰嗦,@ApiImplicitParams、@ApiParam、@ApiModelProperty堆满整个控制器,代码整洁度直线下降。
后来有一次和群里几个同行聊起这个话题,有人提到了 JApiDocs。我一开始没当回事,毕竟 Swagger 已经形成生态习惯了。直到某次项目紧急需要给前端出一份离线接口文档,Swagger 的 JSON 导出来一堆乱码,用 swagger2markup 转换后又丑又难维护,我才认真研究了一下 JApiDocs 这个工具。
JApiDocs 的核心思路是"注释即文档"——不需要写任何额外注解,直接通过解析 Java 源码里的 Javadoc 注释来生成文档。它把"文档"这件事的粒度从"注解驱动"彻底换成了"源码原生驱动"。对于 Spring Boot 项目来说,这意味着:
- 不需要引入一堆运行时依赖,也就不会拖慢启动速度;
- 文档紧跟代码,注释改了,生成的文档就变了,天然避免"改代码忘改文档"的问题;
- 生成的是静态站点/静态文件,可以直接放在服务器上给前端、测试、产品看,也可以纳入 Git 版本管理。
我用了一个下午把项目里某个订单服务模块迁移过去,测了几个接口,效果远超预期。这篇文章就把我从入门到实际用坑的全部经验整理出来,希望能帮到所有还在被 Swagger 注解折磨的 Spring Boot 开发者。
2. 环境准备与 Spring Boot 项目接入的全流程
2.1 导入依赖与版本选择的细节
JApiDocs 目前最新稳定版是 1.4.4(截至我写这篇文章时的经验),官方发布的 Maven 坐标为io.github.yedaxia:japidocs。在你的 Spring Boot 项目pom.xml中添加:
<dependency> <groupId>io.github.yedaxia</groupId> <artifactId>japidocs</artifactId> <version>1.4.4</version> <scope>provided</scope> </dependency>这里有个容易被忽略的关键点:scope建议用provided。因为这个工具只在编译期做源码解析和文档生成,运行时完全不需要它,用provided可以避免把无关的依赖打到生产包中。我第一次接入时图省事没加 scope,结果打出来的 fat jar 胖了好几 MB,排查半天才发现是这个原因。
JDK 环境方面,JApiDocs 是基于 Java 源码解析器(JavaParser)实现的,对 JDK 版本兼容性做得比较激进。我在 JDK 8、JDK 11、JDK 17 三个环境下都跑过,都没问题。如果你用的是 JDK 21 或更新版本,建议先跑一个小 Demo 验证一下,我目前还没有在 JDK 21 上实际测试过,不确定新版javadoc注释格式是否有兼容性问题。
2.2 最基础的生成入口:写一个 main 方法即可
这里和 Swagger 最大的区别就体现出来了。Swagger 是运行时通过拦截器扫描@RestController下的所有 RequestMapping 元数据;而 JApiDocs 是一个普通的 Java 库,你只需要在任意地方写一个main方法就能生成文档。
最基础的用法是:
import io.github.yedaxia.apidocs.Docs; import io.github.yedaxia.apidocs.DocsConfig; public class ApiDocGenerator { public static void main(String[] args) { DocsConfig config = new DocsConfig(); config.setProjectPath("你的项目根路径"); // 绝对路径,要求是包含 pom.xml 的根目录 config.setProjectName("订单服务 API"); config.setApiVersion("V1.0.0"); config.setDocsPath("你的项目根路径/src/main/java"); config.setAutoGenerate(Boolean.TRUE); config.setMvcPattern("com.example.controller.*"); // 控制器包名通配符 Docs.buildHtmlDocs(config); } }把这个 main 方法放在src/test/java目录下的某个工具类中即可,既不影响运行时代码,又能方便地随时执行。
执行完成后,你会发现在项目根目录下多了一个docs文件夹,里面包含了index.html等一批静态文件。直接用浏览器打开index.html,一份完整的、带侧边栏导航的 API 文档就展现在眼前了。整个过程干净利落,没有容器启动,没有端口占用,没有任何运行时依赖。
2.3 配置项逐个拆解:项目里实际用到的几个参数
JApiDocs 的配置核心是DocsConfig类,下面几个字段是按重要性排序的,我逐个解释用途和选值的依据:
| 配置项 | 作用 | 我的建议值 |
|---|---|---|
setProjectPath | 项目根路径,用于解析源码文件 | 必须是包含pom.xml的绝对路径,多模块项目定位到聚合根目录 |
setDocsPath | 要扫描的 Java 源码目录 | 单模块用src/main/java,多模块需按模块分别扫描 |
setMvcPattern | 控制器类的包名通配符,过滤要生成文档的类 | 只写 Controller 所在包,避免扫描到无关类,加快生成速度 |
setAutoGenerate | 是否自动生成 Model 类文档 | 为true时,会自动为文档里出现的实体类生成字段说明 |
setProjectName | 文档显示的项目名称 | 建议写业务系统名而不是代码模块名,前端同事看起来更友好 |
另外还有一个setTrustHost之类的测试相关配置,那个是给官方调试用的,实际项目基本用不到,不用理会。
配置文件路径这块我踩过一次坑:如果你用 Maven 多模块项目,每个模块一个src/main/java,那么setDocsPath只能填一个目录。解决方法是分多次生成,或者使用多模块聚合方案,后面我会单独讲。
3. 注释怎么写,JApiDocs 才能生成高质量文档
3.1 控制器类注释的规范和示例
JApiDocs 识别文档信息的来源完全依赖 Javadoc 注释,所以注释写得好不好,直接决定了文档质量。先说控制器类级别的注释规范。
来看一个标准的示例:
/** * 订单接口 * * 这里可以写一段更详细的描述,例如: * 本模块负责订单的创建、查询、取消等操作,供商城前端和后台管理系统调用。 * 所有接口在调用前均需通过网关鉴权,请求头中需携带 token。 * * @author 某开发者 * @date 2024-06-01 */ @RestController @RequestMapping("/api/order") public class OrderController { }类注释中的@date、@author等内容 JApiDocs 会原样显示到文档页面的"描述"区域,这些信息对于团队内部明确接口负责人很有用。但要注意的是,JApiDocs 不会解析@author里的邮箱、电话等隐私格式,它只会做纯文本展示。所以不要在注释里放敏感信息。
3.2 方法注释里的 @param 和 @return 是重中之重
单个接口方法的文档质量,完全取决于@param和@return有没有写清楚。JApiDocs 会把 Javadoc 中每个@param对应到接口的一个请求参数,所以在方法注释里写参数说明,就是定义接口文档参数的最直接方式。
看一个典型的 POST 接口写法:
/** * 创建订单 * * 新订单创建后状态为待支付,超时未支付会被自动取消。 * * @param req 创建订单请求体,包含商品明细、收货地址等信息 * @return 创建成功的订单信息,包含订单号和订单金额 */ @PostMapping("/create") public Result<OrderVO> createOrder(@RequestBody OrderCreateReq req) { return orderService.create(req); }这里要特别注意:JApiDocs 会自动根据方法签名推断请求类型(POST、GET)、路径(/api/order/create)和参数位置(body 参数还是 query 参数)。如果你想让req对象展开为内层的字段(即多个子参数),只要在OrderCreateReq类的字段上加注释即可,这个下面接着说。
3.3 实体类字段注释:直接对应文档的 Model 表格
这也是 JApiDocs 最让我舒心的一点:实体类不需要任何注解,字段的"参数名"天然是 Java 字段名,字段的"含义"天然取自字段注释。
public class OrderCreateReq { /** * 商品编号,必传 */ private String productId; /** * 购买数量,取值范围 1-999 */ private Integer quantity; /** * 收货地址ID,需提前在用户地址簿中创建 */ private Long addressId; /** * 订单备注,选填,最长 100 字 */ private String remark; // 省略 getter/setter }这样写完后,生成的 HTML 文档中关于"请求参数"的表格会自动列出:productId、quantity、addressId、remark以及对应的中文说明。整个流程没有任何注解参与,Java 代码就是文档的唯一来源。
如果你在字段注释中用@see标签指向另一个类,JApiDocs 还会自动生成关联跳转,这个可以用在一些嵌套对象的场景上。
3.4 让文档更直观的进阶注释技巧
我在实际项目里慢慢摸索出几个让文档更好用的小技巧:
- 在方法注释里用
<p>标签分隔多个段落,JApiDocs 会保留 HTML 换行,文档更清晰; - 在字段注释里写清楚"传参格式",例如
格式: yyyy-MM-dd HH:mm:ss,前端直接照着拼就行了; - 在类注释里列出该接口模块的注意事项,例如"所有金额单位为分"这类容易引起联调歧义的信息;
- 在
@deprecated注释里写清楚"推荐替代哪个接口",JApiDocs 会在文档中标注过时标记,避免前端误用。
注意,JApiDocs 对 Javadoc 的解析是区块式的,它不太支持把多个标签挤在同一行。所以每个@param尽量独占一行,保持规范,解析才稳定。
4. 实际使用中的三组核心功能:分组、参数映射和离线文档
4.1 多控制器分组:让接口目录不再"大锅汇"
一个中大型 Spring Boot 服务的 Controller 少则五六个,多则十几个。JApiDocs 默认会把扫描到的所有 Controller 全部平铺在文档左侧栏,看起来就像菜市场一样混乱。
实际上,JApiDocs 提供了基于包名的分组逻辑。只需要把不同业务模块的 Controller 分别放在不同的包下,然后在mvcPattern中制定匹配规则,JApiDocs 就会按照包结构生成层级目录。
举例来说:
config.setMvcPattern("com.example.order.*.controller.*");如果你把订单相关控制器放在com.example.order.controller,把用户相关控制器放在com.example.user.controller,那么文档侧边栏会自动形成"order""user"两个分组,每个分组下再按类名列出接口条目。前端同事说这个分组视图比 Swagger 按注解分组好用多了——因为注解分组是逻辑上的,代码路径分组是物理上的,按代码结构来找接口会更直观。
对于跨多个系统的大型项目,也可以在启动参数里通过多个mvcPattern配合多个 Controller 的包路径实现更细粒度分组。我实际的做法是:每个后端模块独立生成一套文档,然后通过下文要讲的"多模块聚合"把文档合并,每个模块天然一个分组。
4.2 @RequestBody 和 @PathVariable 的自动识别逻辑
JApiDocs 的智能之处在于,它不需要你额外指出某个参数是 body 还是路径参数,它会自动根据 Spring MVC 的注解来判定。它的识别规则大致如下:
| Spring 注解 | JApiDocs 识别结果 |
|---|---|
@RequestBody | 请求体(body),会在文档中展开为 JSON 对象形式 |
@PathVariable | 路径变量,显示为路径替换占位符/api/user/{id} |
@RequestParam | query 参数,显示为表单键值对 |
| 无注解且为普通类型 | 默认视为 query 参数 |
| 无注解且为自定义对象 | 默认展开 body 参数 |
我最初以为这样一个"无注解"的库在参数识别上会很弱,实际测试后发现,它对 Spring MVC 的标准注解识别准确率非常高。不过有一类特殊情况要多留意:如果你在参数上同时加了@RequestParam却没有指定value和required,JApiDocs 会展示字段名但丢失"是否必传"信息。我现在的习惯是:在@RequestParam注解中显式写明required = true/false,同时确保字段注释里写清"必传/选填",双保险。
4.3 生成 Markdown 和 PDF:离线交付的几种方案
Spring Boot 接口文档最常见的消费场景不仅仅是网页在线查看,更多时候需要以 Markdown 或 PDF 形式交付给测试组、产品经理或外部合作方。
JApiDocs 提供了多种输出格式:
// 生成 HTML Docs.buildHtmlDocs(config); // 生成 Markdown Docs.buildMarkdownDocs(config); // 生成 PDF(底层走 HTML 导出后调用浏览器打印) Docs.buildPdfDocs(config);buildPdfDocs其实是在 HTML 基础上调用了内置的打印引擎,所以对输出目录有一定要求。如果你只需要给外部交付一份不可编辑的文档,Markdown 转 PDF 是更可控的路线。我个人更常用的组合是:先buildHtmlDocs生成网页版,再对 HTML 页面使用浏览器的"另存为 PDF"功能,版式保持得最好。
还有朋友问 JApiDocs 能不能导出 OpenAPI(Swagger 格式)的 JSON。答案是:官方没有直接支持,但存在一个社区方案,把 JApiDocs 生成的模型通过自定义模板映射成 OpenAPI JSON。这个在下文自定义模板部分会展开。
5. 实战踩坑记录:三个最容易翻车的场景
5.1 泛型返回类型导致文档参数显示不全
我第一次接入 JApiDocs 后,生成文档时发现一个问题:某个Result<T>包装类里,T这个泛型参数对应的实际类型在文档中显示成了Object,而不是具体的OrderVO。
后来定位到原因:JApiDocs 在解析带泛型的方法签名时,如果泛型变量在继承链中没有被"具体化"到某个确定的类型,它就不知道如何展示。换句话说,public Result<T> data()这类泛型方法签名,必须让 JApiDocs 能在源码中找到T的赋值类型。
我的解法是:在接口方法的返回类型里,不要写裸泛型,而是显式写明具体的泛型参数,例如:
// 正确写法 public Result<OrderVO> getOrderDetail(@RequestParam Long orderId) // 有问题的写法 public Result<T> getOrderDetail(@RequestParam Long orderId)这个方法改动虽小,但直接影响了文档的可用性。前端拿到Result<OrderVO>能清楚地看到返回 data 里面是订单号、金额、状态等字段;而Result<T>则只能看到data: {}。
5.2 多模块 Maven 项目如何聚合生成一份完整文档
微服务项目一般按功能拆分成多个 Maven 模块:api-gateway、user-service、order-service、common。每个模块都有独立的src/main/java,那么 JApiDocs 的setDocsPath就很尴尬——它只能填一个目录。
我尝试过几种方案,最终可行的是写一个生成器 main 方法,按模块多次创建DocsConfig并执行:
String root = System.getProperty("user.dir"); String[] modules = {"user-service", "order-service", "product-service"}; for (String module : modules) { DocsConfig config = new DocsConfig(); config.setProjectPath(root + "/" + module); config.setDocsPath(root + "/" + module + "/src/main/java"); config.setMvcPattern("com.example." + module.replace("-", "") + ".controller.*"); config.setAutoGenerate(Boolean.TRUE); Docs.buildHtmlDocs(config); }注意,这样生成的文档会各自写到对应模块的docs目录下。如果你希望所有服务接口一键聚合到一个统一的站点,最简单的办法是:在生成器 main 方法的设计上,把所有模块的输出路径统一设置为同一个目录下的不同子目录,即"聚合根目录/docs/user-service/""聚合根目录/docs/order-service/",然后再用 Nginx 或静态网页工具(如 docsify、VuePress)做一个总入口,按接口归属模块索引。
我目前在公司内部就是这样做的:一个总的"API 文档站"静态目录,每次重新生成后由 CI 任务自动同步到内网服务器,前端只用访问一个地址就能在侧边栏切换不同模块的接口文档。
5.3 与 Spring Security/拦截器的冲突:其实根本没有冲突
很多刚接触 JApiDocs 的读者会担心:我的项目里有 Spring Security,接口必须登录才能调用,那 JApiDocs 是不是识别不了?
这里要澄清一个概念:JApiDocs 是编译期/独立运行期的源码解析工具,它的main方法一旦执行,就只做"读 Java 源码 + 生成静态 HTML"两件事。它完全不经过 Spring 容器,既不启动应用,也不拦截请求,所以 Spring Security、拦截器、过滤器、AOP 这些运行时组件对它毫无影响。
JApiDocs 不需要应用启动,反而意味着你可以在开发环境没有数据库、没有 Redis、没有注册中心的情况下,随时生成最新接口文档。这个特性在接口频繁变动的迭代期非常实用。
而我在这部分踩到过唯一的"假坑"是:有些接口方法上加了自定义注解,比如@RepeatSubmit、@Idempotent,这些注解不在 JApiDocs 的识别范围里,它不会在文档里展示。如果你希望这些业务约束能体现在文档中,最好的办法是在方法 Javadoc 中用<p>标注,例如"该接口支持幂等"。
6. 从入门到进阶:自定义模板与集成 CI 的完整思考
6.1 修改文档模板:让文档打出公司品牌
JApiDocs 的默认模板其实比较朴素,类似开源项目的默认 API 文档风格。对于要交付给客户或者公司内部统一规范化使用的场景,自定义模板就很有必要。
JApiDocs 提供了模板扩展机制:生成 HTML 文档时,默认加载的模板位于 jar 包内的japidocs/resources/templates/下。你可以调整DocsConfig中setTemplatePath指向自定义模板目录:
config.setTemplatePath("classpath:custom-templates/");具体模板目录内需要覆盖的文件可以参照官方文档。我只补充两点实践心得:
- 模板是用 FreeMarker 编写的,如果你的项目里没有用过 FreeMarker,改模板最好先在本地跑通默认模板,再逐步替换其中的变量;
- 自定义模板时,注意 HTML 页面里引用的静态资源(CSS、JS)路径,JApiDocs 会复制一份静态资源到输出目录,如果你改模板却没有同步静态资源路径,生成的页面样式可能会乱掉。
我在一次给某政务类项目定制文档时,只是把文档首页的 Logo、标题、页脚全部换成统一风格,然后加上了公司统一的 favicon,整体工作量大约一个上午,比改造 Swagger UI 的样式要省事得多。
6.2 与 Git 版本管理配合:让每次接口变更有迹可循
JApiDocs 生成的 HTML 是静态文件,这给了它一个很大的优势:可以纳入 Git 版本管理。每次接口变更后重新生成文档,然后提交 Git,相当于文档也有了版本历史。前端同事和测试同事可以通过 Diff 看到本次迭代的接口变更,极大减少了沟通成本。
在我目前的团队,我把文档生成器写成一个 Test 类ApiDocGenerator,然后在 CI 流水线的测试阶段末尾增加一个步骤:执行这个 main 方法,将生成的docs/目录上传到内网文档服务器。这样做的好处是:
- 不需要单独部署一套文档服务;
- 每个人本地跑一遍命令也能生成,用于自测;
- 文档更新和历史记录天然沉淀在 Git 仓库里。
6.3 多模块聚合方案的另一种思路:先合并源码再生成
前面提到多模块聚合可以通过多次调用来解决,这里再提供一种我实测可行的思路:在 CI 上,把多个模块的src/main/java通过 symlink 或拷贝方式合并到一个临时目录,然后只调用一次DocsConfig,把docsPath指向临时目录,mvcPattern使用覆盖所有模块的正则,生成一份全量文档。
但这个方案有一个局限:如果两个模块有同名类(比如各有一个Result包装类),合并时会互相覆盖导致解析结果错乱。所以这个方案更适合包名隔离得足够好的项目。如果你对多模块结构没有那么强的一致性约束,我还是推荐多次调用派生的方案,稳定且直观。
6.4 JApiDocs 与 Swagger 生态的边界:什么时候不该用
这篇文章虽然全篇都在讲 JApiDocs 的好,但我还是要客观地说说它的边界,避免读者盲目替换把自己坑了。
以下场景我觉得还是保留 Swagger 或 SpringDoc 更合适:
- 团队对 OpenAPI 规范有硬性要求,例如接口文档需要直接导入 Postman、Apifox 或某些 API 网关产品。JApiDocs 目前没有官方的 OpenAPI 导出能力,虽然社区有 hack 方案,但维护成本高;
- 需要在线调试接口。JApiDocs 生成的文档是纯静态的,不能像 Swagger UI 那样直接在页面上发起请求调接口。对于前端联调阶段来说,这个缺失还是有点明显。不过我们团队已经改用 Apifox 来直接联调,JApiDocs 负责提供数据结构参考,两者互补效果很好;
- 对象关系复杂、接口数量庞大且依赖注解驱动的特殊逻辑。JApiDocs 的解析规则相对简单直接,遇到非常复杂的泛型嵌套时,虽然不至于失败,但展示效果可能不尽如人意。
简而言之:JApiDocs 更适合"文档为主、联调为辅"的团队协作模式;如果团队已经重度依赖 Swagger 的调试功能和 OpenAPI 联结生态,迁移要慎重。
7. 生产环境里的一个优化实践:从文档到 Mock 数据
最后分享一个我认为很有价值的进阶用法。JApiDocs 生成的 Markdown 文档中,每个接口的参数表和返回值结构都是结构化的。我写了一个小脚本,解析 JApiDocs 生成的 Markdown 表格,自动生成对应接口的 Mock 数据(随机生成符合字段说明的示例参数),然后配合 Mock 服务让前端在没有后端环境时也能先行开发。
具体流程是:
- 后端修改好接口代码和 Javadoc 注释;
- 执行
Docs.buildMarkdownDocs生成 Markdown 文件; - 脚本读取参数表格,识别字段名、类型、是否必传;
- 根据字段注释中的限制(如"取值范围 1-999")生成 Mock 模板;
- 将 Mock 模板更新到 Mock 服务配置。
这个流程跑通后,前后端并行开发的效率提升非常明显。因为 JApiDocs 的文档本身就是接口的最新定义,Mock 数据不会出现"前端等后端改完才跑得通"的问题。
在实际操作中,我建议给实体类的字段注释写得尽量"机器可读",比如用"必传"、"选填"和"取值范围"这些关键词,脚本解析起来会更准确。如果注释写得随意,Mock 生成的字段可能不符合真实约束。
8. 一点个人体会
从 Swagger 换到 JApiDocs,我最大的感受不是"工具变了",而是"写文档的方式变了"。以前我写接口文档,是在写完代码后,再去对照接口写注解;写注解时心里想的是 Swagger 的规范,而不是接口本身。但 JApiDocs 强迫我回归到最原始、最自然的 Javadoc 注释上——写注释的时候,我关注的是这个接口的业务说明、参数约束、返回含义。文档最终变成代码的一部分,而不是附加在代码上的额外负担。
如果你所在的团队厌倦了 Swagger 注解的臃肿和启动时的沉重,或者你只是希望给自己的 Spring Boot 项目留一份简洁、可版本化、不依赖运行时的接口文档,JApiDocs 值得你用一小时做个小 Demo,再决定要不要全面推广。我个人从 1.4.x 版本用到现在,最核心的一条经验是:想让文档好用,先让注释好写。只要团队把注释规范立起来,JApiDocs 生成的文档质量能超过大多数商业化 API 文档工具。