SpringBoot integrates the Swagger2 process, super detailed!

Beep

foreword

This article will introduce the integration process of the API visualization framework Swagger in the SpringBoot framework, discuss the problems encountered in the integration process, and test a series of contents of Swagger2.

Brief introduction of Swagger

Swagger is a specification and complete framework for generating, describing, invoking and visualizing RESTful web services. The overall goal is to have the client and the filesystem update at the same rate as the server. Documenting methods, parameters and models is tightly integrated into the server-side code, allowing the API to always stay in sync. Swagger makes deployment management and using powerful APIs easier than ever.

Preparation

Before starting the integration, you need to create a SpringBoot project. The tools and versions used in this article are as follows:

project content
IDE I understand the idea
Java Ten thousand years unchanged Java8
SpringBoot 2.2.5.RELEASE

When the SpringBoot version is higher than 2.6, the default path matching method is PathPatternMatcher, while Swagger2 is based on AntPathMatcher, and there will be a documentationPluginsBootstrapper'; nested exception is java.lang.NullPointer error. Here you need to pay attention to the compatibility of the two! ! !

Introduce dependencies

This article will discuss using Maven as a management tool. There are two ways to introduce Swagger2, namely the starter method and the native Maven dependency method.

Native Maven dependencies

The dependencies of Swagger2 can be found in the Maven warehouse . The version used is 2.9.2, and the dependencies are posted here:

<!-- 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 introduction

The spring-boot-starter-swagger under SpringForAll uses the automatic configuration feature of Spring Boot to quickly introduce swagger2 into the spring boot application to generate API documents, and simplify the integration code for native use of swagger2.

There is a super detailed introduction and integration tutorial on the GitHub page of spring-boot-starter-swagger, so I won’t go into too much detail here, and if you need to integrate in this way, please move to spring-boot-starter- swagger .

other dependencies

Lombok

In addition to the dependencies of swagger2 and SpringBoot, we introduce Lombok to reduce the writing of some get/set/toString methods, and use the wheels made by the predecessors reasonably:

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.12</version>
</dependency>

Of course, it is not impossible to introduce Lombok when using IDEA to create a project.

commons-lang3

There are a large number of tool classes in commons-lang3, which we also quote here:

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.9</version>
</dependency>

configuration

Here we still focus on the integration of native Maven dependencies. Unlike Starter, which uses configuration files for configuration, the native method needs to be configured using configuration classes;

Of course, in this article, we will also use configuration classes and configuration files together, which can also solve the problem of inflexible configuration.

SwaggerProperties

Here we encapsulate the parameters required by the Swagger2 configuration class into the Properties class. In the src/main/java package, create a config/properties package to store the Properties class:

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

Next, configure the application.yml or application.properties configuration file. This article uses application.yml, which is consistent with the Properties class:

spring:
  application:
    name: BoxCloud
swagger:
  # 是否启用
  enable: true
  base:
    # 扫描的包,多个包使用逗号隔开
    package: com.javafeng
  contact:
    email: [email protected]
    name: JAVAFENG
    url: https://www.javafeng.com
  description:
  title: ${
    
    spring.spring.name} API Document
  url: https://www.javafeng.com
  version: @project.version@

Swagger2Config

After the parameters are configured, configure Swagger2Config. Here, we refer to the configuration method of spring-boot-plus and make some simplifications:

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-enhanced

Because the configuration of the enhancement is simpler and the efficiency of using the enhancement is higher, this part of the content is advanced. The reason for not explaining it together with the previous one is to better distinguish which ones are from Swagger and which ones are from knife4j, which is easier to understand.

In this article, we integrate knife4j as an enhancement of Swagger and add dependencies to pom:

<dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>knife4j-spring-boot-starter</artifactId>
    <version>2.0.2</version>
</dependency>

Add the following configuration in application.yml:

knife4j:
  enable: ${swagger.enable} -- knife4j启用与否取决于Swagger是否启用
  basic:
    enable: true
    username: admin
    password: admin

Modify Swagger2Config, and add relevant notes for enabling knife4j:

......
@Configuration
@EnableSwagger2
@EnableKnife4j
public class Swagger2Config {
  ......
}

For more detailed configuration of knife4j, you can check on the official website as needed.

Knife4j 2.0.6 or above, no need to use @EnableKnife4j annotation, just configure knife4j.enable = true in the configuration file. In order to distinguish here, 2.0.2 is used for demonstration.

test

After the above steps are completed, the preliminary configuration work is basically over, and you can directly access it with a browser: http://localhost:8080/swagger-ui.html, and the following interface appears to indicate that the configuration is successful:

insert image description here

Because the enhancement is configured, it is recommended to use knife4j to view it, visit http://localhost:8080/doc.html, the following interface appears, indicating that the enhancement configuration is successful (before this page, there will be a page that requires user name and password, follow the configuration Just fill in):

insert image description here

use

The screenshots in this part are screenshots of the knife4j page, the display effect and page logic are clearer

Next, we create a Controller and declare some interfaces for testing. Here we will show how to use Swagger2 in an all-round way by simulating functions such as adding, deleting, checking and modifying users, uploading avatars, and logging in. The Swagger2 annotations that appear in the class will be introduced after the class (only common attributes are introduced, if you need in-depth research, it is recommended to consult the documentation on the official website).

