likes
comments
collection

SpringBoot整合Swagger2流程,超详细!

作者站长头像
站长
· 阅读数 12

小声叭叭

前言

本文将介绍下API可视化框架Swagger在SpringBoot框架中的整合流程,对在整合过程中遇到的问题进行讨论,并对Swagger2进行测试的一系列内容。

Swagger简述

Swagger 是一个规范和完整的框架,用于生成、描述、调用和可视化 RESTful 风格的 Web 服务。总体目标是使客户端和文件系统作为服务器以同样的速度来更新。文件的方法,参数和模型紧密集成到服务器端的代码,允许API来始终保持同步。Swagger 让部署管理和使用功能强大的API从未如此简单。

前期准备

在开始整合之前,需要先创建SpringBoot项目,本文所用到的工具及版本如下:

项目内容
IDEIntelliJ IDEA
Java万年不变Java8
SpringBoot2.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,这里贴出依赖:

<!-- swagger start -->
<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>
<!-- swagger end -->

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类:

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 {
    // 是否启用Swagger
    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类一致的:

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的配置方式,进行了一些简化:

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 = ";";

    // Swagger忽略的参数类型
    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();
        // 如果扫描的包为空,则默认扫描类上有@Api注解的类
        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;
    }

    /**
     * 获取apiInfo
     * @return
     */
    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();
    }

    /**
     * 获取扫描的包
     *
     * @return
     */
    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
@EnableSwagger2
@EnableKnife4j
public class Swagger2Config {
  ......
}

对于knife4j更加详细的配置,自行按需到官网查询即可。

knife4j 2.0.6以上版本,无需使用@EnableKnife4j注解,直接在配置文件中配置knife4j.enable = true 即可。此处为了进行区分,使用2.0.2进行演示。

测试

上述步骤完成后,基本上前期配置工作就结束了,可以直接浏览器访问:http://localhost:8080/swagger-ui.html ,出现以下界面表示配置成功:

SpringBoot整合Swagger2流程,超详细!

因为配置了增强,则推荐使用knife4j进行查看,访问http://localhost:8080/doc.html ,出现如下界面则表示增强配置成功(在此页面前会有要求输入用户名密码的页面,按照配置中的填写即可):

SpringBoot整合Swagger2流程,超详细!

使用

此部分截图均为knife4j页面截图,显示效果和页面逻辑更加清晰

接下来我们创建Controller并声明一些接口进行测试,这里我们来通过对用户增删查改、头像上传、登录等功能的模拟,来全方位展示如何使用Swagger2。在类中出现的Swagger2注解,会在类后有相应的介绍(只对常用属性进行了介绍,若需要深层次研究,建议到官网查阅文档)。

这里实际上有几种情况需要加以区分:

  • 入参是否为实体类
  • 响应是否为实体类

简单分析,上述的几个Api接口中:

  • 新增接口、修改接口、登录接口等入参为实体类,其余入参为非实体类
  • 查找接口响应为实体类,其余响应为非实体类

根据不同的情况,使用不同的Swagger注解进行处理。

实体类

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接口方法 **/
   /** --start-- **/
   /** --end-- **/
}
注解/常用属性说明
@Api用于修饰Controller,可将此Controller视作一个接口组,该类中的Api将自动生成文档
 tags可视为该接口组的名称

接下来是接口方法的定义,在本节开始处,我们就了解了接口方法的集中参数情况和响应情况,这里针对这几种不同的情况分别进行讲解。

  1. 实体类作为入参

新增接口、修改接口均为实体类作为入参的情况:

@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将自动生成文档
 valueApi接口名称
 notes接口描述
 tags定义额外的接口组例如:即使在@Api(tags = '用户管理')定义了接口组,也可以在此注解内指定另一个接口组,如@ApiOperation(tags = '账号管理'),使此接口可以同时出现在这两个接口组(用户管理 和 账号管理)中

当实体类作为入参时,我们在上一部分【实体类】中已经介绍了相应的参数描述注解以及用法,除@ApiOperation外,此处不再对其他注解(@ApiModel、@ApiModelProperty)进行赘述。

SpringBoot整合Swagger2流程,超详细!

  1. 非实体类作为入参

当非实体类作为入参时,又可以细分为以下几种情况:

  • 普通查询参数

这里我们将查找某个用户的参数定义为普通参数的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推荐使用实体类作为入参]

SpringBoot整合Swagger2流程,超详细!

  • 路径参数
@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注解,此处不做过多介绍。

SpringBoot整合Swagger2流程,超详细!

  • header参数
@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中获取到参数。

SpringBoot整合Swagger2流程,超详细!

  • 文件参数

这里我们直接讲解一种复杂情况,即考虑同时包含文件参数和普通参数,在实际开发过程中按需修改即可。

@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进行修饰。

SpringBoot整合Swagger2流程,超详细!

  1. 实体类作为响应

当实体类作为响应时,通常在实体类上的注解所生成的描述也会作为响应的描述出现,在此处不再进行赘述。

SpringBoot整合Swagger2流程,超详细!

  1. 非实体类作为响应

我们对新增接口进行修改,在其方法上添加@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。

SpringBoot整合Swagger2流程,超详细!

Token处理

在做前后端分离的应用时,后端接口通常会要求在Header中添加Token以保证安全性,这里我们依然是参考SpringBootPlus的处理方式,对Token进行处理。

需要注意的是,此处的Token处理是基于Swagger进行接口测试时的,并不是对接口如何增加Token进行讲解,只是对有Token的接口如何通过Swagger进行测试做出讲解。

修改SwaggerProperties

思路是,在Swagger配置中添加默认的全局参数描述,对Token进行处理,这里我们默认Token信息附加在Header中。

首先在SwaggerProperties中新增以下内容:

@NestedConfigurationProperty
private List<ParameterConfig> parameterConfig;

// 自定义参数配置
@Data
public static class ParameterConfig {
    // 名称
    private String name;
    // 描述
    private String description;
    // 参数类型
    // header, cookie, body, query
    private String type = "head";
    // 数据类型
    private String dataType = "String";
    // 是否必填
    private boolean required;
    // 默认值
    private String defaultValue;
}

修改application.yml

随后在application.yml中对新增部分编写对应的配置项,这里贴出整体内容,自定义参数配置部分为新增内容:

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@
  # 自定义参数配置,可配置N个
  parameter-config:
    - name: token
      description: Token Request Header
      # header, cookie, body, query
      type: header
      data-type: String
      required: false
      # 测试接口时,自动填充token的值
      default-value:
      
knife4j:
  enable: ${swagger.enable}
  basic:
    enable: true
    username: admin
    password: admin

这里可以根据不同的需求,配置多个自定义参数,这里只演示了Token一个参数,如果是多个参数的话,配置多个即可,如下:

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参数并进行封装:

/**
 * 添加额外参数
 *
 * @return
 */
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的相关内容:

SpringBoot整合Swagger2流程,超详细!

当然,在Swagger的原生界面也可以看到:

SpringBoot整合Swagger2流程,超详细!

参考和引用