ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

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

2022-05-27 23:03:22  阅读:161  来源: 互联网

标签:context Swagger description SpringMVC 文档 整合 new import 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官网

标签:context,Swagger,description,SpringMVC,文档,整合,new,import,swagger
来源: https://www.cnblogs.com/strongmore/p/16308783.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有