引子
由于我们公司在尝试着做前后端分离,那么后端的api文档是必须的,因此研究了一下swagger。swagger的2.0版本已经升级为springfox, 看这名字就知道springfox只能与springmvc做集成。
swagger介绍
- Swagger-Core,swagger最核心的部分,能够根据注解来生成swagger definition(json或者yaml格式)
- Swagger Codegen,顾名思义,根据swagger definition生成代码
- Swagger UI,用来展示swagger definition的web项目
- Swagger Editor,用来编辑swagger definition
本文主要涉及swagger-core,swagger-ui方面的内容
配置
- 首先添加maven依赖,通过maven repository可以知道兼容的spring的最低版本是4.1.8.RELEASE
<dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.4.0</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-annotations</artifactId> <version>2.6.4</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.6.4</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> <version>2.6.4</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.4.0</version> </dependency>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
2.swagger config配置
package com.xxx.swaggerimport static com.google.common.collect.Lists.newArrayListimport static springfox.documentation.builders.PathSelectors.antimport java.util.Listimport org.springframework.context.annotation.Beanimport org.springframework.context.annotation.ComponentScanimport org.springframework.context.annotation.Configurationimport org.springframework.web.servlet.config.annotation.EnableWebMvcimport springfox.documentation.builders.ApiInfoBuilderimport springfox.documentation.builders.ImplicitGrantBuilderimport springfox.documentation.builders.OAuthBuilderimport springfox.documentation.service.ApiInfoimport springfox.documentation.service.ApiKeyimport springfox.documentation.service.AuthorizationScopeimport springfox.documentation.service.GrantTypeimport springfox.documentation.service.LoginEndpointimport springfox.documentation.service.SecurityReferenceimport springfox.documentation.service.SecuritySchemeimport springfox.documentation.spi.DocumentationTypeimport springfox.documentation.spi.service.contexts.SecurityContextimport springfox.documentation.spring.web.plugins.Docketimport springfox.documentation.swagger.web.ApiKeyVehicleimport springfox.documentation.swagger.web.SecurityConfigurationimport springfox.documentation.swagger2.annotations.EnableSwagger2@Configuration@EnableWebMvc @EnableSwagger2 @ComponentScan("com.xxx")public class SpringfoxDocConfig { @Bean public Docket petApi() { return new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo()) .select() .build() .securitySchemes(newArrayList(oauth())) .securityContexts(newArrayList(securityContext())) } private ApiInfo apiInfo() { return new ApiInfoBuilder() .title("Springfox petstore API") .description("Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum " + "has been the industry's standard dummy text ever since the 1500s, when an unknown printer " + "took a " + "galley of type and scrambled it to make a type specimen book. It has survived not only five " + "centuries, but also the leap into electronic typesetting, remaining essentially unchanged. " + "It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum " + "passages, and more recently with desktop publishing software like Aldus PageMaker including " + "versions of Lorem Ipsum.") .termsOfServiceUrl("http://springfox.io") .contact("springfox") .license("Apache License Version 2.0") .licenseUrl("https://github.com/springfox/springfox/blob/master/LICENSE") .version("2.0") .build() } @Bean SecurityContext securityContext() { AuthorizationScope readScope = new AuthorizationScope("read:pets", "read your pets") AuthorizationScope[] scopes = new AuthorizationScope[1] scopes[0] = readScope SecurityReference securityReference = SecurityReference.builder() .reference("petstore_auth") .scopes(scopes) .build() return SecurityContext.builder() .securityReferences(newArrayList(securityReference)) .forPaths(ant("/api/pet.*")) .build() } @Bean SecurityScheme oauth() { return new OAuthBuilder() .name("petstore_auth") .grantTypes(grantTypes()) .scopes(scopes()) .build() } @Bean SecurityScheme apiKey() { return new ApiKey("api_key", "api_key", "header") } List<AuthorizationScope> scopes() { return newArrayList( new AuthorizationScope("write:pets", "modify pets in your account"), new AuthorizationScope("read:pets", "read your pets")) } List<GrantType> grantTypes() { GrantType grantType = new ImplicitGrantBuilder() .loginEndpoint(new LoginEndpoint("http://petstore.swagger.io/api/oauth/dialog")) .build() return newArrayList(grantType) } @Bean public SecurityConfiguration securityInfo() { return new SecurityConfiguration("abc", "123", "pets", "petstore", "123", ApiKeyVehicle.HEADER, "", ",") }}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
3.配置controller,这里仅举一例,详细完整的例子在springfox项目中的springfox-petstore模块里面
@Controller@RequestMapping(value = "/api/user", produces = MediaType.APPLICATION_JSON_VALUE)@Api(value = "/user", description = "Operations about user")public class UserController { UserRepository userRepository = new UserRepository(); static class UserRepository extends MapBackedRepository<String, User> { } @RequestMapping(value = "/{username}", method = GET) @ApiOperation(value = "Get user by user name", response = User.class) @ApiResponses(value = { @ApiResponse(code = 400, message = "Invalid username supplied"), @ApiResponse(code = 404, message = "User not found")}) public ResponseEntity<User> getUserByName( @ApiParam(value = "The name that needs to be fetched. Use user1 for testing. ", required = true) @PathVariable("username") String username) { User user = userRepository.get(username); if (null != user) { return new ResponseEntity<User>(user, HttpStatus.OK); } else { throw new NotFoundException(404, "User not found"); } }}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
4.下载swagger-ui, 将dist目录copy到web项目中
5.测试一下,访问http://host:port/v2/api-docs应该可以下载swagger definition,访问http://host:port/swagger-ui/index.html会展现如下页面
6.在有些项目中spring mvc的请求是以某一后缀(url-pattern, 例如 .do)结尾的, 这样就要做一些必要的修改,在swagger-ui.js中,修改如下
opts.responseContentType = $('div select[name=responseContentType]', $(this.el)).val(); opts.requestContentType = $('div select[name=parameterContentType]', $(this.el)).val(); $('.response_throbber', $(this.el)).show(); var suffix = ".do"; if (!this.model.path.endsWith(suffix)) { this.model.path = this.model.path + suffix; } if (isFileUpload) { $('.request_url', $(this.el)).html('<pre></pre>'); $('.request_url pre', $(this.el)).text(this.invocationUrl); opts.useJQuery = true; map.parameterContentType = 'multipart/form-data'; this.map = map; return this.model.execute(map, opts, this.showCompleteStatus, this.showErrorStatus, this); } else { this.map = map; return this.model.execute(map, opts, this.showCompleteStatus, this.showErrorStatus, this); }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
这样在swagger里面try it out的时候,即能发出正确的请求了
springfox源码走读
我们请求的地址是http://host:port/v2/api-docs,这个请求是Swagger2Controller
来处理的
@ApiIgnore @RequestMapping(value = "${springfox.documentation.swagger.v2.path:" + DEFAULT_URL + "}", method = RequestMethod.GET, produces = { APPLICATION_JSON_VALUE, HAL_MEDIA_TYPE }) public @ResponseBody ResponseEntity<Json> getDocumentation( @RequestParam(value = "group", required = false) String swaggerGroup, HttpServletRequest servletRequest) { String groupName = Optional.fromNullable(swaggerGroup).or(Docket.DEFAULT_GROUP_NAME); Documentation documentation = documentationCache.documentationByGroup(groupName); if (documentation == null) { return new ResponseEntity<Json>(HttpStatus.NOT_FOUND); } Swagger swagger = mapper.mapDocumentation(documentation); if (isNullOrEmpty(swagger.getHost())) { swagger.host(hostName(servletRequest)); } return new ResponseEntity<Json>(jsonSerializer.toJson(swagger), HttpStatus.OK); }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
文档的数据是从documentationCache
里面拿的,那么documentationCache
的数据又是怎么添加进去的呢?在DocumentationPluginsBootstrapper
中,其实是一个监听器,接收到ContextRefreshedEvent
事件后会扫描所有的文档插件
@Override public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) { if (initialized.compareAndSet(false, true)) { log.info("Context refreshed"); List<DocumentationPlugin> plugins = pluginOrdering() .sortedCopy(documentationPluginsManager.documentationPlugins()); log.info("Found {} custom documentation plugin(s)", plugins.size()); for (DocumentationPlugin each : plugins) { DocumentationType documentationType = each.getDocumentationType(); if (each.isEnabled()) { scanDocumentation(buildContext(each)); } else { log.info("Skipping initializing disabled plugin bean {} v{}", documentationType.getName(), documentationType.getVersion()); } } } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
那么scan的具体细节又是什么呢?在ApiDocumentationScanner
中我们看到DocumentationBuilder
先组装context的全局配置,然后apiListingScanner.scan(listingContext)
会扫描所有的controller的api信息,具体操作在ApiListingScanner
的scan方法中
public Documentation scan(DocumentationContext context) { ApiListingReferenceScanResult result = apiListingReferenceScanner.scan(context) ApiListingScanningContext listingContext = new ApiListingScanningContext(context, result.getResourceGroupRequestMappings()) 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()) .tags(toTags(apiListings)) Set<ApiListingReference> apiReferenceSet = newTreeSet(listingReferencePathComparator()) apiReferenceSet.addAll(apiListingReferences(apiListings, context)) ResourceListing resourceListing = new ResourceListingBuilder() .apiVersion(context.getApiInfo().getVersion()) .apis(from(apiReferenceSet).toSortedList(context.getListingReferenceOrdering())) .securitySchemes(context.getSecuritySchemes()) .info(context.getApiInfo()) .build() group.resourceListing(resourceListing) return group.build() }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
总的来说springfox的原理比较简单,但是在文档格式的规定上比较严格
参考
http://swagger.io/
https://github.com/swagger-api/swagger-core/wiki/Annotations