SpringMVC整合Swagger简单使用及原理分析

前言

Swagger可以让我们根据API生成在线文档,且可以在线测试,极大的简化了手工编写文档的工作。

简单使用

添加maven依赖

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

代码示例

import java.util.Collections;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@Configuration
@EnableSwagger2
public class SwaggerConfig {

  @Value("${spring.swagger.enable:true}")
  private String swaggerEnable;

  /**
   * swagger配置
   */
  @Bean
  public Docket api() {
    return new Docket(DocumentationType.SWAGGER_2)
        .select()
        .apis(RequestHandlerSelectors.basePackage("com.imooc.cnblogs.web")) //扫描包路径
        .paths(PathSelectors.any())
        .build()
        .apiInfo(apiInfo())
        .enable("true".equals(swaggerEnable)); // 是否启用,我们可以使用这个属性关闭生产环境的swagger文档
  }

  // 文档的一些描述信息
  private ApiInfo apiInfo() {
    return new ApiInfo(
        "接口文档", "", "1.0", "",
        new Contact("strongmore", "", "xxx@163.com"),
        "strongmore", "", Collections.emptyList());
  }
}

配置开启swagger注解的扫描

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;

@AllArgsConstructor
@NoArgsConstructor
@Setter
@Getter
@ToString
@ApiModel("订单信息模型") //注意,多个@ApiModel的value不能重复
public class OrderInfo {
  @ApiModelProperty("订单号")
  private String orderId;
  @ApiModelProperty("订单创建时间")
  private Long createDate;
}

在模型类及属性上添加描述注解供swagger扫描处理,主要有@ApiModel和@ApiModelProperty

import com.imooc.cnblogs.model.OrderInfo;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping("/order")
@Api(tags = "订单接口")
public class OrderController {


  @GetMapping("/order/detail")
  @ResponseBody
  @ApiOperation("订单详情查询")
  public OrderInfo queryOrderDetail(@RequestParam @ApiParam("订单号") String orderId) {
    OrderInfo orderInfo = new OrderInfo();
    orderInfo.setOrderId(orderId);
    orderInfo.setCreateDate(System.currentTimeMillis());
    return orderInfo;
  }
}

在Controller类及方法上添加描述注解供swagger扫描处理,主要有@Api,@ApiOperation,@ApiParam

页面效果

固定的请求路径为/swagger-ui.html

原理分析

@EnableSwagger2

通过此注解开启swagger注解的扫描,它会导入Swagger2DocumentationConfiguration配置类。

@Configuration
@Import({ SpringfoxWebMvcConfiguration.class, SwaggerCommonConfiguration.class })
@ComponentScan(basePackages = {
    "springfox.documentation.swagger2.mappers"
})
@ConditionalOnWebApplication
public class Swagger2DocumentationConfiguration {

  // 用来自定义Jackson这个JSON解析器
  @Bean
  public JacksonModuleRegistrar swagger2Module() {
    return new Swagger2JacksonModule();
  }

  // 处理器映射器,用来处理Swagger2Controller(我们项目中定义的所有接口及Model信息都是此类响应到swagger-ui.html页面的)
  @Bean
  public HandlerMapping swagger2ControllerMapping(
      Environment environment,
      DocumentationCache documentationCache,
      ServiceModelToSwagger2Mapper mapper,
      JsonSerializer jsonSerializer) {
    return new PropertySourcedRequestMappingHandlerMapping(
        environment,
        // 注意,Swagger2Controller没有被扫描到,所以不是一个Bean对象,通过new的方式来创建实例
        new Swagger2Controller(environment, documentationCache, mapper, jsonSerializer));
  }
}

此配置类又导入了SpringfoxWebMvcConfiguration配置类(重要)和SwaggerCommonConfiguration配置类(不重要)。
SpringfoxWebMvcConfiguration配置类的作用:

  1. 指定扫描包路径,扫描很多类型为Plugin的Bean。
  2. 通过Spring-Plugin组件向容器注册很多PluginRegistry对象。关于Spring-Plugin组件原理,可以查看Spring Plugin插件系统入门

