Spring Gateway聚合Swagger在线文档
为什么需要聚合?
微服务模块众多,如果不聚合文档,则访问每个服务的API文档都需要单独访问一个Swagger UI界面,这么做客户端能否接受?
既然使用了微服务,就应该有统一的API文档入口。
如何聚合?
统一的文档入口显然应该聚合到网关中,通过网关的入口统一映射到各个模块。
本文采用Spring Cloud Gateway
聚合 Swagger
的方式生成API文档。
案例源码结构如下:
| spring-gateway-swagger
|–gateway – 网关
|–swagger-ui – swagger配置工程
|–user-center – 用户服务
|–order – 订单服务
单个服务如何聚合Swagger?
创建swagger配置工程
1.添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<scope>provided</scope>
</dependency>
<!-- swagger ui -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>swagger-bootstrap-ui</artifactId>
</dependency>
2.基础配置类
后续每个微服务依赖后即可通过配置控制文档属性
@Configuration
@ConfigurationProperties(prefix = "swagger")
public class SwaggerConfig {
private String title;
private String host;
private String description;
private String version;
private String docs = "v2/api-docs";
private boolean disable = false;
private String basePackage = "com.gitee.xqxyxchy";
private String termsOfServiceUrl = "https://gitee.com/xqxyxchy";
private String contactName = "xqxyxchy";
private String contactEmail = "xqxyxchy@126.com";
private String contactUrl = "https://gitee.com/xqxyxchy";
private Set<StringVendorExtension> stringExtensions = new HashSet<>();
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getDocs() {
return docs;
}
public void setDocs(String docs) {
this.docs = docs;
}
public boolean isDisable() {
return disable;
}
public void setDisable(boolean disable) {
this.disable = disable;
}
public String getBasePackage() {
return basePackage;
}
public void setBasePackage(String basePackage) {
this.basePackage = basePackage;
}
public String getTermsOfServiceUrl() {
return termsOfServiceUrl;
}
public void setTermsOfServiceUrl(String termsOfServiceUrl) {
this.termsOfServiceUrl = termsOfServiceUrl;
}
public String getContactName() {
return contactName;
}
public void setContactName(String contactName) {
this.contactName = contactName;
}
public String getContactEmail() {
return contactEmail;
}
public void setContactEmail(String contactEmail) {
this.contactEmail = contactEmail;
}
public String getContactUrl() {
return contactUrl;
}
public void setContactUrl(String contactUrl) {
this.contactUrl = contactUrl;
}
public Set<StringVendorExtension> getStringExtensions() {
return stringExtensions;
}
public void setStringExtensions(Set<StringVendorExtension> stringExtensions) {
this.stringExtensions = stringExtensions;
}
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
@SuppressWarnings("rawtypes")
public List<VendorExtension> getExtensions() {
List<VendorExtension> result = new ArrayList<>();
if(null == getStringExtensions()) {
return result;
}
for(StringVendorExtension stringExtension : getStringExtensions()) {
result.add(new springfox.documentation.service.StringVendorExtension(stringExtension.getName(), stringExtension.getValue()));
}
return result;
}
@Override
public String toString() {
return "SwaggerConfig [disable=" + isDisable() + ", host=" + getHost() + ", basePackage=" + getBasePackage() + "]";
}
}
class StringVendorExtension implements VendorExtension<String> {
private String name;
private String value;
public StringVendorExtension() {
super();
}
public StringVendorExtension(String name, String value) {
super();
this.name = name;
this.value = value;
}
public void setName(String name) {
this.name = name;
}
public void setValue(String value) {
this.value = value;
}
@Override
public String getName() {
return name;
}
@Override
public String getValue() {
return value;
}
}
3.Swagger文档信息装配类
支持多个基础包扫描
支持添加自定义参数
@Configuration
@EnableSwagger2
public class Swagger2Configuration extends WebMvcConfigurationSupport {
private static Logger logger = LoggerFactory.getLogger(Swagger2Configuration.class);
@Autowired
@Lazy
private SwaggerConfig swaggerConfig;
@Bean
public Docket createRestApi() {
if(logger.isDebugEnabled()) {
logger.debug("Starting Swagger");
}
logger.info("swaggerConfig ==> {}", swaggerConfig);
StopWatch watch = new StopWatch();
watch.start();
Docket docket0 = new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo());
String host = swaggerConfig.getHost();
if(StringUtils.hasText(host)) {
docket0.host(host);
}
ApiSelectorBuilder builder = docket0.select();
builder.apis(basePackage(swaggerConfig.getBasePackage()));
builder.apis(RequestHandlerSelectors.withClassAnnotation(Api.class));
builder.apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class));
if(swaggerConfig.isDisable()){
builder.paths(PathSelectors.none());
}else{
builder.paths(PathSelectors.any());
}
Docket docket = builder.build();
watch.stop();
if(logger.isDebugEnabled()) {
logger.debug("Started Swagger in {} ms", watch.getTotalTimeMillis());
}
return docket;
}
/**
* Predicate that matches RequestHandler with given base package name for the class of the handler method.
* This predicate includes all request handlers matching the provided basePackage
*
* @param basePackage - base package of the classes
* @return this
*/
public Predicate<RequestHandler> basePackage(final String basePackage) {
return new Predicate<RequestHandler>() {
@Override
public boolean apply(RequestHandler input) {
return declaringClass(input).transform(handlerPackage(basePackage)).or(true);
}
};
}
/**
* 处理包路径配置规则,支持多路径扫描匹配以逗号隔开
*
* @param basePackage 扫描包路径
* @return Function
*/
private static Function<Class<?>, Boolean> handlerPackage(final String basePackage) {
return new Function<Class<?>, Boolean>() {
@Override
public Boolean apply(Class<?> input) {
for (String strPackage : basePackage.split(",")) {
if(Objects.isNull(input)) {
continue;
}
if(Objects.isNull(input.getPackage())) {
continue;
}
if(!StringUtils.hasText(strPackage)) {
continue;
}
boolean isMatch = input.getPackage().getName().startsWith(strPackage);
if (isMatch) {
return true;
}
}
return false;
}
};
}
/**
* @param input RequestHandler
* @return Optional
*/
@SuppressWarnings("deprecation")
private static Optional<? extends Class<?>> declaringClass(RequestHandler input) {
return Optional.fromNullable(input.declaringClass());
}
@Bean
public UiConfiguration uiConfig() {
return UiConfigurationBuilder.builder().validatorUrl("").build();
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("swagger-ui.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("doc.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
}
private ApiInfo apiInfo() {
ApiInfo info = new ApiInfoBuilder()
.extensions(swaggerConfig.getExtensions())
.title(swaggerConfig.getTitle())
.description(swaggerConfig.getDescription())
.termsOfServiceUrl(swaggerConfig.getTermsOfServiceUrl())
.version(swaggerConfig.getVersion())
.contact(new Contact(swaggerConfig.getContactName(), swaggerConfig.getContactUrl(), swaggerConfig.getContactEmail()))
.build();
return info;
}
@Value("${spring.jackson.date-format}")
private String pattern;
@Bean
public LocalDateTimeSerializer localDateTimeSerializer() {
return new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(pattern));
}
}
4.微服务添加引用
<dependency>
<groupId>com.gitee.xqxyxchy</groupId>
<artifactId>swagger-ui</artifactId>
</dependency>
5.微服务添加配置
swagger:
title: 用户中心API文档
description: 用户中心接口文档说明
string-extensions:
- name: apiPrefix
value: /xqxyxchy/uc
至此单个服务的配置完成,此时我们可以验证一下,直接访问:http://127.0.0.1:5101/v2/api-docs,结果如下图:
网关如何聚合Swagger?
网关聚合的思想很简单,就是从路由中获取微服务的访问地址,然后拼接上 /v2/api-docs 即可。
1.添加依赖
<!-- swagger ui -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>swagger-bootstrap-ui</artifactId>
</dependency>
2.实现资源接口SwaggerResourcesProvider
生成聚合文档路由数据
@Component
@Primary
public class GatewaySwaggerResourcesProvider implements SwaggerResourcesProvider {
/**
* swagger3默认的url后缀
*/
private static final String SWAGGER2URL = "/v2/api-docs";
/**
* 网关路由
*/
@Autowired
private RouteLocator routeLocator;
/**
* 网关应用名称
*/
@Value("${spring.application.name}")
private String self;
/**
* 对于gateway来说这块比较重要 让swagger能找到对应的服务
*/
@Override
public List<SwaggerResource> get() {
List<SwaggerResource> resources = new ArrayList<>();
Map<String, String> routeHosts = Maps.newLinkedHashMap();
// 获取所有可用的host:serviceId
routeLocator.getRoutes().filter(route -> route.getUri().getHost() != null)
.filter(route -> !self.equals(route.getUri().getHost()))
.subscribe(route -> {
Object routeObject = route.getMetadata().get("route");
if(Objects.nonNull(routeObject)) {
routeHosts.put(route.getUri().getHost(), routeObject.toString());
}
});
// 记录已经添加过的server
Set<String> dealed = new HashSet<>();
routeHosts.keySet().forEach(instance -> {
String uri = routeHosts.get(instance);
// 拼接url
String url = uri + SWAGGER2URL;
if (!dealed.contains(url)) {
dealed.add(url);
SwaggerResource swaggerResource = new SwaggerResource();
swaggerResource.setUrl(url);
swaggerResource.setName(instance);
resources.add(swaggerResource);
}
});
return resources;
}
}
3.重写ApiResourceController接口类
spring gateway不支持webmvc,需要手动注入资源接口类
@Controller
@ApiIgnore
@RequestMapping("/swagger-resources")
public class GatewayApiResourceController {
@Autowired(required = false)
private SecurityConfiguration securityConfiguration;
@Autowired(required = false)
private UiConfiguration uiConfiguration;
private final SwaggerResourcesProvider swaggerResources;
@Autowired
public GatewayApiResourceController(SwaggerResourcesProvider swaggerResources) {
this.swaggerResources = swaggerResources;
}
@RequestMapping(value = "/configuration/security")
@ResponseBody
public ResponseEntity<SecurityConfiguration> securityConfiguration() {
return new ResponseEntity<SecurityConfiguration>(
Optional.fromNullable(securityConfiguration).or(SecurityConfigurationBuilder.builder().build()),
HttpStatus.OK);
}
@RequestMapping(value = "/configuration/ui")
@ResponseBody
public ResponseEntity<UiConfiguration> uiConfiguration() {
return new ResponseEntity<UiConfiguration>(
Optional.fromNullable(uiConfiguration).or(UiConfigurationBuilder.builder().build()), HttpStatus.OK);
}
@RequestMapping
@ResponseBody
public ResponseEntity<List<SwaggerResource>> swaggerResources() {
return new ResponseEntity<List<SwaggerResource>>(swaggerResources.get(), HttpStatus.OK);
}
}
网关的配置这里就完成,此时启动网关、用户、订单服务,直接访问网关的文档:http://localhost:5100/doc.html,结果如下图:
相关源码已上传gitee SpringGateway聚合Swagger接口文档