ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Swagger注解实战:从零构建高可用接口文档,告别裸奔式API描述

Swagger注解实战:从零构建高可用接口文档,告别裸奔式API描述 1. 接口文档烂在没人写描述Swagger只是把问题暴露出来了先说一个我真实遇到过的场景。去年接手一个维护了三年多的老项目代码里Controller写了一堆Swagger UI也开着看起来好像有文档。可真到对接的时候我对着/api/orders/queryOrderDetail这个接口愣了半天——这个接口是干嘛的参数status到底传什么pageNum从0开始还是从1最后实在没办法只能去翻ServiceImpl里的几百行逻辑才搞清楚原来status还分待支付、已支付、已取消三种而且pageNum确实是从1开始的。这种痛苦想必很多后端开发都体会过。问题根本不在于Swagger这个工具好不好用而在于我们暴露出去的接口和参数几乎没有加任何说明描述。Swagger能自动扫描出接口名、参数名、返回类型但它不是神它读不懂你的业务逻辑。status在代码里是个Integer在文档里就是个integer至于这个整数的业务含义你不在注解里写清楚Swagger永远不可能知道。所以这篇文章解决的就一件事怎么把Swagger生成的这份冷冰冰的接口清单变成一份调用方不看代码也能直接对接的接口说明文档。核心就是用好Swagger提供的注解体系给接口方法、方法参数、实体字段、全局请求头都补上业务描述。文章适合以下三种人看一是刚接触Swagger、想知道注解怎么用的新手二是项目里Swagger已经跑起来了但文档一直处于裸奔状态、想改善的开发者三是需要给团队定接口文档规范的技术负责人。先说透一点Swagger注解这件事跟写注释一样道理谁都知道但真正做好的团队没几个。原因无非是嫌麻烦、不知道怎么组织描述、或者担心写了注解会影响代码可读性。这篇文章我尽量把每一步操作和背后的设计逻辑都讲清楚看完你直接照抄就行。对比一下加注解前后的效果你就能直观感觉到差距在哪里了。没加注解时Swagger显示的是接口名queryOrderDetail参数orderIdinteger、statusinteger、includeItemsboolean返回ResultOrderDetailVO加了注解之后接口名查询订单详情描述根据订单ID查询订单详情可选择是否返回订单商品明细订单不存在时返回404错误码status参数只对历史订单有效参数orderId订单ID必填示例值100233、status订单状态1-待支付2-已支付3-已取消不传则查全部、includeItems是否返回订单商品明细默认false哪个对接起来省心一目了然。下面进入正题。2. 接口方法描述从Api到ApiOperation的完整打法给接口加说明描述第一步不是在参数上花功夫而是先把这个Controller是干什么的这个接口是干什么的说清楚。Swagger里对应两个注解类级别的Api和方法级别的ApiOperation。2.1 Api注解先给整个控制器一个姓名牌一个Controller类通常管理一组相同领域的接口比如UserController管理用户相关的所有操作。Api注解就是贴在这个类上的说明标签告诉看文档的人这一整组接口是干嘛的。Api(tags 用户管理, description 用户信息查询、创建、禁用等相关操作) RestController RequestMapping(/api/users) public class UserController { }这里有个知识点很多人用错了。tags和description看着都能写描述文字但在Swagger UI里的表现完全不同。tags会作为文档左侧的模块名称展示而description只在展开模块时显示在顶部说明区。实际项目中tags的效果比description更直观因为Swagger UI默认按tags分组展示接口列表。推荐做法是一个模块一个tags名称用中文业务名别用英文类名。我看到有人写Api(tags User Management)说实话对接方能看懂中文的话用中文更直接。如果团队有国际化需求那另说但大部分内部项目中文就是最高效的。Api注解里还有个hidden属性设置为true可以让整个Controller不出现在文档里。这个后面第五部分讲隐藏的时候细说这里先有个印象。2.2 ApiOperation接口描述的正文value和notes要分清楚ApiOperation是整个接口说明的核心直接标注在接口方法上。它的属性很多但日常使用频率最高的就这几个value、notes、httpMethod、response。ApiOperation( value 根据ID查询用户信息, notes 返回用户的基本资料包含用户名、邮箱、手机号字段 如果用户不存在返回错误码404 该接口需要登录后才能调用未登录返回401。, httpMethod GET, response UserVO.class ) GetMapping(/{id}) public ResultUserVO getUser(PathVariable Long id) { return userService.getUserById(id); }value和notes的区别一定要搞懂。value是接口的简短标题显示在接口列表的那一行相当于一句话概括。notes是接口的详细说明点击接口展开后才看得到适合描述业务规则、异常情况、调用前提等。我在实际项目中看到的最常见浪费就是把notes当摆设只写value让接口列表看起来像黄页一样干巴巴的。真正有价值的文档恰恰是notes里那些订单不存在时...这类业务边界描述。httpMethod其实可以省略Springfox和springdoc都会根据方法上的GetMapping、PostMapping自动识别。但如果你用了自定义的组合注解或者路由规则比较绕显式写上能让文档展示更稳定。response这个属性要注意它不一定能覆盖所有返回类型。比如接口返回的是ResultUserVO这种统一包装泛型光写response UserVO.class有时候生成的文档里response body还是显示成Result里面的泛型参数反而不展开。这个坑我在第四部分会专门讲排查思路。2.3 从无注释到完整描述一个接口的进化过程拿一个最普通的用户查询接口举例看下完整加注解后的效果。未加注解时方法长这样GetMapping(/{id}) public ResultUserVO getUser(PathVariable Long id) { return userService.getUserById(id); }加了注解后Api(tags 用户管理模块, description 用户相关信息查询与维护) RestController RequestMapping(/api/users) public class UserController { ApiOperation( value 查询用户详情, notes 根据用户ID返回用户的基本资料 用户ID必须存在否则返回错误码404 该接口需要管理员权限。, response UserVO.class ) GetMapping(/{id}) public ResultUserVO getUser( PathVariable ApiParam(value 用户ID, required true, example 1001) Long id ) { return userService.getUserById(id); } }注意这里ApiParam已经出现在参数上了这就是第三部分要展开的内容。看到这个例子你应该也已经发现Swagger的注解体系不是孤立用的它们是叠加的。类上贴Api说明模块方法上贴ApiOperation说明接口参数上贴ApiParam说明入参三者配合起来文档才完整。3. 参数说明的三条路线方法参数、实体字段、全局请求头接口的方法说明做完接下来是重头戏——参数的描述。参数说明这块最容易翻车因为描述入口分散在三处方法参数上的注解、实体类字段上的注解、以及Docket配置里的全局参数。很多人只知其一导致有的参数有说明、有的参数没有文档看起来支离破碎。我把这三条路线分别说清楚。3.1 方法参数怎么加描述ApiParam与ApiImplicitParam方法参数分两种简单的单个参数如RequestParam、PathVariable和复杂的对象参数如RequestBody。单个参数直接在参数前加ApiParam就行GetMapping(/list) public ResultListUserVO getUsers( RequestParam ApiParam(value 当前页码从1开始, required false, defaultValue 1) Integer pageNum, RequestParam ApiParam(value 每页条数最大不超过100, required false, defaultValue 20) Integer pageSize, RequestParam ApiParam(value 用户状态1-正常2-禁用不传则查全部, required false) Integer status ) { return userService.getUsers(pageNum, pageSize, status); }这里有几个属性值得专门说一下。required表示参数是否必填不加的话Swagger默认按非必填处理但如果你方法上的RequestParam设置了required falseSwagger一般也能识别不过保险起见还是显式写清楚。example和defaultValue不一样example只是展示一个示例值方便调用方参考defaultValue表示接口内部的默认值Swagger会在文档里把参数标注为有默认值。如果你的接口本身没有做默认值处理千万别乱写defaultValue否则会误导调用方以为传不传都行。ApiParam理论上能处理大部分单参数场景但有一种情况它搞不定方法签名里没有显式参数、而是从HttpServletRequest里取参数的情况。比如有些老项目喜欢这么写GetMapping(/list) public ResultListUserVO getUsers(HttpServletRequest request) { String pageNum request.getParameter(pageNum); }这时候ApiParam没法直接标注在request上因为Swagger不认为HttpServletRequest是个需要展示的接口参数。解决办法是使用ApiImplicitParamApiOperation(查询用户列表) ApiImplicitParams({ ApiImplicitParam(name pageNum, value 当前页码从1开始, required false, dataType int, defaultValue 1), ApiImplicitParam(name pageSize, value 每页条数, required false, dataType int, defaultValue 20) }) GetMapping(/list) public ResultListUserVO getUsers(HttpServletRequest request) { }注意ApiImplicitParam里的name必须和实际请求参数名完全一致包括大小写。dataType决定了文档里参数的类型展示写成int、string、long等前端好理解的类型名即可。我个人建议能不用ApiImplicitParam就不用能重构方法签名让参数显式化最好因为ApiImplicitParam和实际代码是分离的代码里改了参数名注解没同步改文档就出错了。但如果碰到老代码实在没法动它就是唯一的救命稻草。3.2 实体类字段怎么加描述ApiModelProperty的正确姿势RequestBody传对象、ResponseBody返回对象这两种场景的参数描述要落在实体类上用ApiModel和ApiModelProperty。ApiModel(description 用户创建请求对象) public class UserCreateRequest { ApiModelProperty(value 用户昵称, required true, example 张三) private String nickname; ApiModelProperty(value 用户邮箱, required true, example zhangsanexample.com) private String email; ApiModelProperty(value 用户手机号, required false, example 13800138000) private String mobile; ApiModelProperty(value 用户角色编码多个角色用英文逗号分隔, example ADMIN,OPERATOR) private String roleCodes; ApiModelProperty(value 备注信息, required false) private String remark; ApiModelProperty(value 是否启用默认true, example true) private Boolean enabled; }ApiModel是贴在类上的描述这个对象在业务流程中的作用ApiModelProperty贴在字段上描述每个字段的业务含义。这里最容易出问题的是required的使用。required一旦设为trueSwagger会在文档里把该字段标记为必填项前端对接时会据此做参数校验。如果接口内部实际上没有强校验这个字段只是注解里随手写了required true那前端真的会按必填传结果后端代码里判空的逻辑都没有就会出现前端死等、后端不报错的尴尬。所以required必须跟代码里的校验逻辑严格对齐拿不准就不写或者写false。example这个属性非常有用它能让文档里的请求示例自动带上合理的模拟值。但要注意一个坑ApiModelProperty的example只接受字符串如果你把它用在Date类型的字段上生成的示例会直接展示你填的字符串没有格式转换。所以日期类字段的example最好写成2024-01-01 10:00:00这种可读格式别写成时间戳。另外如果你用了Lombok的Data注解加在字段上完全没问题Swagger是通过反射读取字段上的注解的不受Lombok影响。3.3 全局参数与请求头描述让每个接口都带上必要信息很多项目的接口需要鉴权每个请求都得带上Authorization请求头或某些通用参数。如果只在某个接口的方法上描述其他接口就没有了文档不完整。这时应该用Docket配置里的globalRequestParameters做全局参数。Bean public Docket apiDocket() { return new Docket(DocumentationType.SWAGGER_2) .select() .apis(RequestHandlerSelectors.basePackage(com.example.controller)) .paths(PathSelectors.any()) .build() .globalRequestParameters( Collections.singletonList( new RequestParameterBuilder() .name(Authorization) .description(登录令牌格式Bearer {token}) .required(false) .in(ParameterType.HEADER) .build() ) ); }这段配置加了之后文档里的每个接口都会自动带上Authorization请求头参数说明。这里要区分两个版本Springfox 2.9.x用的是ParameterBuilder也就是com.google.common.collect.Lists配合Parameter构造Springfox 3.0.0以后才引入RequestParameterBuilder。如果你项目里用的是较老的Springfox版本写RequestParameterBuilder会直接编译报错得改用ParameterBuilder。我在给一个老项目升级Springfox时专门踩过这个版本差异后面第四部分细说。全局参数适合放所有接口统一需要的东西比如鉴权头、请求ID、客户端类型。业务相关的参数别塞到全局里否则文档里每个接口都多出一堆无关参数反而干扰阅读。4. 加了注解却不出效果六个高频坑的排查实录注解本身不难写难的是写完发现Swagger UI上没生效。这一部分我把自己和团队遇到过的高频问题集中整理一下每一条都按现象、根因、解法来梳理方便你排查。4.1 坑一文档只显示部分接口扫描路径配置不对现象启动项目后Swagger UI页面只显示了一部分接口或者干脆一个接口都不显示只有个空页面。根因Docket配置里的apis()扫描范围没覆盖到对应的Controller包。Springfox默认通过RequestHandlerSelectors.basePackage(...)来指定扫描哪个包下的接口如果在basePackage里填错了包路径或者Controller根本不在这个包下面那文档肯定空着。解法把basePackage改成Controller实际所在的包.apis(RequestHandlerSelectors.basePackage(com.example.project.controller))这里有个小建议扫描包路径最好精确到Controller所在的那一层别直接扫com.example.project整个工程。扫得太宽会把一些框架自带的接口、监控端点、内部服务接口全暴露到文档里。paths(PathSelectors.any())同理如果你只想暴露/api/**下的接口就写PathSelectors.ant(/api/**)。4.2 坑二RequestBody对象里的字段描述不显示关键看DTO与实体是不是同一层现象接口的RequestBody参数指定了一个类文档里能显示这个类的对象框架但字段的ApiModelProperty描述全不显示只显示字段名和类型。根因大概率是你在Controller方法参数上写的类型和实际字段注解所在的类不一致。最常见的是把POJO实体类直接用作了RequestBody然后字段里的ApiModelProperty写在了一个父类上或者字段没加注解、注解加在了getter方法上。解法一把ApiModelProperty统一写在字段上不要写在getter方法上。虽然Swagger也支持读getter上的注解但一旦类和Lombok配合getter是自动生成的注解写在字段上最稳。解法二别把POJO直接暴露为接口参数新建一个独立的DTO类。实体类通常包含数据库映射信息、审计字段、甚至一些敏感字段直接暴露给前端风险很大。DTO类里的字段可以按接口真实需要的维度重新定义加起ApiModelProperty来也完全可控。4.3 坑三example属性类型不匹配导致生成的文档报错现象文档能打开但点开某个接口时页面报错或者Swagger UI无法正常渲染。根因某些版本的Springfox在解析ApiModelProperty的example属性时会尝试把字符串转换成字段类型。如果你给一个Long字段写了example abcJava侧可能不报错但Swagger生成JSON文档时会解析异常UI渲染就挂了。解法给数值类型字段写example时确保字符串内容是可被转换成对应类型的数字ApiModelProperty(value 用户ID, example 1001) private Long id; ApiModelProperty(value 状态码, example 200) private Integer code;这算是一个比较隐蔽的坑因为它不是编译期报错而是运行时Swagger UI的渲染异常。我在一次给订单接口加文档时把totalAmount字段的example写成了一百二十元结果整个订单模块的文档都打不开了。排查了半天才发现是这个原因排查方式很简单——把该接口对应的/v2/api-docs?groupxxx地址直接在浏览器打开看返回的JSON里有没有无效类型或者看控制台的具体异常日志。4.4 坑四泛型返回类型的参数描述被吞掉现象接口返回ResultUserVO文档里response body只显示Result结构code、msg、data但data里嵌套的具体对象字段不展示。根因Springfox对通用泛型包装类型的解析经常出问题。它默认能识别ResultT这个类的基本结构但不会自动深入到T的具体泛型参数去展示字段详情除非你在ApiOperation里显式指定response并配合responseContainer或者泛型解析配置。解法一在ApiOperation里显式声明response UserVO.class让文档展示UserVO的字段结构ApiOperation(value 查询用户详情, response UserVO.class)但注意这样设置之后响应结构展示的可能是UserVO本身而不是Result包装后的四层结构。如果你希望展示的是Result里嵌套UserVO的完整结构得用泛型解析方式。解法二在Docket上配置GenericTypeResolver或者用AlternateTypeRule处理泛型。Springfox本身提供了对Java泛型的部分支持但实际效果因版本而异。我在项目中比较实用主义的做法是统一封装返回类为ResultT后在ApiOperation的notes里写明响应体为Result结构data字段具体结构见下方UserVO定义通常对接方也能看懂。真要追求完美展示可以用springdoc库它在泛型处理上比Springfox好不少。4.5 坑五文档描述和代码不同步注解成了过期文档现象接口逻辑已经改了但代码里的ApiOperation描述还是旧的。调用方照着文档传参数结果接口返回错误。根因这不是注解用法问题而是团队协作规范问题。Swagger文档本质上是代码的一部分它需要跟代码一起变更。很多团队把写Swagger注解当成一个收尾工作开发完成后补写补完就不再管。等需求变了改代码时注解没同步更新。解法把Swagger注解的完善和修改纳入代码评审的必查项。Code Review的时候不光看逻辑还要看接口描述跟实际行为是否一致。我团队里有一个小约定凡是改了接口的参数含义、必填项、返回值结构提交信息里必须附带对应的Swagger改动说明。久而久之大家就养成了同步更新的习惯。4.6 坑六Springfox 2.x和Swagger 3注解混用文档不识别现象项目里同时引入了springfox-swagger2和io.swagger.core.v3:swagger-annotations依赖字段上用了Schema注解方法上用了Operation注解但Swagger文档完全没显示这些描述。根因Springfox 2.x底层解析的是io.swagger.annotations包下的注解Api、ApiOperation、ApiParam等而Swagger 3即springdoc系列用的是io.swagger.v3.oas.annotations包下的注解Tag、Operation、Parameter、Schema。混用的话Springfox认不出新版注解springdoc也认不出旧版注解结果就是文档活生生变回裸奔状态。解法确定自己项目用的是哪一套生态然后统一。如果你用的是Springfox 2.xspringfox-boot-starter3.0.0之前的版本注解统一用io.swagger.annotations.*如果你用的是springdoc-openapiSwagger 3风格注解统一用io.swagger.v3.oas.annotations.*。别两个依赖都引除非你极其清楚自己在做什么。5. 分组、排序与隐藏把Swagger文档调成产品说明书基础的描述做完了文档能看但不代表好用。一个中型项目上百个接口全堆在一个列表里找接口全靠CtrlF体验还是很差。这部分讲几个能让文档体验上一个大台阶的进阶配置。5.1 分组配置按业务模块拆开文档Springfox支持通过定义多个DocketBean来生成多组API文档每组文档对应一个groupName。Bean public Docket userApi() { return new Docket(DocumentationType.SWAGGER_2) .groupName(用户模块) .select() .apis(RequestHandlerSelectors.basePackage(com.example.controller.user)) .paths(PathSelectors.any()) .build() .apiInfo(apiInfo()); } Bean public Docket orderApi() { return new Docket(DocumentationType.SWAGGER_2) .groupName(订单模块) .select() .apis(RequestHandlerSelectors.basePackage(com.example.controller.order)) .paths(PathSelectors.any()) .build() .apiInfo(apiInfo()); }配置完成后Swagger UI右上角会出现一个下拉框可以切换用户模块订单模块等不同分组。这里强调一下包路径划分要跟Controller的实际包结构对应。如果你们团队是按模块分包如controller.user、controller.order那就按包分如果是按功能分层如controller.web、controller.admin可以考虑按注释Api(tags xxx)配合分组来分。分组的根本目的是降低阅读者的认知负担怎么清晰怎么来。5.2 控制接口列表的展示顺序Swagger UI默认按方法所在Controller的字母顺序和方法名的字母顺序排序跟接口设计的先后顺序、业务调用顺序都没关系。如果你想让创建接口排在查询接口前面或者想让主流程的接口排在第一位就得在注解层面控制排序。Springfox 2.x里可以用ApiOperation的position属性指定顺序ApiOperation(value 创建用户, position 1) PostMapping public ResultUserVO createUser(RequestBody UserCreateRequest request) { } ApiOperation(value 查询用户, position 2) GetMapping(/{id}) public ResultUserVO getUser(PathVariable Long id) { }position数字越小排得越靠前。但注意position在Springfox的某些版本里表现不稳定还会受方法名排序影响。如果你真的需要精确控制顺序更靠谱的方式是在Docket上自定义OperationOrdering。不过说实话大部分团队用不到这种精细控制大致按模块分组够了真要按业务流走建议把接口设计成Restful风格本身就有一定的逻辑顺序可循。5.3 隐藏接口ApiIgnore的正确使用场景Swagger文档是给对接方看的不是所有接口都适合暴露。比如内部调用的接口、系统自动生成的管理接口、还在实验阶段不想让别人调用的接口就可以用ApiIgnore隐藏掉。它可以标注在方法上也可以标注在类上。标注在类上等于隐藏整个Controller。ApiIgnore PostMapping(/internal/refreshCache) public ResultVoid refreshCache(RequestBody RefreshRequest request) { }有一个场景我特别建议用ApiIgnoreSpring Boot Actuator的端点。不少项目把Actuator相关的端点也纳入了Swagger扫描范围导致文档里出现/actuator/health、/actuator/metrics这些和业务无关的地址。这种端点本质上不该出现在业务接口文档里直接在Docket里用paths().none()排除掉就行或者给对应Controller加上ApiIgnore。打个比方Swagger文档就像产品的使用说明书接口列表的展示方式影响的是阅读者的找信息效率。分组是目录排序是章节顺序隐藏是删掉不需要给用户看的内容。三者调好了文档从能看变成好用。写在最后的一点实践补充如果你手头有一个已经跑起来的Swagger项目我建议不要追求一次性把所有历史接口都补上注解。先挑一两个最核心、被调用最频繁的接口模块把ApiOperation、ApiParam、ApiModelProperty都补全让团队看到效果差异。看到好处之后再逐步把其他模块补齐阻力会小很多。我在自己团队推这个事的时候定了两个底线新写的接口必须带注解老接口逐步补。三个月之后文档质量整体就上来了。另外一个很容易被忽略的点Swagger注解里的文字描述尽量让产品、测试、前端都能看懂别写该字段用于系统内部逻辑判断这种模棱两可的话。一条说明写到订单创建时的渠道来源1-APP2-小程序3-线下门店比渠道类型四个字有用得多。文档的价值在于消除信息差写得越具体沟通成本就越低。
返回列表