其中很重要的两个Bean为DocumentationPluginsBootstrapper和DocumentationPluginsManager,
两者配合通过管理Plugin对象将我们项目中标注了swagger注解的接口和Model收集整理并存储起来,
在这个过程中就使用到了上面所说的PluginRegistry对象。

DocumentationPluginsBootstrapper

此类可以看做一个插件引导器,它实现了SmartLifecycle接口,所以会在ApplicationContext的refresh()流程最后被执行,这也是Spring提供的一个扩展点。

@Override
public void start() {
    if (initialized.compareAndSet(false, true)) {
      // 这里的DocumentationPlugin实现类其实就是我们在SwaggerConfig配置类中定义的Docket对象
      List<DocumentationPlugin> plugins = pluginOrdering()
          .sortedCopy(documentationPluginsManager.documentationPlugins());
      for (DocumentationPlugin each : plugins) {
        DocumentationType documentationType = each.getDocumentationType();
        // 是否启用
        if (each.isEnabled()) {
          // 开启文档扫描
          scanDocumentation(buildContext(each));
        } else {
        }
      }
    }
  }

继续跟进去

private DocumentationContext buildContext(DocumentationPlugin each) {
    // 创建文档上下文
    return each.configure(defaultContextBuilder(each));
  }

其实通过defaultContextBuilder()方法这一步已经获取到了所有的处理器方法(包含@RequestMapping注解的方法),具体来说是通过WebMvcRequestHandlerProvider类,
它内部会依赖所有的RequestMappingInfoHandlerMapping对象,SpringMVC定义的RequestMappingHandlerMapping处理器映射器就是此类型,其中包含所有的处理器方法。
关于RequestMappingHandlerMapping的原理,可以查看SpringMVC源码分析之一个请求的处理
继续分析scanDocumentation()方法

private void scanDocumentation(DocumentationContext context) {
    try {
      // resourceListing在这里是ApiDocumentationScanner类型,scanned为DocumentationCache(保存所有文档信息)
      scanned.addDocumentation(resourceListing.scan(context));
    } catch (Exception e) {
      log.error(String.format("Unable to scan documentation context %s", context.getGroupName()), e);
    }
  }

ApiDocumentationScanner,从名称就可以看出来,是一个文档扫描器

public Documentation scan(DocumentationContext context) {
    // 根据所有的处理器方法获取所有的Controller
    ApiListingReferenceScanResult result = apiListingReferenceScanner.scan(context);
    ApiListingScanningContext listingContext = new ApiListingScanningContext(context,
        result.getResourceGroupRequestMappings());
    // 扫描出所有Controller的文档及其中所有的方法文档,包括返回值及参数的文档,这里的apiListingScanner类型为ApiListingScanner
    Multimap<String, ApiListing> apiListings = apiListingScanner.scan(listingContext);
    DocumentationBuilder group = new DocumentationBuilder()
        .name(context.getGroupName())
        .apiListingsByResourceGroupName(apiListings)
        .produces(context.getProduces())
        .consumes(context.getConsumes())
        .host(context.getHost())
        .schemes(context.getProtocols())
        .basePath(context.getPathProvider().getApplicationBasePath())
        .extensions(context.getVendorExtentions())
        .tags(tags);
    return group.build();
  }

继续跟进去ApiListingScanner

