• 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官网

  • 相关阅读:
    【Beta阶段】第五次Scrum Meeting
    wireshark怎么抓包、wireshark抓包详细图文教程
    Java环境变量的配置
    Office2007 每次打开斗需要检查 【配置进度】
    思科SVI接口和路由接口区别
    Windows Server 2008 R2之管理Sysvol文件夹
    Windows Server 2008 R2之六活动目录域服务的卸载
    Windows Server 2008 R2之五操作主控的管理
    Windows Server 2008 R2之三管理活动目录数据库
    23. Merge k Sorted Lists
  • 原文地址:https://www.cnblogs.com/strongmore/p/16308783.html
Copyright © 2020-2023  润新知