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配置类的作用:
- 指定扫描包路径,扫描很多类型为Plugin的Bean。
- 通过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": "订单信息模型"
}
}
}
总结
- 通过@EnableSwagger2注解将swagger中各种扫描器及插件注册到容器中。
- DocumentationPluginsBootstrapper使用各种扫描器收集各种文档信息,最终保存到DocumentationCache对象中。
- Swagger2Controller提供一个接口,可以查询DocumentationCache中的文档信息。
- 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
地址来访问