public Multimap<String, ApiListing> scan(ApiListingScanningContext context) {
    final Multimap<String, ApiListing> apiListingMap = LinkedListMultimap.create();
    int position = 0;
    
    Map<ResourceGroup, List<RequestMappingContext>> requestMappingsByResourceGroup
        = context.getRequestMappingsByResourceGroup();
    Collection<ApiDescription> additionalListings = pluginsManager.additionalListings(context);
    Set<ResourceGroup> allResourceGroups = FluentIterable.from(collectResourceGroups(additionalListings))
        .append(requestMappingsByResourceGroup.keySet())
        .toSet();
    // 这里的allResourceGroups可以看做就是所有的Controller
    for (final ResourceGroup resourceGroup : sortedByName(allResourceGroups)) {
      
      DocumentationContext documentationContext = context.getDocumentationContext();
      Set<ApiDescription> apiDescriptions = newHashSet();

      Map<String, Model> models = new LinkedHashMap<String, Model>();
      List<RequestMappingContext> requestMappings = nullToEmptyList(requestMappingsByResourceGroup.get(resourceGroup));
      // 扫描Controller下每一个包含@RequestMapping注解的方法
      for (RequestMappingContext each : sortedByMethods(requestMappings)) {
        // 扫描Model,包括方法返回值和参数,主要就是@ApiModel注解和属性上的@ApiModelProperty注解
        models.putAll(apiModelReader.read(each.withKnownModels(models)));
        // 扫描方法上的@ApiOperation注解和参数中的@ApiParam注解
        apiDescriptions.addAll(apiDescriptionReader.read(each));
      }

      List<ApiDescription> sortedApis = FluentIterable.from(apiDescriptions)
          .toSortedList(documentationContext.getApiDescriptionOrdering());

      String resourcePath = new ResourcePathProvider(resourceGroup)
          .resourcePath()
          .or(longestCommonPath(sortedApis))
          .orNull();

      PathProvider pathProvider = documentationContext.getPathProvider();
      String basePath = pathProvider.getApplicationBasePath();
      PathAdjuster adjuster = new PathMappingAdjuster(documentationContext);
      ApiListingBuilder apiListingBuilder = new ApiListingBuilder(context.apiDescriptionOrdering())
          .apiVersion(documentationContext.getApiInfo().getVersion())
          .basePath(adjuster.adjustedPath(basePath))
          .resourcePath(resourcePath)
          .produces(produces)
          .consumes(consumes)
          .host(host)
          .protocols(protocols)
          .securityReferences(securityReferences)
          .apis(sortedApis)
          .models(models)
          .position(position++)
          .availableTags(documentationContext.getTags());

      ApiListingContext apiListingContext = new ApiListingContext(
          context.getDocumentationType(),
          resourceGroup,
          apiListingBuilder);
      // 扫描Controller类上的@Api注解
      apiListingMap.put(resourceGroup.getGroupName(), pluginsManager.apiListing(apiListingContext));
    }
    return apiListingMap;
  }

至此所有文档信息已经收集完成了,接下来就是显示到页面上。

Swagger2Controller

http://localhost:8081/cnblogs/swagger-ui.html请求路径开始,这个swagger-ui.html是swagger框架提供的

SpringMVC通过SimpleUrlHandlerMapping处理器映射器根据swagger-ui.html查找到对应处理器为ResourceHttpRequestHandler,
对应的处理器适配器为HttpRequestHandlerAdapter。

swagger-ui.html会请求/v2/api-docs这个接口来获取文档信息,Swagger2Controller提供了此接口,对Swagger2Controller的配置有疑问的话,可以看上面的
@EnableSwagger2原理

@Controller
@ApiIgnore
public class Swagger2Controller {

  public static final String DEFAULT_URL = "/v2/api-docs";
  private final DocumentationCache documentationCache;
  private final ServiceModelToSwagger2Mapper mapper;
  private final JsonSerializer jsonSerializer;

  // 请求路径为 /v2/api-docs
  @RequestMapping(
      value = DEFAULT_URL,
      method = RequestMethod.GET,
      produces = { APPLICATION_JSON_VALUE, HAL_MEDIA_TYPE })
  @PropertySourcedMapping(
      value = "${springfox.documentation.swagger.v2.path}",
      propertyKey = "springfox.documentation.swagger.v2.path")
  @ResponseBody
  public ResponseEntity<Json> getDocumentation(
      @RequestParam(value = "group", required = false) String swaggerGroup,
      HttpServletRequest servletRequest) {
    // 组名为default
    String groupName = Optional.fromNullable(swaggerGroup).or(Docket.DEFAULT_GROUP_NAME);
    // documentationCache存储着所有的文档信息
    Documentation documentation = documentationCache.documentationByGroup(groupName);
    if (documentation == null) {
      return new ResponseEntity<Json>(HttpStatus.NOT_FOUND);
    }
    // 将文档对象转换成Swagger对象
    Swagger swagger = mapper.mapDocumentation(documentation);
    // 转成JSON字符串并响应给页面
    return new ResponseEntity<Json>(jsonSerializer.toJson(swagger), HttpStatus.OK);
  }
}

