前言 本文将介绍下API可视化框架Swagger在SpringBoot框架中的整合流程,对在整合过程中遇到的问题进行讨论,并对Swagger2进行测试的一系列内容。
Swagger简述 Swagger 是一个规范和完整的框架,用于生成、描述、调用和可视化 RESTful 风格的 Web 服务。总体目标是使客户端和文件系统作为服务器以同样的速度来更新。文件的方法,参数和模型紧密集成到服务器端的代码,允许API来始终保持同步。Swagger 让部署管理和使用功能强大的API从未如此简单。
前期准备 在开始整合之前,需要先创建SpringBoot项目,本文所用到的工具及版本如下:
项目 内容 IDE IntelliJ IDEA Java 万年不变Java8 SpringBoot 2.2.5.RELEASE
SpringBoot版本高于2.6时默认路径匹配方式为PathPatternMatcher,而Swagger2基于的是AntPathMatcher,会出现documentationPluginsBootstrapper’; nested exception is java.lang.NullPointer错误,这里需要千万注意两者的兼容性!!!
引入依赖 本文将使用Maven作为管理工具进行探讨。引入Swagger2有两种方式,分别为starter方式和原生Maven依赖方式。
原生Maven依赖 Swagger2的依赖可到Maven仓库 中找到,所用到的版本为2.9.2
,这里贴出依赖:
<dependency > <groupId > io.springfox</groupId > <artifactId > springfox-swagger2</artifactId > <version > 2.9.2</version > </dependency > <dependency > <groupId > io.springfox</groupId > <artifactId > springfox-swagger-ui</artifactId > <version > 2.9.2</version > </dependency >
Starter引入 SpringForAll下的spring-boot-starter-swagger 利用Spring Boot的自动化配置特性来实现快速的将swagger2引入spring boot应用来生成API文档,简化原生使用swagger2的整合代码。
在spring-boot-starter-swagger的GitHub页面有着超级详细的介绍以及整合教程,此处不再进行过多的赘述,有需要此方式整合的小伙伴请移步spring-boot-starter-swagger 。
其他依赖 Lombok 除了swagger2的依赖和SpringBoot的依赖之外,我们再引入Lombok,来减少一些 get/set/toString 方法的编写,合理的使用前人做好的轮子:
<dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > <version > 1.18.12</version > </dependency >
当然,在使用IDEA创建项目的时候顺手引入Lombok也不是不行。
commons-lang3 commons-lang3中有大量的工具类,这里我们也引用进来:
<dependency > <groupId > org.apache.commons</groupId > <artifactId > commons-lang3</artifactId > <version > 3.9</version > </dependency >
配置 此处依然是重点介绍原生Maven依赖方式整合,与Starter采取配置文件方式进行配置不同的是,原生方式需要使用配置类进行配置;
当然,在本文中我们也会将配置类与配置文件进行配合使用,同样可解决配置不够灵活的问题。
SwaggerProperties 这里我们将Swagger2配置类所需的参数封装到Properties类中,在src/main/java的包中,创建config/properties包用于存放Properties类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 package com.javafeng.boxcloud.config.properties;import lombok.Data;import org.springframework.beans.factory.annotation.Value;import org.springframework.boot.context.properties.ConfigurationProperties;import org.springframework.boot.context.properties.NestedConfigurationProperty;import org.springframework.stereotype.Component;import java.util.List;@Data @Component @ConfigurationProperties(prefix = "swagger") public class SwaggerProperties { private boolean enable; @Value("${swagger.base.package}") private String basePackage; @Value("${swagger.contact.email}") private String contactEmail; @Value("${swagger.contact.name}") private String contactName; @Value("${swagger.contact.url}") private String contactUrl; private String description; private String title; private String url; private String version; }
application.yml 接下来配置application.yml或者application.properties配置文件,本文使用的是application.yml,此处是与Properties类一致的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 spring: application: name: BoxCloud swagger: enable: true base: package: com.javafeng contact: email: blog@javafeng.com name: JAVAFENG url: https://www.javafeng.com description: title: ${spring.spring.name} API Document url: https://www.javafeng.com version: @project.version@
Swagger2Config 参数配置完后,进行Swagger2Config的配置,此处参考了spring-boot-plus 的配置方式,进行了一些简化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 package com.javafeng.boxcloud.config;import com.google.common.base.Function;import com.google.common.base.Optional;import com.google.common.base.Predicate;import com.javafeng.boxcloud.config.properties.SwaggerProperties;import io.swagger.annotations.Api;import org.apache.commons.lang3.ArrayUtils;import org.apache.commons.lang3.StringUtils;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import springfox.documentation.RequestHandler;import springfox.documentation.annotations.ApiIgnore;import springfox.documentation.builders.ApiInfoBuilder;import springfox.documentation.builders.PathSelectors;import springfox.documentation.builders.RequestHandlerSelectors;import springfox.documentation.service.ApiInfo;import springfox.documentation.service.Contact;import springfox.documentation.spi.DocumentationType;import springfox.documentation.spring.web.plugins.ApiSelectorBuilder;import springfox.documentation.spring.web.plugins.Docket;import springfox.documentation.swagger2.annotations.EnableSwagger2;import javax.servlet.ServletRequest;import javax.servlet.ServletResponse;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import javax.servlet.http.HttpSession;import java.util.Arrays;@Configuration @EnableSwagger2 public class Swagger2Config { @Autowired private SwaggerProperties swaggerProperties; private static final String SPLIT_COMMA = "," ; private static final String SPLIT_SEMICOLON = ";" ; private Class<?>[] ignoredParameterTypes = new Class []{ ServletRequest.class, ServletResponse.class, HttpServletRequest.class, HttpServletResponse.class, HttpSession.class, ApiIgnore.class }; @Bean public Docket createRestApi () { String[] basePackages = getBasePackages(); ApiSelectorBuilder apiSelectorBuilder = new Docket (DocumentationType.SWAGGER_2) .apiInfo(apiInfo()) .select(); if (ArrayUtils.isEmpty(basePackages)) { apiSelectorBuilder.apis(RequestHandlerSelectors.withClassAnnotation(Api.class)); } else { apiSelectorBuilder.apis(basePackage(basePackages)); } Docket docket = apiSelectorBuilder.paths(PathSelectors.any()) .build() .enable(swaggerProperties.isEnable()) .ignoredParameterTypes(ignoredParameterTypes); return docket; } private ApiInfo apiInfo () { return new ApiInfoBuilder () .title(swaggerProperties.getTitle()) .description(swaggerProperties.getDescription()) .termsOfServiceUrl(swaggerProperties.getUrl()) .contact(new Contact (swaggerProperties.getContactName(), swaggerProperties.getContactUrl(), swaggerProperties.getContactEmail())) .version(swaggerProperties.getVersion()) .build(); } public String[] getBasePackages() { String basePackage = swaggerProperties.getBasePackage(); if (StringUtils.isBlank(basePackage)) { throw new RuntimeException ("Swagger basePackage不能为空" ); } String[] basePackages = null ; if (basePackage.contains(SPLIT_COMMA)) { basePackages = basePackage.split(SPLIT_COMMA); } else if (basePackage.contains(SPLIT_SEMICOLON)) { basePackages = basePackage.split(SPLIT_SEMICOLON); } return basePackages; } public static Predicate<RequestHandler> basePackage (final String[] basePackages) { return input -> declaringClass(input).transform(handlerPackage(basePackages)).or(true ); } private static Function<Class<?>, Boolean> handlerPackage(final String[] basePackages) { return input -> { for (String strPackage : basePackages) { boolean isMatch = input.getPackage().getName().startsWith(strPackage); if (isMatch) { return true ; } } return false ; }; } @SuppressWarnings("deprecation") private static Optional<? extends Class <?>> declaringClass(RequestHandler input) { return Optional.fromNullable(input.declaringClass()); } }
knife4j增强 因为增强的配置较为简单,且使用增强效率更高,所以将此部分内容提前。不和前面的一起讲解的原因是为了更好地区分哪些是Swagger的哪些是knife4j的,更容易理解。
本文中我们整合knife4j作为Swagger的增强,在pom中添加依赖:
<dependency > <groupId > com.github.xiaoymin</groupId > <artifactId > knife4j-spring-boot-starter</artifactId > <version > 2.0.2</version > </dependency >
在application.yml中添加如下配置:
knife4j: enable: ${swagger.enable} -- knife4j启用与否取决于Swagger是否启用 basic: enable: true username: admin password: admin
修改Swagger2Config,添加启用knife4j的相关注解:
......@Configuration @EnableSwagger 2@EnableKnife 4jpublic class Swagger2Config { ...... }
对于knife4j更加详细的配置,自行按需到官网查询即可。
knife4j 2.0.6以上版本,无需使用@EnableKnife4j注解,直接在配置文件中配置knife4j.enable = true 即可。此处为了进行区分,使用2.0.2进行演示。
测试 上述步骤完成后,基本上前期配置工作就结束了,可以直接浏览器访问:http://localhost:8080/swagger-ui.html ,出现以下界面表示配置成功:
因为配置了增强,则推荐使用knife4j进行查看,访问http://localhost:8080/doc.html ,出现如下界面则表示增强配置成功(在此页面前会有要求输入用户名密码的页面,按照配置中的填写即可):
使用 此部分截图均为knife4j页面截图,显示效果和页面逻辑更加清晰
接下来我们创建Controller并声明一些接口进行测试,这里我们来通过对用户增删查改、头像上传、登录等功能的模拟,来全方位展示如何使用Swagger2。在类中出现的Swagger2注解,会在类后有相应的介绍(只对常用属性进行了介绍,若需要深层次研究,建议到官网查阅文档)。
这里实际上有几种情况需要加以区分:
简单分析,上述的几个Api接口中:
新增接口、修改接口、登录接口等入参为实体类,其余入参为非实体类 查找接口响应为实体类,其余响应为非实体类 根据不同的情况,使用不同的Swagger注解进行处理。
实体类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import lombok.Data;@Data @ApiModel(value = "用户", description = "查询用户") public class Users { @ApiModelProperty(value = "ID", example = "1") private Integer id; @ApiModelProperty(value = "用户名") private String username; @ApiModelProperty(value = "密码") private String password; @ApiModelProperty(value = "头像") private String avatar; public Users (Integer id, String username, String password, String avatar) { this .id = id; this .username = username; this .password = password; this .avatar = avatar; } public Users () { } }
@ApiModel
注解/常用属性 说明 @ApiModel 用于修饰实体类(模型),可视作对实体类的说明 value 模型的备用名称 description 该类的详细说明
@ApiModelProperty
注解/常用属性 说明 @ApiModelProperty 用于修饰实体类字段,可视作对字段的各方面描述与限制 value 字段的说明 name 覆盖原字段名的新字段名 example 默认值(当此字段为String类型时默认值为 “”) allowableValues 限制该字段值取值范围,表示为限定值列表({1,2,3})、范围值([1,5])、 最大/最小值([1, infinity] infinity或-infinity表示无限值) required 标记该字段是否必需,默认false hidden 标记该字段是否隐藏,默认false
当使用实体类作为入参时,可使用上述注解对实体类进行修饰,以达到描述实体类参数的目的。
控制器 控制器这里,分情况进行分别讲解,首先是Controller控制器类的定义:
package com.javafeng.boxcloud.controller;import io.swagger.annotations.Api;import org.springframework.web.bind.annotation.*;@RestController @RequestMapping("/users") @Api(tags = {"用户管理"}) public class UsersController { }
注解/常用属性 说明 @Api 用于修饰Controller,可将此Controller视作一个接口组,该类中的Api将自动生成文档 tags 可视为该接口组的名称
接下来是接口方法的定义,在本节开始处,我们就了解了接口方法的集中参数情况和响应情况,这里针对这几种不同的情况分别进行讲解。
实体类作为入参 新增接口、修改接口均为实体类作为入参的情况:
@PostMapping("/add") @ApiOperation(value = "新增用户", notes = "用户不得为空") public Boolean add (@RequestBody Users user) { return true ; }@PostMapping("/update") @ApiOperation(value = "修改用户", notes = "用户不得为空") public Boolean update (@RequestBody Users user) { return true ; }
@ApiOperation
注解/常用属性 说明 @ApiOperation 用于修饰Controller,可将此Controller视作一个接口组,该类中的Api将自动生成文档 value Api接口名称 notes 接口描述 tags 定义额外的接口组 例如:即使在@Api(tags = ‘用户管理’)定义了接口组,也可以在此注解内指定另一个接口组,如@ApiOperation(tags = ‘账号管理’),使此接口可以同时出现在这两个接口组(用户管理 和 账号管理)中
当实体类作为入参时,我们在上一部分【实体类】中已经介绍了相应的参数描述注解以及用法,除@ApiOperation外,此处不再对其他注解(@ApiModel、@ApiModelProperty)进行赘述。
非实体类作为入参 当非实体类作为入参时,又可以细分为以下几种情况:
这里我们将查找某个用户的参数定义为普通参数的Api方法:
@GetMapping("/get") @ApiOperation(value = "查询单个用户") @ApiImplicitParams({ @ApiImplicitParam(name = "id", value = "用户ID", required = true, paramType = "query" ) }) public Users get (@RequestParam("id") Integer id) { return new Users (id, "admin" , "123456" , "/resource/image/head.png" ); }
@ApiImplicitParams 和 ApiImplicitParam
注解/常用属性 说明 @ApiImplicitParams 修饰Api接口方法,用来声明请求参数 @ApiImplicitParam 定义在 @ApiImplicitParams,每个@ApiImplicitParam 对应一个参数 name 参数名称[通常与入参名对应] value 参数说明 required 标记该参数是否必需,默认false paramType 标记该参数的位置,包含path、query、body、form、header等几种情况[一般情况下,body、form推荐使用实体类作为入参]
@DeleteMapping("/delete/{id}") @ApiOperation(value = "删除用户", notes = "用户ID不得为空") @ApiImplicitParams({ @ApiImplicitParam(name = "id", value = "用户ID", required = true, paramType = "path" ) }) public Boolean delete (@PathVariable("id") Integer id) { return true ; }
当参数为路径参数时,@ApiImplicitParam的paramType取值应为path,同时,使用@PathVariable注解修饰该路径参数。
@PathVariable注解能够识别URL里面的一个模板,如上述接口的{id},并且标记该参数是基于该模板获取的,并不属于Swagger注解,此处不做过多介绍。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @PostMapping ("/login" )@ApiOperation (value = "登录" )@ApiImplicitParams ({ @ApiImplicitParam (name = "username" , value = "用户名" , required = true, paramType = "header" ), @ApiImplicitParam (name = "password" , value = "密码" , required = true, paramType = "header" ) }) public Boolean login(@RequestHeader ("username" ) String username,@RequestHeader ("password" ) String password) { System .out .println (username + password); return true ; }
当参数为路径参数时,@ApiImplicitParam的paramType取值应为header,同时,使用@RequestHeader注解对参数进行修饰,将参数的获取位置标记为从Header中获取,让SpringMVC能够正确的从Header中获取到参数。
这里我们直接讲解一种复杂情况,即考虑同时包含文件参数和普通参数,在实际开发过程中按需修改即可。
@PostMapping("/upload") @ApiOperation(value = "上传头像", notes = "参数需要头像文件以及对应用户ID") @ApiImplicitParams({ @ApiImplicitParam(name = "id", value = "该头像对应的用户ID", required = true ) }) public Boolean upload (@ApiParam(value = "图片文件", required = true) @RequestParam("avatar") MultipartFile avatar, @RequestParam("id") Integer id) { System.out.println(avatar); return true ; }
对于非文件参数的普通参数,参照第一条【普通查询参数】中的声明方式即可;
对于文件参数,则需要使用@ApiParam对参数进行修饰;
@ApiParam
注解/常用属性 说明 @ApiImplicitParams 修饰Api接口方法,用来声明请求参数 value 参数名称 required 标记该参数是否必需,默认false allowMultiple 是否允许多个文件,默认false
同样的,需要在参数上使用@RequestParam进行修饰。
这里插入一个解释,就是对于同时出现Swagger注解(一般以Api作为前缀)和非Swagger注解同时对参数进行修饰时,并不会彼此影响,Swagger注解只是用来对Api方法进行描述,并不会对该方法造成实质影响,
而例如@RequestParam、@RequestHeader在内的注解是决定SpringMVC是否能正确读取到参数的关键
这里对于两种注解之间的侵入性,掘金大佬1黄鹰 在其源码剖析@ApiImplicitParam对@RequestParam的required属性的侵入性 做了深入剖析,感兴趣的可以去看看
实体类作为响应 当实体类作为响应时,通常在实体类上的注解所生成的描述也会作为响应的描述出现,在此处不再进行赘述。
非实体类作为响应 我们对新增接口进行修改,在其方法上添加@ApiResponses来对响应进行描述:
@PostMapping ("/add" )@ApiOperation (value = "新增用户" , notes = "用户不得为空" )@ApiResponses ({ @ApiResponse (code = 200 , message = "添加成功" ), @ApiResponse (code = 500 , message = "服务器错误" ), @ApiResponse (code = 400 , message = "参数异常" ) }) public Boolean add(@RequestBody Users user) { return true ; }
需要注意的是,Swagger无法对非实体类响应进行详细描述,只能通过@ApiResponses和@ApiResponse描述响应码信息。同时,在以实体类作为响应时,同样可也以使用@ApiResponses和@ApiResponse。
Token处理 在做前后端分离的应用时,后端接口通常会要求在Header中添加Token以保证安全性,这里我们依然是参考SpringBootPlus的处理方式,对Token进行处理。
需要注意的是,此处的Token处理是基于Swagger进行接口测试时的,并不是对接口如何增加Token进行讲解,只是对有Token的接口如何通过Swagger进行测试做出讲解。
修改SwaggerProperties 思路是,在Swagger配置中添加默认的全局参数描述,对Token进行处理,这里我们默认Token信息附加在Header中。
首先在SwaggerProperties中新增 以下内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @NestedConfigurationProperty private List<ParameterConfig> parameterConfig;@Data public static class ParameterConfig { private String name; private String description; private String type = "head" ; private String dataType = "String" ; private boolean required; private String defaultValue; }
修改application.yml 随后在application.yml中对新增部分编写对应的配置项,这里贴出整体内容,自定义参数配置部分为新增内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 spring: application: name: BoxCloud swagger: enable: true base: package: com.javafeng contact: email: blog@javafeng.com name: JAVAFENG url: https://www.javafeng.com description: title: ${spring.application.name} API Document url: https://www.javafeng.com version: @project.version@ parameter-config: - name: token description: Token Request Header type: header data-type: String required: false default-value: knife4j: enable: ${swagger.enable} basic: enable: true username: admin password: admin
这里可以根据不同的需求,配置多个自定义参数,这里只演示了Token一个参数,如果是多个参数的话,配置多个即可,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 parameter-config: - name: param1 description: This is param1 type: header data-type: String required: false default-value: parameter-config: - name: param2 description: This is param2 type: header data-type: String required: false default-value: parameter-config: - name: param3 description: This is param3 type: header data-type: String required: false default-value:
修改Swagger2Config 接下来,我们在Swagger2Config中对Token参数进行处理,首先在Swagger2Config中添加如下方法,从application.yml中获取到配置的额外Token参数并进行封装:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 private List<Parameter> getParameters () { List<SwaggerProperties.ParameterConfig> parameterConfig = swaggerProperties.getParameterConfig(); if (CollectionUtils.isEmpty(parameterConfig)) { return null ; } List<Parameter> parameters = new ArrayList <>(); parameterConfig.forEach(parameter -> { parameters.add(new ParameterBuilder () .name(parameter.getName()) .description(parameter.getDescription()) .modelRef(new ModelRef (parameter.getDataType())) .parameterType(parameter.getType()) .required(parameter.isRequired()) .defaultValue(parameter.getDefaultValue()) .build()); }); return parameters; }
随后修改createRestApi方法,在声明Docket的位置添加对额外参数的处理,添加后如下:
... ...Docket docket = apiSelectorBuilder.paths(PathSelectors.any()) .build() .enable(swaggerProperties.isEnable()) .ignoredParameterTypes(ignoredParameterTypes) .globalOperationParameters(getParameters()); ... ...
重启项目后,我们随意打开一个接口的文档,这里我们打开的是knife4j的页面,选择调试,在请求头部位置就可以看到Token的相关内容:
当然,在Swagger的原生界面也可以看到:
参考和引用 dynamicbeam :swagger2常用注解API ,来源 CSDN随风行云 :Spring Boot整合swagger使用教程 ,来源 CNBLOG1黄鹰 :源码剖析@ApiImplicitParam对@RequestParam的required属性的侵入性 , 来源 掘金SpringBootPlus