ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Spring Boot中Swagger接口描述注解详解:从Controller到参数说明

Spring Boot中Swagger接口描述注解详解:从Controller到参数说明 1. 接口文档的描述困境与Swagger的解法我在实际项目里见过太多次这样的场景后端接口调通了功能也正常但前端同事拿着接口文档一脸懵——这个参数到底传什么格式那个字段是必填还是选填枚举值有哪些时间戳要不要带时区问后端后端忙得没空理你翻代码翻了半天也在service层里出不来。这就是接口描述缺失带来的经典问题。Swagger作为一套接口文档工具核心能力不只是自动生成文档更关键的是它允许你在代码里“原地”补充描述信息让接口、参数、模型字段都带上人能看懂的解释。说白了Swagger不只是给你看接口长什么样的它是让你把接口“说清楚”的。这篇文章我就围绕“如何在Swagger中给暴露的接口及参数添加说明描述”这个主题把从Controller到实体类、从全局配置到细节注解的完整做法梳理一遍。内容基于Spring Boot集成Swagger/SpringDoc的常见实践也包含Knife4j增强插件的使用经验适合正在为接口文档发愁的后端开发者参考。2. Swagger注解体系一套注解两套作用2.1 Swagger的注解分类逻辑Swagger描述接口的方式本质上可以拆成三个层次接口层、参数层、模型层。接口层描述这个接口是干什么的比如“根据用户ID查询订单列表”参数层描述每个参数的名称、类型、是否必填、取值范围模型层描述请求体或响应体里的实体字段比如User对象的name字段表示什么、长度限制是多少。对应到代码里这三层分别由Api、ApiOperation、ApiParam、ApiModel、ApiModelProperty这些注解来承担。很多人容易把它们混在一起用或者干脆全堆在接口上结果文档还是乱七八糟。理解分层逻辑之后写注解就清晰多了。需要注意的是目前Swagger在Spring Boot项目里有两套主流版本SpringfoxSwagger 2和SpringDocOpenAPI 3。Springfox常用的注解包名是io.swagger.annotations.*SpringDoc则是io.swagger.v3.oas.annotations.*。虽然包名不同但解决的描述问题是一样的。下面讲的核心思路两套通用具体注解名以Springfox为主SpringDoc用户对应替换即可。2.2 注解与文档展示的映射关系Swagger生成的文档页里每个接口对应一张卡片卡片上有接口地址、请求方式、接口说明、参数列表、响应状态码等信息。接口说明来自ApiOperation的value和notes控制器分组说明来自Controller类上的Api的tags参数说明来自ApiParam的value以及实体类字段上的ApiModelProperty的value响应说明来自ApiResponses配合ApiResponse。把这些对应关系记在心里写注解的时候就不会“想写却不知道写在哪”。下一节开始逐一演示具体写法。3. 给接口方法添加说明描述3.1 ApiOperation一句话讲清接口职责ApiOperation是给单个接口方法添加说明的注解也是整个文档里最容易被看到的信息。RestController RequestMapping(/api/user) Api(tags 用户管理) public class UserController { GetMapping(/{id}) ApiOperation(value 根据ID查询用户信息, notes 传入用户主键ID返回用户详情不存在时返回404) public ResultUserVO getUser(PathVariable Long id) { // 业务逻辑省略 } }value是接口的简要说明通常一句话讲清楚接口做什么notes是详细说明可以补充注意事项、异常场景、特殊逻辑。我建议value控制在10到20个字notes可以写两三句话把这个接口的边界条件和返回值行为交代清楚。这里有一个经验value不要写成“查询接口”这种没有信息量的话。前端在文档里看到value之后应该能判断这个接口和自己要做的需求是否匹配这就够了。3.2 通过ApiResponses补充响应说明接口说明不只是“请求是啥”还得告诉调用方“会返回什么”。ApiResponses配合ApiResponse可以给不同响应码附加说明。GetMapping(/{id}) ApiOperation(value 根据ID查询用户信息) ApiResponses({ ApiResponse(code 200, message 查询成功), ApiResponse(code 404, message 用户不存在), ApiResponse(code 500, message 服务器内部错误) }) public ResultUserVO getUser(PathVariable Long id) { // 业务逻辑省略 }这个写法在配合前端联调时特别有用。很多前端同事看文档只看“200”等到接404的时候才发现没处理来回问人。你把响应码和语义写清楚能减少大量沟通成本。3.3 隐藏不需要暴露的接口在实际项目里Controller里常常有一些内部使用的接口比如定时任务触发的接口、运维专用的接口、或者临时调试的接口。这些不应该出现在对外的文档里。ApiOperation(value 内部专用刷新用户缓存, hidden true) PostMapping(/refreshCache) public ResultVoid refreshCache() { // 业务逻辑省略 }ApiOperation的hidden属性设为true后该接口就不会出现在Swagger文档里。同样ApiIgnore也可以实现类似效果。我的建议是所有非业务接口一律加上隐藏标记避免前端误调用。4. 给接口参数添加说明描述4.1 路径参数与Query参数直接用ApiParam参数描述是接口文档里最容易被忽视、又最容易出问题的地方。先看最简单的场景——路径参数和Query参数。GetMapping(/{id}) ApiOperation(value 根据ID查询用户信息) public ResultUserVO getUser( ApiParam(value 用户主键ID, required true, example 1001) PathVariable Long id, ApiParam(value 是否包含已删除用户, allowableValues true,false, defaultValue false) RequestParam(required false) Boolean includeDeleted ) { // 业务逻辑省略 }这里几个属性的作用value参数的中文说明告诉调用方这个参数是什么required标记是否必填和RequestParam的required保持一致example给一个示例值前端可以直接复制调试allowableValues限制可选值范围比如枚举、布尔等。example这个属性很多人不重视但实际联调时前端最爱的就是这个。拿1001这种真实形状的示例值比看描述猜格式好用得多。4.2 请求头参数与隐藏参数有些接口需要传递自定义请求头比如X-Request-Id、Authorization之类。ApiImplicitParam可以描述这类参数。GetMapping(/list) ApiOperation(value 分页查询用户列表) ApiImplicitParams({ ApiImplicitParam(name X-Request-Id, value 请求追踪ID, required true, paramType header, example abc-123), ApiImplicitParam(name page, value 页码从1开始, defaultValue 1, paramType query), ApiImplicitParam(name size, value 每页条数最大100, defaultValue 20, paramType query) }) public ResultPageResultUserVO listUsers(RequestParam Integer page, RequestParam Integer size) { // 业务逻辑省略 }paramType支持header、query、path、body等取值按参数实际所在位置填写。请求头参数在文档中会显示在Parameters区域前端用Postman或Apifox调试时可以直接看到。4.3 实体对象参数ApiModel与ApiModelProperty当接口参数是一个实体对象时比如POST请求的JSON体光在方法参数上写ApiParam是远远不够的。需要在DTO类上使用ApiModel和ApiModelProperty把字段级别的描述补齐。ApiModel(value 创建用户请求参数) public class UserCreateRequest { ApiModelProperty(value 用户名, required true, example zhangsan, maxLength 32) private String username; ApiModelProperty(value 手机号, required true, example 13800138000, pattern ^1[3-9]\\d{9}$) private String phone; ApiModelProperty(value 用户角色, allowableValues ADMIN,USER,GUEST, defaultValue USER) private String role; ApiModelProperty(value 用户年龄, minimum 0, maximum 150) private Integer age; ApiModelProperty(value 备注信息, dataType string, notes 最长不超过200字符) private String remark; // getter/setter省略 }ApiModel加在类上ApiModelProperty加在字段上。对于常用字段像用户名、手机号、邮箱这些我建议在项目里统一约束required是否必填必须和校验注解一致example一定给前端调试会感激你allowableValues枚举字段一定要写pattern正则约束写上去能避免前端反复试错。加完这些之后Swagger文档里会展示一个完整的JSON示例前端直接点“Try it out”就能把整个请求体复制出来。这是我自己最常用也最推荐的做法。4.4 枚举参数的优雅描述方式枚举字段是接口文档里最容易产生歧义的地方。如果只写“用户角色”前端根本不知道传ADMIN还是1。public enum UserRole { ADMIN(管理员), USER(普通用户), GUEST(访客); private final String desc; UserRole(String desc) { this.desc desc; } public String getDesc() { return desc; } }在DTO里配合ApiModelProperty使用ApiModelProperty(value 用户角色, allowableValues ADMIN,USER,GUEST, notes ADMIN-管理员,USER-普通用户,GUEST-访客) private UserRole role;notes在这里没有专门的枚举语义位但可以人为约定把枚举值和中文含义写在一起。这是很多团队实际在用的方案简单有效。如果用的是Swagger 3/SpringDoc还可以直接写Schema(allowableValues {ADMIN, USER, GUEST})语义更明确。5. 给接口模块与文档整体添加说明5.1 Api让模块分组更清晰当一个项目有几十个接口时按模块分组是文档可用的前提。Api的tags属性就是干这个的。RestController RequestMapping(/api/user) Api(tags 用户管理) public class UserController { // 接口方法省略 }注意Api的value属性虽然也能描述模块但实际显示效果不如tags直观。Springfox里value主要作为默认标签名多个Controller的tags不同才能实现分组description属性可以补充模块的详细说明比如“用户注册、登录、信息查询等操作”。我习惯在tags里写“模块名——子模块”的格式比如“用户管理——账号管理”、“用户管理——权限配置”。当Controller数量多时这种命名比单一“用户管理”更容易检索。5.2 多Docket下的分组说明配置在微服务或中大型单体项目里一个Swagger文档常常要按业务域拆分成多个分组。Docket的groupName加上Api的tags组合可以实现“多组文档、各自说明”。Configuration public class SwaggerConfig { 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()); } }分组的核心逻辑是包路径隔离每个Docket对应一个业务域。配合每个Controller上的Api(tags xxx)文档里就能实现“分组——模块——接口”三层结构。5.3 全局接口信息配置Docket上的apiInfo是文档首页展示的整体描述。很多项目默认不配置这个导致文档打开只有Swagger默认的标题“Api Documentation”看起来就像没做完一样。private ApiInfo apiInfo() { return new ApiInfoBuilder() .title(用户中心API文档) .description(本文档描述用户中心对外提供的所有RESTful接口包含注册、登录、用户信息查询与修改等能力。) .version(2.1.0) .termsOfServiceUrl(https://example.com) .contact(new Contact(技术团队, https://example.com, devexample.com)) .license(内部使用) .licenseUrl(https://example.com/license) .build(); }这里的description不要照抄模板把模块范围、部署环境、联调注意事项都写进去。我自己会在description里补一句“基础环境地址http://dev-api.example.com”前端拿到文档不用再问环境配置。5.4 Knife4j增强插件的说明体验Knife4j是Swagger的增强UI插件不只是界面好看它对描述信息的呈现也更清晰。在pom.xml中引入依赖后原来的注解基本不用改就能获得更好的文档展示效果。dependency groupIdcom.github.xiaoymin/groupId artifactIdknife4j-spring-boot-starter/artifactId version3.0.3/version /dependencyKnife4j支持的增强功能包括对ApiOperation的说明进行更友好的排版支持接口调试时自动填充Example值导出Markdown/Word格式文档方便线下评审全局搜索接口接口多的时候很好用。我推荐在项目里直接上Knife4j它能在不改代码、不重写注解的前提下把描述信息的呈现质量提升一个档次。6. 常见问题与排查技巧实录6.1 注解写了但文档不显示怎么办这是最常遇到的问题。Swagger文档里接口有但ApiOperation的说明不显示或显示成方法名一般是以下原因第一注解导错包了。Springfox使用io.swagger.annotations.*SpringDoc使用io.swagger.v3.oas.annotations.*。混用这两个包会静默失效。排查方法很简单看IDE里import的注解来源不对就改。第二Api和ApiOperation的包正确但依赖缺少springfox-swagger-ui相关组件。文档页渲染不出描述并不代表注解没生效用/v2/api-docs接口直接访问JSON看里面有没有swagger节点下的description字段就能判断是注解问题还是UI问题。第三ApiOperation的hidden属性被误设为true。检查一下是不是从别的接口复制代码时把hidden也复制过来了。我遇到最多的情况其实是第一种导错包。开了SpringDoc的项目网上找的Springfox示例代码直接粘进来注解全无效。6.2 参数说明乱码或中文不显示中文乱码在Swagger里通常是编码问题。Spring Boot项目里server.servlet.encoding.forcetrue可以强制UTF-8编码但有时还不够。Maven项目里可以在pom.xml的spring-boot-starter-web依赖之外再确认一下filter配置。如果用的是Springfox 2.9.2这个版本中文字体的展示问题也比较常见升级到2.10.5或换到SpringDoc基本能解决。还有一种“乱码”是注释里的中文字符本身没问题但UI组件渲染不了上Knife4j之后通常会好很多。6.3 接口文档泄露风险Swagger文档本身是开发辅助工具但它会暴露所有接口信息在生产环境开启有严重的信息泄露风险。必要的防护手段是有的首要是生产环境关闭Swagger相关路径或者通过配置项开关控制。# application-prod.yml springfox: documentation: enabled: falseSpringDoc对应配置springdoc: api-docs: enabled: false另一个常见做法是通过Profile(!prod)限定Swagger配置只在非生产环境加载Configuration Profile(!prod) public class SwaggerConfig { // 配置内容省略 }我在实际项目里见过因为忘记关Swagger导致线上接口被扫的案例所以这个必须放在配置阶段就处理不要等出事了再补救。6.4 Swagger未授权访问漏洞的防护与接口文档泄露相关的还有一个专门的“Swagger API 未授权访问漏洞”原理就是Swagger的/v2/api-docs或/v3/api-docs、/swagger-ui.html等路径在生产环境未做鉴权任何人都可以读取接口定义JSON进而分析接口结构进行攻击。修复方式其实不复杂生产环境禁止访问Swagger所有端点用安全框架或网关拦截如果必须在测试环境开放加HTTP Basic认证或集成Spring Security统一鉴权定期扫描线上服务看/swagger-ui.html、/v2/api-docs、/swagger-resources这些路径的返回状态码不要用springfox.documentation.enabledtrue配合某个自定义路径直接暴露公网。补充一点很多人以为Swagger路径改成自定义就安全了比如用springfox.documentation.path/api-docs但这只是隐晦而非防御。安全的关键还是关闭或加上访问控制路径隐藏只能拖延扫描器挡不住真实攻击。7. 超过5000字还可以补充的部分如果内容还没达到5000字以上这里再把日常项目里关于Swagger描述的一些额外经验补充进来。7.1 Get请求的数组参数描述当前端要传多选的ID列表时例如GET /api/user?ids1ids2ids3后端写法是ListLong idsSwagger默认可能显示不出完整参数名。GetMapping(/batch) ApiOperation(value 批量查询用户) public ResultListUserVO getUsers( ApiParam(value 用户ID列表, required true) RequestParam(ids) ListLong ids ) { // 业务逻辑省略 }注意RequestParam的value要写成ids否则Spring可能默认以ids的变量名绑定但Swagger展示时不够明确。配合ApiParam的说明前端可以比较清楚地理解这是可重复的Query参数。7.2 Date、LocalDateTime类型的描述时间类型是前端最容易踩坑的地方。后端返回2023-06-01T12:00:00还是2023-06-01 12:00:00还是时间戳不写清楚前端只能对着响应结果猜。在字段上加ApiModelProperty时一定要把格式说明写进去ApiModelProperty(value 创建时间, example 2023-06-01 12:00:00, notes 格式yyyy-MM-dd HH:mm:ss服务器时区为北京时间) private LocalDateTime createTime; ApiModelProperty(value 生效时间戳, example 1685606400000, notes 毫秒级时间戳UTC时区) private Long effectiveTimestamp;时间格式的统一说明可以放在Api的description或者全局apiInfo的description里两条腿走路字段级写清楚全局兜底。7.3 使用Swagger导出离线文档做团队评审Swagger在线文档适合开发时自查和联调但产品评审、测试用例编写、接口变更会议这些场景离线文档更实用。Knife4j支持导出Markdown和Word文档我每次发版前会把接口文档导出来发给测试同事方便他们写用例。导出后的文档里包含了之前写的ApiOperation、ApiParam、ApiModelProperty里的描述信息一份完整的接口规格说明书就出来了。这比让测试同事自己去UI上一个一个点效率高很多。7.4 描述信息与代码同步维护的小习惯Swagger注解本质上是代码的一部分不是写一次就完了。接口改了参数、改了返回结构注解也要同步改否则文档就会变成误导。我个人的习惯是“接口改动必须触碰注解层”。每次改Controller方法时先看ApiOperation和ApiParam是否还准确参数名变了但ApiParam的value没改前端就会按旧描述传参。如果团队规模大还可以在Code Review的检查清单里加一条“接口相关改动是否同步更新Swagger描述”。不需要强制所有注解都写满但至少变更点涉及的说明要跟上。8. 一个完整示例把描述落到极致这里给出一个综合的Controller示例把上面所有技巧串起来方便你直接参考复制。RestController RequestMapping(/api/order) Api(tags 订单管理, description 订单查询、创建、取消等操作) public class OrderController { PostMapping ApiOperation(value 创建订单, notes 创建订单前请确保用户已登录商品库存充足。创建成功返回订单ID。) ApiResponses({ ApiResponse(code 200, message 创建成功返回订单ID), ApiResponse(code 400, message 参数校验失败), ApiResponse(code 409, message 库存不足) }) public ResultLong createOrder( ApiParam(value 创建订单请求体, required true) Valid RequestBody OrderCreateRequest request ) { // 业务逻辑省略 return Result.success(1001L); } GetMapping(/{orderId}) ApiOperation(value 查询订单详情) public ResultOrderVO getOrder( ApiParam(value 订单ID, required true, example 20230601001) PathVariable Long orderId ) { // 业务逻辑省略 } }对应请求体ApiModel(value 创建订单请求参数) public class OrderCreateRequest { ApiModelProperty(value 用户ID, required true, example 10086) private Long userId; ApiModelProperty(value 商品SKU ID, required true, example SKU123456) private String skuId; ApiModelProperty(value 购买数量, required true, example 2, minimum 1, maximum 99) private Integer quantity; ApiModelProperty(value 收货地址ID, required true, example 8888) private Long addressId; ApiModelProperty(value 订单备注, example 请放在快递柜) private String remark; // getter/setter省略 }这个示例就是把接口说明、参数说明、模型字段说明都落实到位的效果。前端打开文档不需要后端讲解就能完成接口联调这是Swagger描述工作的终极目标。9. 写在最后的一些经验用了这么久Swagger我最深的一个体会是接口描述这事做得好不好不取决于工具而取决于人。Swagger已经把所有“怎么把说明写上去”的机制都给你了剩下的问题是愿不愿意在写接口的时候多敲几个字。我习惯写接口先写注解再写逻辑把ApiOperation的value和notes当成接口设计的确认环节。如果连一句话都概括不了这个接口的职责大概率这个接口本身设计得就不够清晰。反过来当你能清楚描述接口的入参、出参和异常行为时接口的质量通常也不会差。具体的操作层面再分享两个小技巧。第一在apiInfo的description里写上环境和联调信息新同事入职看文档就能自己跑起来不用挨个问。第二在ApiModelProperty里尽量多用example字段Swagger文档的“Try it out”功能会根据示例值自动填充请求参数调试效率能提升一大截。如果你正在做的项目还没有完善的接口描述不妨从今天写的这个接口开始把注解补齐。磨刀不误砍柴工文档上的这几行字省下来的沟通时间远比写它们的时间多。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进