当系统中微服务数量越来越多时,如果任由这些服务散落在各处,那么最终管理每个项目的接口文档将是一件十分麻烦的事情,单是记住所有微服务的接口文档访问地址就是一件苦差事了。当如果能够将所有微服务项目的接口文档都统一汇总在同一个可视化页面,那么将大大减少我们的接口文档管理维护工作,为此,我们可以基于SpringCloudGateway网关+nacos+knife4j对所有微服务项目的接口文档进行聚合,从而实现我们想要的文档管理功能
注:本案例需要springboot提前整合nacos作为注册中心,springcloud整合nacos注册中心部分内容欢迎阅读这篇文章:Nacos注册中心的部署与用法详细介绍
开启gateway自动路由功能:
随着我们的系统架构不断地发展,系统中微服务的数量肯定会越来越多,我们不可能每添加一个服务,就在网关配置一个新的路由规则,这样的维护成本很大;特别在很多种情况,我们在请求路径中会携带一个路由标识方便进行转发,而这个路由标识一般都是服务在注册中心中的服务名,因此这是我们就可以开启springcloudgateway的自动路由功能,网关自动根据注册中心的服务名为每个服务创建一个router,将以服务名开头的请求路径转发到对应的服务,配置如下:
# enabled:默认为false,设置为true表明spring cloud gateway开启服务发现和路由的功能,网关自动根据注册中心的服务名为每个服务创建一个router,将以服务名开头的请求路径转发到对应的服务
spring.cloud.gateway.discovery.locator.enabled = true
# lowerCaseServiceId:启动 locator.enabled=true 自动路由时,路由的路径默认会使用大写ID,若想要使用小写ID,可将lowerCaseServiceId设置为true
spring.cloud.gateway.discovery.locator.lower-case-service-id = true
这里需要注意的是,如果我们的配置了server.servlet.context-path属性,这会导致自动路由失败的问题,因此我们需要做如下两个修改:
# 重写过滤链,解决项目设置了 server.servlet.context-path 导致 locator.enabled=true 默认路由策略404的问题
spring.cloud.gateway.discovery.locator.filters[0] = PreserveHostHeader
@Configuration
public class GatewayConfig
{
@Value ('${server.servlet.context-path}')
private String prefix;
/**
* 过滤 server.servlet.context-path 属性配置的项目路径,防止对后续路由策略产生影响,因为 gateway 网关不支持 servlet
*/
@Bean
@Order (-1)
public WebFilter apiPrefixFilter()
{
return (exchange, chain) ->
{
ServerHttpRequest request = exchange.getRequest();
String path = request.getURI().getRawPath();
path = path.startsWith(prefix) ? path.replaceFirst(prefix, '') : path;
ServerHttpRequest newRequest = request.mutate().path(path).build();
return chain.filter(exchange.mutate().request(newRequest).build());
};
}
}
至此,网关将自动根据注册中心的服务名为每个服务创建一个router,将以服务名开头的请求路径转发到对应的服务。
poxml文件引入knife4j依赖:
com.github.xiaoymin
knife4j-spring-boot-starter
2.0.4
io.springfox
springfox-swagger-ui
io.springfox
springfox-swagger2
配置SwaggerHeaderFilter:
在集成SpringCloudGateway网关的时候,会出现没有basePath的情况,例如定义的/user、/order等微服务前缀,因此我们需要在Gateway网关添加一个Filter过滤器
import org.apache.commons.lang3.StringUtils;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.web.server.ServerWebExchange;
@Configuration
public class SwaggerHeaderFilter extends AbstractGatewayFilterFactory
{
private static final String HEADER_NAME = 'X-Forwarded-Prefix';
private static final String URI = '/v2/api-docs';
@Override
public GatewayFilter apply(Object config)
{
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
String path = request.getURI().getPath();
if(StringUtils.endsWithIgnoreCase(path, URI))
{
String basePath = path.substring(0, path.lastIndexOf(URI));
ServerHttpRequest newRequest = request.mutate().header(HEADER_NAME, basePath).build();
ServerWebExchange newExchange = exchange.mutate().request(newRequest).build();
return chain.filter(newExchange);
}
else
{
return chain.filter(exchange);
}
};
}
}
重写swagger-resources:
在使用SpringBoot等单体架构集成swagger时,我们是基于包路径进行业务分组,然后在前端进行不同模块的展示,而在微服务架构下,一个服务就类似于原来我们写的一个业务组。springfox-swagger提供的分组接口是swagger-resource,返回的是分组接口名称、地址等信息,而在SpringCloud微服务架构下,我们需要重写该接口,改由通过网关的注册中心动态发现所有的微服务文档,代码如下:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import springfox.documentation.swagger.web.SwaggerResource;
import springfox.documentation.swagger.web.SwaggerResourcesProvider;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* 使用Spring Boot单体架构集成swagger时,是通过包路径进行业务分组,然后在前端进行不同模块的展示,而在微服务架构下,单个服务类似于原来业务组;
* springfox-swagger提供的分组接口是swagger-resource,返回的是分组接口名称、地址等信息;
* 在Spring Cloud微服务架构下,需要swagger-resource重写接口,由网关的注册中心动态发现所有的微服务文档
*/
@Primary
@Configuration
public class SwaggerResourceConfig implements SwaggerResourcesProvider
{
@Autowired
private RouteLocator routeLocator;
// 网关应用名称
@Value ('${spring.application.name}')
private String applicationName;
//接口地址
private static final String API_URI = '/v2/api-docs';
@Override
public List get() {
//接口资源列表
List resources = new ArrayList<>();
//服务名称列表
List routeHosts = new ArrayList<>();
// 获取所有可用的应用名称
routeLocator.getRoutes()
.filter(route -> route.getUri().getHost() != null)
.filter(route -> !applicationName.equals(route.getUri().getHost()))
.subscribe(route -> routeHosts.add(route.getUri().getHost()));
// 去重,多负载服务只添加一次
Set existsServer = new HashSet<>();
routeHosts.forEach(host -> {
// 拼接url
String url = '/' + host + API_URI;
//不存在则添加
if (!existsServer.contains(url)) {
existsServer.add(url);
SwaggerResource swaggerResource = new SwaggerResource();
swaggerResource.setUrl(url);
swaggerResource.setName(host);
resources.add(swaggerResource);
}
});
return resources;
}
}
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
import springfox.documentation.swagger.web.*;
import java.util.Optional;
/**
* 获取api接口信息
*/
@RestController
@RequestMapping ('/swagger-resources')
public class SwaggerHandler
{
@Autowired(required = false)
private SecurityConfiguration securityConfiguration;
@Autowired(required = false)
private UiConfiguration uiConfiguration;
private final SwaggerResourcesProvider swaggerResources;
@Autowired
public SwaggerHandler(SwaggerResourcesProvider swaggerResources) {
this.swaggerResources = swaggerResources;
}
@GetMapping('/configuration/security')
public Mono> securityConfiguration()
{
return Mono.just(new ResponseEntity<>(Optional.ofNullable(securityConfiguration).orElse(SecurityConfigurationBuilder.builder().build()), HttpStatus.OK));
}
@GetMapping ('/configuration/ui')
public Mono> uiConfiguration()
{
return Mono.just(new ResponseEntity<>(Optional.ofNullable(uiConfiguration).orElse(UiConfigurationBuilder.builder().build()), HttpStatus.OK));
}
@GetMapping('')
public Mono swaggerResources()
{
return Mono.just((new ResponseEntity<>(swaggerResources.get(), HttpStatus.OK)));
}
}
微服务架构中其他项目接入knife4j:
com.github.xiaoymin
knife4j-micro-spring-boot-starter
2.0.4
import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.async.DeferredResult;
import springfox.documentation.builders.*;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.paths.RelativePathProvider;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import javax.servlet.ServletContext;
/**
* @description: swagger配置文件
**/
@Configuration
@EnableSwagger2
@EnableKnife4j
public class Swagger2Config
{
@Value('${spring.profiles.active}')
private String env;
@Value('${spring.application.name}')
private String serviceName;
@Value('${gateway.service.name}')
private String serviceNameForGateway;
@Bean
public Docket createDocket(ServletContext servletContext)
{
Docket docket = new Docket(DocumentationType.SWAGGER_2)
.genericModelSubstitutes(DeferredResult.class)
.forCodeGeneration(true)
.pathMapping('/')
.select()
.build()
.apiInfo(new ApiInfoBuilder()
.title(serviceName + '接口文档')
.version('1.0')
.contact(new Contact('xxx','',''))
.license('XXX有限公司')
.build())
// 如果为生产环境,则不创建swagger
.enable(!'real'.equals(env));
// 在knife4j前端页面的地址路径中添加gateway网关的项目名,解决在调试接口、发送请求出现404的问题
docket.pathProvider(new RelativePathProvider(servletContext)
{
@Override
public String getApplicationBasePath()
{
return '/' + serviceNameForGateway + super.getApplicationBasePath();
}
});
return docket;
}
}
最终效果:
文章的再介绍knife4j官方提供的另一种接口文档聚合的方式:Aggregation微服务聚合组件,官方地址:https://doxiaominfo.com/knife4j/resources/,感兴趣的读者可以去官方看下如何使用
文章为作者独立观点,不代表 股票程序化软件自动交易接口观点