There are actually several situations that need to be distinguished here:

  • Whether the input parameter is an entity class
  • Whether the response is an entity class

Simple analysis, among the above-mentioned Api interfaces:

  • Input parameters such as new interfaces, modified interfaces, and login interfaces are entity classes, and other input parameters are non-entity classes
  • The response of the search interface is an entity class, and the rest of the responses are non-entity classes

According to different situations, different Swagger annotations are used for processing.

Entity class

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

Annotations/common attributes illustrate
@ApiModel It is used to modify the entity class (model), which can be regarded as a description of the entity class
 value Alternate name for the model
 description A detailed description of the class

@ApiModelProperty

Annotations/common attributes illustrate
@ApiModelProperty Used to modify entity class fields, which can be regarded as descriptions and restrictions on all aspects of fields
 value Description of the field
 name The new field name that overrides the original field name
 example Default value (the default value is "" when this field is of String type)
 allowableValues Limit the value range of this field, expressed as a limited value list ({1,2,3}), range value ([1,5]),
maximum/minimum value ([1, infinity] infinity or -infinity means infinite value )
 required Mark whether this field is required, the default is false
 hidden Mark whether the field is hidden, default false

When using the entity class as an input parameter, the above annotations can be used to modify the entity class to achieve the purpose of describing the entity class parameters.

controller

The controller here is explained separately according to the situation. The first is the definition of the Controller controller class:

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-- **/
}
Annotations/common attributes illustrate
@Api It is used to decorate the Controller, which can be regarded as an interface group, and the Api in this class will automatically generate documents
 tags can be considered the name of the interface group

Next is the definition of the interface method. At the beginning of this section, we have learned about the centralized parameters and response of the interface method. Here we will explain these different situations.

  1. Entity class as input parameter

The new interface and modified interface are all entity classes as input parameters:

@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

Annotations/common attributes illustrate
@ApiOperation It is used to decorate the Controller, which can be regarded as an interface group, and the Api in this class will automatically generate documents
 value Api interface name
 notes interface description
 tags Define additional interface groups
For example: Even if an interface group is defined in @Api(tags = 'User Management'), another interface group can be specified in this annotation, such as @ApiOperation(tags = 'Account Management'), so that this Interfaces can appear in both interface groups (User Management and Account Management) at the same time

When the entity class is used as an input parameter, we have introduced the corresponding parameter description annotations and usage in the previous part [Entity Class]. Except for @ApiOperation, other annotations (@ApiModel, @ApiModelProperty) will not be described here.

insert image description here

  1. Non-entity class as input parameter

When a non-entity class is used as an input parameter, it can be subdivided into the following situations:

  • common query parameters

Here we define the parameter of finding a certain user as an Api method of common parameters:

@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 and ApiImplicitParam

Annotations/common attributes illustrate
@ApiImplicitParams Modify the Api interface method to declare request parameters
 @ApiImplicitParam Defined in @ApiImplicitParams, each @ApiImplicitParam corresponds to a parameter
  name Parameter name
[usually corresponds to the input parameter name]
  value Parameter Description
  required Mark whether this parameter is required, the default is false
  paramType Mark the position of the parameter, including path, query, body, form, header, etc.
[In general, body and form are recommended to use entity classes as input parameters]

insert image description here

  • path parameters
@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;
}

When the parameter is a path parameter, the paramType value of @ApiImplicitParam should be path, and at the same time, use the @PathVariable annotation to modify the path parameter.

The @PathVariable annotation can identify a template in the URL, such as the {id} of the above interface, and mark that the parameter is obtained based on the template, which does not belong to the Swagger annotation, and will not be introduced here.

insert image description here

  • header parameter
@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;
}

When the parameter is a path parameter, the paramType value of @ApiImplicitParam should be header. At the same time, use the @RequestHeader annotation to modify the parameter, and mark the acquisition position of the parameter as obtained from the Header, so that SpringMVC can correctly obtain it from the Header. parameter.

insert image description here

  • file parameters

Here we directly explain a complex situation, that is, consider including file parameters and common parameters at the same time, and modify them as needed during the actual development process.

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

insert image description here

这里插入一个解释,就是对于同时出现Swagger注解(一般以Api作为前缀)和非Swagger注解同时对参数进行修饰时,并不会彼此影响,Swagger注解只是用来对Api方法进行描述,并不会对该方法造成实质影响,

而例如@RequestParam、@RequestHeader在内的注解是决定SpringMVC是否能正确读取到参数的关键

这里对于两种注解之间的侵入性,掘金大佬1黄鹰在其源码剖析@ApiImplicitParam对@RequestParam的required属性的侵入性做了深入剖析,感兴趣的可以去看看

  1. 实体类作为响应

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

insert image description here

  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。

insert image description here

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: [email protected]
    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的相关内容:

insert image description here

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

insert image description here

参考和引用

  1. dynamicbeamswagger2常用注解API,来源 CSDN
  2. 随风行云Spring Boot整合swagger使用教程,来源 CNBLOG
  3. 1 Yellow Eagle : Source code analysis of @ApiImplicitParam's intrusiveness of @RequestParam's required attribute , source Nuggets
  4. SpringBootPlus

Guess you like

Origin blog.csdn.net/u012751272/article/details/127101042