最终的文档数据为

点击查看
{
  "swagger": "2.0",
  "info": {
    "version": "1.0",
    "title": "接口文档",
    "contact": {
      "name": "strongmore",
      "email": "xxx@163.com"
    },
    "license": {
      "name": "strongmore"
    }
  },
  "host": "localhost:8081",
  "basePath": "/cnblogs",
  "tags": [
    {
      "name": "cnblogs-back-up-controller",
      "description": "Cnblogs Back Up Controller"
    },
    {
      "name": "订单接口",
      "description": "Order Controller"
    }
  ],
  "paths": {
    "/cnblogs/backup": {
      "get": {
        "tags": [
          "cnblogs-back-up-controller"
        ],
        "summary": "backUp",
        "operationId": "backUpUsingGET",
        "produces": [
          "*/*"
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not Found"
          }
        },
        "deprecated": false
      }
    },
    "/cnblogs/index": {
      "get": {
        "tags": [
          "cnblogs-back-up-controller"
        ],
        "summary": "index",
        "operationId": "indexUsingGET",
        "produces": [
          "*/*"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "schema": {
              "type": "string"
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not Found"
          }
        },
        "deprecated": false
      }
    },
    "/cnblogs/swagger/index": {
      "get": {
        "tags": [
          "cnblogs-back-up-controller"
        ],
        "summary": "swaggerIndex",
        "operationId": "swaggerIndexUsingGET",
        "produces": [
          "*/*"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "schema": {
              "$ref": "#/definitions/ModelAndView"
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not Found"
          }
        },
        "deprecated": false
      }
    },
    "/cnblogs/testSendMqtt": {
      "get": {
        "tags": [
          "cnblogs-back-up-controller"
        ],
        "summary": "testSendMqtt",
        "operationId": "testSendMqttUsingGET",
        "produces": [
          "*/*"
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not Found"
          }
        },
        "deprecated": false
      }
    },
    "/order/order/detail": {
      "get": {
        "tags": [
          "订单接口"
        ],
        "summary": "订单详情查询",
        "operationId": "queryOrderDetailUsingGET",
        "produces": [
          "*/*"
        ],
        "parameters": [
          {
            "name": "orderId",
            "in": "query",
            "description": "订单号",
            "required": false,
            "type": "string",
            "allowEmptyValue": false
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "schema": {
              "$ref": "#/definitions/订单信息模型"
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not Found"
          }
        },
        "deprecated": false
      }
    }
  },
  "definitions": {
    "ModelAndView": {
      "type": "object",
      "properties": {
        "empty": {
          "type": "boolean"
        },
        "model": {
          "type": "object"
        },
        "modelMap": {
          "type": "object",
          "additionalProperties": {
            "type": "object"
          }
        },
        "reference": {
          "type": "boolean"
        },
        "status": {
          "type": "string",
          "enum": [
            "100 CONTINUE",
            "101 SWITCHING_PROTOCOLS",
            "102 PROCESSING",
            "103 CHECKPOINT",
            "200 OK",
            "201 CREATED",
            "202 ACCEPTED",
            "203 NON_AUTHORITATIVE_INFORMATION",
            "204 NO_CONTENT",
            "205 RESET_CONTENT",
            "206 PARTIAL_CONTENT",
            "207 MULTI_STATUS",
            "208 ALREADY_REPORTED",
            "226 IM_USED",
            "300 MULTIPLE_CHOICES",
            "301 MOVED_PERMANENTLY",
            "302 FOUND",
            "302 MOVED_TEMPORARILY",
            "303 SEE_OTHER",
            "304 NOT_MODIFIED",
            "305 USE_PROXY",
            "307 TEMPORARY_REDIRECT",
            "308 PERMANENT_REDIRECT",
            "400 BAD_REQUEST",
            "401 UNAUTHORIZED",
            "402 PAYMENT_REQUIRED",
            "403 FORBIDDEN",
            "404 NOT_FOUND",
            "405 METHOD_NOT_ALLOWED",
            "406 NOT_ACCEPTABLE",
            "407 PROXY_AUTHENTICATION_REQUIRED",
            "408 REQUEST_TIMEOUT",
            "409 CONFLICT",
            "410 GONE",
            "411 LENGTH_REQUIRED",
            "412 PRECONDITION_FAILED",
            "413 PAYLOAD_TOO_LARGE",
            "413 REQUEST_ENTITY_TOO_LARGE",
            "414 URI_TOO_LONG",
            "414 REQUEST_URI_TOO_LONG",
            "415 UNSUPPORTED_MEDIA_TYPE",
            "416 REQUESTED_RANGE_NOT_SATISFIABLE",
            "417 EXPECTATION_FAILED",
            "418 I_AM_A_TEAPOT",
            "419 INSUFFICIENT_SPACE_ON_RESOURCE",
            "420 METHOD_FAILURE",
            "421 DESTINATION_LOCKED",
            "422 UNPROCESSABLE_ENTITY",
            "423 LOCKED",
            "424 FAILED_DEPENDENCY",
            "425 TOO_EARLY",
            "426 UPGRADE_REQUIRED",
            "428 PRECONDITION_REQUIRED",
            "429 TOO_MANY_REQUESTS",
            "431 REQUEST_HEADER_FIELDS_TOO_LARGE",
            "451 UNAVAILABLE_FOR_LEGAL_REASONS",
            "500 INTERNAL_SERVER_ERROR",
            "501 NOT_IMPLEMENTED",
            "502 BAD_GATEWAY",
            "503 SERVICE_UNAVAILABLE",
            "504 GATEWAY_TIMEOUT",
            "505 HTTP_VERSION_NOT_SUPPORTED",
            "506 VARIANT_ALSO_NEGOTIATES",
            "507 INSUFFICIENT_STORAGE",
            "508 LOOP_DETECTED",
            "509 BANDWIDTH_LIMIT_EXCEEDED",
            "510 NOT_EXTENDED",
            "511 NETWORK_AUTHENTICATION_REQUIRED"
          ]
        },
        "view": {
          "$ref": "#/definitions/View"
        },
        "viewName": {
          "type": "string"
        }
      },
      "title": "ModelAndView"
    },
    "View": {
      "type": "object",
      "properties": {
        "contentType": {
          "type": "string"
        }
      },
      "title": "View"
    },
    "订单信息模型": {
      "type": "object",
      "properties": {
        "createDate": {
          "type": "integer",
          "format": "int64",
          "description": "订单创建时间"
        },
        "orderId": {
          "type": "string",
          "description": "订单号"
        }
      },
      "title": "订单信息模型"
    }
  }
}

