[版权申明] 非商业目的注明出处可自由转载
出自:shusheng007
概述
近来颇为懈怠,竟两月有余未发一篇博客,惭愧惭愧。值此端午佳节(dragon boat festival)作为调包高手、API小王子的我就聊聊API文档的那些事吧,如果你也是API小王子,那我们就可以在一起欢度佳节了,哈哈...
我惯用Java,惯用Spring,所以这里谈论的是关于springboot中如何处理文档的内容
概念解释
谈到API文档,那就绕不开大名鼎鼎的Swagger
,但是你是否还听说过:OpenAPI
,Springfox
,Springdoc
?你第一次看到这些脑瓜子是不是嗡嗡的?
是一个组织(OpenAPI Initiative),他们指定了一个如何描述HTTP API的规范(OpenAPI Specification)。既然是规范,那么谁想实现都可以,只要符合规范即可。
它是SmartBear这个公司的一个开源项目,里面提供了一系列工具,包括著名的 swagger-ui
。swagger
是早于OpenApi的,某一天swagger
将自己的API设计贡献给了OpenApi,然后由其标准化了。
是Spring生态的一个开源库,是Swagger与OpenApi规范的具体实现。我们使用它就可以在spring中生成API文档。以前基本上是行业标准,目前最新版本可以支持 Swagger2, Swagger3 以及 OpenAPI3 三种格式。但是其从 2020年7月14号就不再更新了,不支持springboot3,所以业界都在不断的转向我们今天要谈论的另一个库Springdoc,新项目就不要用了。
算是后起之秀,带着继任Springfox的使命而来。其支持OpenApi规范,支持Springboot3,我们的新项目都应该使用这个。
SpringDoc使用
我们可以在springboot中使用SpringDoc来生成API文档,详情可以参考官网,下面我们来简单的实践一下。
简单集成
在springboot中使用springdoc起步非常容易,只需要引入其starter即可
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>1.7.0</version>
</dependency>
运行后访问下面的链接即可
http://server:port/context-path/swagger-ui.html
例如
http://localhost:8080/swagger-ui.html
例如我们有如下代码:
@RestController
@RequestMapping("/api/programmer")
public class ProgrammerController {
@PostMapping()
public Programmer createProgrammer(@RequestBody CreateProgrammerRequest request) {
return new Programmer(1, request.getAge(), request.getProgrammingLang());
}
@GetMapping("/{id}")
public Programmer getProgrammer(@PathVariable Integer id) {
return new Programmer(1, 35, List.of("Java,Python,SQL"));
}
}
添加依赖运行后访问http://localhost:8080/swagger-ui.html
后结果如下
是不是特牛逼?当然springdoc的集成不可能就这点东西,不然也没有这篇文章了,咱接着往下看。苦于网上的一些教程不直观,对初学者不友好,所以本文以代码和其效果的形式来展示,保你一看就会。
配置文档信息
得益于springboot的强大,我们只需添加一个依赖就可以使用API文档了,但是使用的都是默认值,我们当然也希望对其进行各种自定义的配置
- 配置文档信息
创建一个OpenAPI 的bean,配置文档名称等信息
@Configuration
public class SpringDocConfig {
@Bean
public OpenAPI myOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("程序员API")
.description("程序员的大本营")
.version("v1.0.0")
.license(new License()
.name("许可协议")
.url("https://shusheng007.top"))
.contact(new Contact()
.name("书生007")
.email("wangben850115@gmail.com")))
.externalDocs(new ExternalDocumentation()
.description("ShuSheng007博客")
.url("https://shusheng007.top"));
}
}
效果:
里面的配置项很多,可以根据代码和截图对照一下,按照自己的需求配置即可
配置文档分组
用来配置分组的,假如你有两类controller一类以/api
为前缀,一类以/admin
为前缀,就可以将其配置为两个分组。很多时候我们只有一个分组,所以就不需要下面的配置。
@Configuration
public class SpringDocConfig {
...
@Bean
public GroupedOpenApi publicApi() {
return GroupedOpenApi.builder()
.group("api")
.pathsToMatch("/api/**")
.build();
}
@Bean
public GroupedOpenApi adminApi() {
return GroupedOpenApi.builder()
.group("admin")
.pathsToMatch("/admin/**")
.build();
}
}
效果:
可以通过右上角的下拉框选择要展示的group。
使用注解
这个是咱们的重头戏,OpenApi
规范提供了很多注解,下面是一些常用的
注解 | 含义 |
---|---|
@Tag | 用在controller类上,描述此controller的信息 |
@Operation | 用在controller的方法里,描述此api的信息 |
@Parameter | 用在controller方法里的参数上,描述参数信息 |
@Parameters | 用在controller方法里的参数上 |
@Schema | 用于Entity,以及Entity的属性上 |
@ApiResponse | 用在controller方法的返回值上 |
@ApiResponses | 用在controller方法的返回值上 |
@Hidden | 用在各种地方,用于隐藏其api |
下面我们一起来看看效果
@Tag
@Tag(name = "程序员", description = "程序员乐园")
@RestController
@RequestMapping("/api/programmer")
public class ProgrammerController {
...
}
效果
@Operation
@Operation(summary = "创建程序员", description = "用于创建一个闷骚的程序员")
@PostMapping()
public Programmer createProgrammer(@RequestBody CreateProgrammerRequest request) {
return new Programmer(666, "王二狗", request.getAge(), request.getProgrammingLang());
}
效果
@Operation
其实很复杂的,我们可以将下面要将的@Parameter
以及@ApiResponse
都可以配置在它里面。
@Schema
@Schema
用于实体类和其属性,例如有如下代码:
@Schema(description = "创建程序员入参")
public class CreateProgrammerRequest {
@Schema(description = "名称", example = "王二狗")
private String name;
@Schema(description = "年龄", example = "35")
private Integer age;
@Schema(description = "掌握的编程语言", type = "List", example = "[\"Java\",\"Sql\"]")
private List<String> programmingLang;
}
效果:
还可以切换到 Schema选项进行查看
同时,Springdoc还支持 Java Bean Validation API 的注解,例如@NotNull
等
@Schema(description = "创建程序员入参")
public class CreateProgrammerRequest {
@NotNull
@Schema(description = "名称", example = "王二狗")
private String name;
@NotNull
@Min(18)
@Max(35)
@Schema(description = "年龄", example = "35")
private Integer age;
...
}
效果:
注意红框中的内容,name和age右上角都有出现了一个红色的星星,表示是必填的。age也被限制了范围。
@Parameter
用于添加接口参数信息
@GetMapping("/{id}")
public Programmer getProgrammer(@Parameter(description = "程序员id") @PathVariable Integer id) {
...
}
效果
@Parameters
与@Parameter
作用一样,但是可以批量添加,不用一个一个的写在参数前面
@Parameters(value = {
@Parameter(name = "name", description = "姓名", in = ParameterIn.PATH),
@Parameter(name = "age", description = "年龄", in = ParameterIn.QUERY)
})
@GetMapping("/{name}")
public List<Programmer> getProgrammers(@PathVariable("name") String name, @RequestParam("age") Integer age) {
...
}
parameters里的parameter使用name来找到方法中的入参,这块要对应上。
效果:
@ApiResponses
和@ApiResponse
顾名思义,此注解用来描述返回值的。
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "成功",
content = {@Content(mediaType = "application/json",
schema = @Schema(implementation = Programmer.class))}),
@ApiResponse(responseCode = "405", description = "非法输入",
content = @Content)
})
@PostMapping()
public Programmer createProgrammer(@RequestBody CreateProgrammerRequest request) {
...
}
效果
可见,我们成功配置了两种情况的返回值。但是我们一般不会手动给每个API 写上一堆 @ApiResponse
,那的多烦啊。业界通常会有一个统一的返回类型,例如
public class Result<T> implements Serializable {
private int code;
private String message;
private T data;
private String traceId;
}
还会有一个统一的异常处理类,使用@RestControllerAdvice
标记,然后每个方法会捕捉对应的异常,只要我们使用@ResponseStatus
来标记这些方法,springdoc就会自动生成相应的文档
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(value = Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Result handleException(HttpServletRequest httpServletRequest, Exception e) {
return new Result(StatusCode.FAILED.getCode(), StatusCode.FAILED.getMessage(), null);
}
@ExceptionHandler(value = ApiException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result handleBusinessException(HttpServletRequest httpServletRequest, ApiException e) {
return new Result(e.getCode(), e.getMessage(), null);
}
}
例如我们有如下代码:
...
@RequestMapping(value = "/api/programmer",produces = "application/json")
public class ProgrammerController {
@GetMapping("/{id}")
public Result<Programmer> getProgrammer(@Parameter(description = "程序员id") @PathVariable Integer id) {
...
}
}
生成的api文档如下:
可见,多了400和500。
认证授权
swagger-ui
不仅支持查看API文档也可以直接调用API,这点很牛逼。但有时我们的服务需要认证,例如需要检查header里是否携带正确的token, 否则就调用不通,那怎么办?稍安勿躁,OpenApi规范也考虑到了这个问题。
OpenAPI 3.0 支持下面的认证模式:
- HTTP authentication schemes (使用Authorization header):
- Basic
- Bearer
- Other HTTP schemes as defined by RFC 7235 and HTTP Authentication Scheme Registry
- API keys in headers, query string or cookies
- Cookie authentication
- OAuth 2
- OpenID Connect Discovery
有的我也没用过,常用的就是Http Auth,以及OAuth2。本文以HTTP Bearer来作为我们服务的认证授权模式,如下图所示。
无需认证
当你的服务没有认证机制的话是可以直接调用的:
每个API 右上角都有一个try it out
按钮,点击输入参数点击execute按钮即可,如下所示。
需要认证
如果你的服务需要认证后才能调用,那么默认情况下就不行了。例如你使用了Spring Security,或者你自己写了个Filter 来实现认证功能。
下面是demo服务用来做认证的Filter,采用HTTP Bearer 模式。所以需要在请求的Authentication Header 里携带token 123才能通过认证。
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
...
@Override
protected void doFilterInternal(HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain filterChain) throws ServletException, IOException {
...
// get token from header [Authorization: Bearer <token>]
String authHeader = request.getHeader(AUTH_HEADER);
...
String authToken = authHeader.split(" ")[1];
if (!"123".equals(authToken)) {
genUnauthResponse(response);
return;
}
filterChain.doFilter(request, response);
}
所以当从swagger-ui
调用API时返回了401,如下图所示。那怎么才能正常调用API呢?接下来让我们看一下
Springdoc 使用@SecurityScheme
来定义一个安全模式,我们可以定义全局的,也可以针对某个controller定义类级别的,我们这里定义一个全局的。
- 在Application类上添加
@SecurityScheme
@SecurityScheme(name = "api_token", type = SecuritySchemeType.HTTP, scheme ="bearer", in = SecuritySchemeIn.HEADER)
@SpringBootApplication
public class SpringdocIntegrateApplication {
public static void main(String[] args) {
SpringApplication.run(SpringdocIntegrateApplication.class, args);
}
}
我们定义了一个名为api_token
的安全模式,并指定其使用HTTP Bearer的方式。
- 使此安全模式生效
@Configuration
public class SpringDocConfig {
@Bean
public OpenAPI myOpenAPI() {
return new OpenAPI()
...
.security(List.of(new SecurityRequirement().addList("api_token")));
}
}
注意api_token
正是我们第一步定义的那个Security schema。
经过上面两步后查看生成的API文档,你会发现其右上角出现了一个Authorize
的按钮, 而且每个API的右边也出现了一把小锁,如下图所示。
当点击Authorize
按钮后会弹出一个让你提供认证信息的弹出,其根据不同的认证方式会有所区别。
我们这里需要输入一个token,然后点击Authorize按钮后关闭此弹窗。你会发现每个API右边的那把小锁从打开状态变为了关闭状态,颜色也从灰色变成了黑色,证明其生效了。
然后我们再来请求一下API,就会正常返回了。
- 声明是否需要认证
默认情况下按照上面两步设置后,整个应用程序的API生效,但是有的API是不需要认证的,例如登录。
对于这种情况,我们可以使用@SecurityRequirements()
来设置。
@RestController
@RequestMapping(value = "/admin",produces = "application/json")
public class AuthController {
...
@SecurityRequirements()
@PostMapping("/login")
public Result<String> login(@RequestBody LoginRequest request){
return Result.ok("123");
}
}
@SecurityRequirements()
里面需要一个String数组,里面列出需要使用的@SecurityScheme
,例如我们这里的api_token
。如果不写就说明不需要任何的安全模式,这里就是这种情况。
从上图可以看出,/admin/login
这个API右边没有小锁的标志,表示其不需要认证。针对本案例来说,不需要认证的意思就是:在发起这个请求的时候,不在Authentication header里面附加token。
扩展
我们可以给API同时设置多个安全验证,假设我们有一个API,既要验证请求头里是否有token,还要验证是否有api-key。怎么办呢?
首先把这些security scheme给定义出来,如下所示:
@SecurityScheme(name = Constant.API_TOKEN_SECURITY_SCHEMA, type = SecuritySchemeType.HTTP, scheme ="bearer", in = SecuritySchemeIn.HEADER)
@SecurityScheme(name = Constant.API_KEY_SECURITY_SCHEMA, type = SecuritySchemeType.APIKEY, paramName = "api-key", in = SecuritySchemeIn.HEADER)
@SpringBootApplication
public class SpringdocIntegrateApplication {
public static void main(String[] args) {
SpringApplication.run(SpringdocIntegrateApplication.class, args);
}
}
然后配置给OpenApi中,使其全局生效
@Configuration
public class SpringDocConfig {
@Bean
public OpenAPI myOpenAPI() {
return new OpenAPI()
...
.security(List.of(new SecurityRequirement().addList(Constant.API_TOKEN_SECURITY_SCHEMA),
new SecurityRequirement().addList(Constant.API_KEY_SECURITY_SCHEMA)));
}
最后就可以使用 @SecurityRequirements
来对具体的api进行调节,例如我们的登录方法只需要api-key的验证,不需要token的验证
@Operation(description = "登录")
@SecurityRequirements(value = {
@SecurityRequirement(name = Constant.API_KEY_SECURITY_SCHEMA)
})
@PostMapping("/login")
public Result<String> login(@RequestBody LoginRequest request) {
return Result.ok("123");
}
点击Authorize 按钮后,会显示出连个凭证,如下图所示:
这里只是展示了HTTP bearer 这种安全模式,其他的原理类似,小朋友们可以开动你们聪明的大脑研究一下,给补充到这里。
总结
终于写完了,要写一篇逻辑清晰,对初学者友好的博客可真不容易。当然我写博客主要是为了自己梳理知识,但是我一直秉承着解决自己初次接触时候的困难场景来写的,相信对其他小朋友会有很大的帮助。
本文只是对Springdoc常用功能做了介绍,更个性化的功能还是要去看 Springdoc官网
最后,这么好的文章不给点个赞你还是那个无私奉献,互帮互助,开源博爱的IT人吗?
此情此景我想吟诗一首:
端阳采撷
玉粽袭香千舸竞,艾叶黄酒可驱邪。
骑父稚子香囊佩,粉俏媳妇把景撷。
祝小朋友们端午节快乐!
源码
一如既往,你可以从Github上找到本文源码:springdoc-integrate
文章评论
非常好的教程,跟着执行了所有的步骤!学习过程中也参考了这个教程:https://www.baeldung.com/spring-rest-openapi-documentation
几点心得和踩到的坑:
1. 默认api-docs是localhost:8080/v3/api-docs,swagger是localhost:8080/swagger-ui/index.html。
2. 我用了过滤器,过滤器需要放行/v3/api-docs、/swagger-ui/*。
3. 我的前后端交互验证使用http请求头中添加的自定义“Accesstoken”,@SecurityScheme需要写成 @SecurityScheme(name = "Accesstoken", type = SecuritySchemeType.APIKEY, in = SecuritySchemeIn.HEADER) ,SpringDocConfig的配置需要写成 .security(List.of(new SecurityRequirement().addList("Accesstoken")))。
4. 在application.yml配置文件中,我做了以下配置:
springdoc:
api-docs:
path: /api-docs
swagger-ui:
url: /api-docs
这样可以把api-docs的默认路径改为localhost:8080/api-docs,swagger的界面打开默认即是本项目的api文档。