总结

  1. 通过@EnableSwagger2注解将swagger中各种扫描器及插件注册到容器中。
  2. DocumentationPluginsBootstrapper使用各种扫描器收集各种文档信息,最终保存到DocumentationCache对象中。
  3. Swagger2Controller提供一个接口,可以查询DocumentationCache中的文档信息。
  4. swagger-ui.html请求Swagger2Controller的接口获取到文档信息,展示到页面上。

扩展

swagger-bootstrap-ui 是 swagger 的一个增强包,提供了页面搜索和身份认证的功能,还可以通过 @ApiOperationSupport 注解来对接口排序。

引入依赖

<dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>swagger-bootstrap-ui</artifactId>
    <version>1.9.6</version>
</dependency>

开启身份认证

swagger:
  basic:
    # 开启身份认证功能
    enable: true
    username: root
    password: 123456

启用配置

@Configuration
@EnableSwagger2
@EnableSwaggerBootstrapUI
public class SwaggerConfig {

}

通过 @EnableSwaggerBootstrapUI 注解来开启此扩展,内部通过 SecurityConfiguration 配置来处理身份认证。

页面效果

通过 http://localhost:8090/cnblogs/doc.html 地址来访问

参考

Swagger官网
聊一聊Swagger ui登录功能实现方案

posted @ 2022-05-27 22:43  strongmore  阅读(1572)  评论(0编辑  收藏  举报