基于前两篇文章,进行整合
springcloud-gateway 聚合swagger3请求接口丢失appliactionName解决
springcloud-gateway聚合knife4j接口文档
为何要兼容?微服务开发者有的使用了swagger2版本,有的使用了swagger3版本,但暴露外部给前端使用的,均统一走网关处
兼容核心点:swagger3 返回缺少bathPath
网关配置
@Data
@ConfigurationProperties(prefix = MyGateWayProperties.PREFIX)
public class MyGateWayProperties {
public static final String PREFIX = "gateway";
/**
* swagger3 服务列表
*/
private List<String> swagger3Set;
/**
* 请求前缀
*/
private String apiPrefix;
}
请求文档的SwaggerResourcesProvider 进行调整,带上自定义网关请求前缀与 swagger文档后缀
@RestController
@RequestMapping("/swagger-resources")
public class SwaggerHandler {
@Autowired(required = false)
private SecurityConfiguration securityConfiguration;
@Autowired(required = false)
private UiConfiguration uiConfiguration;
private final SwaggerResourcesProvider provider;
@Autowired
public SwaggerHandler(SwaggerResourcesProvider provider) {
this.provider = provider;
}
@GetMapping("/configuration/security")
public Mono<ResponseEntity<SecurityConfiguration>> securityConfiguration() {
return Mono.just(new ResponseEntity<>(
Optional.ofNullable(securityConfiguration).orElse(SecurityConfigurationBuilder.builder().build()),
HttpStatus.OK));
}
@GetMapping("/configuration/ui")
public Mono<ResponseEntity<UiConfiguration>> uiConfiguration() {
return Mono.just(new ResponseEntity<>(
Optional.ofNullable(uiConfiguration).orElse(UiConfigurationBuilder.builder().build()), HttpStatus.OK));
}
@GetMapping
public Mono<ResponseEntity<List<SwaggerResource>>> swaggerResources() {
return Mono.just((new ResponseEntity<>(provider.get(), HttpStatus.OK)));
}
}
/**
* @author lei
* @create 2022-05-25 10:01
* @desc 网关处获取所有服务swagger文档
**/
@Slf4j
@Component
@Primary
public class SwaggerProvider implements SwaggerResourcesProvider {
/**
* swagger2默认的url后缀
*/
public static final String V3_API_DOCS = "/v3/api-docs";
public static final String V2_API_DOCS = "/v2/api-docs";
/**
* 路由加载器
*/
private final RouteLocator routeLocator;
private final MyGateWayProperties myGateWayProperties;
/**
* 网关应用名称
*/
@Value("${spring.application.name}")
private String gateway;
@Autowired
public SwaggerProvider(RouteLocator routeLocator, MyGateWayProperties myGateWayProperties) {
this.routeLocator = routeLocator;
this.myGateWayProperties = myGateWayProperties;
}
@Override
public List<SwaggerResource> get() {
List<SwaggerResource> resources = new ArrayList<>();
List<String> routeHosts = new ArrayList<>();
Flux<Route> routes = routeLocator.getRoutes();
// 获取所有可用的host,因为是注册表服务调用,所以实际是服务名
routes.filter(route -> route.getUri().getHost() != null)
.filter(route -> !gateway.equals(route.getUri().getHost()))
.subscribe(route -> routeHosts.add(route.getUri().getHost()));
// 去重下拉列表相同服务名文档,多个服务展示一个,当网关调用这个接口时,会自动通过负载均衡寻找服务host
Set<String> docServerSet = new HashSet<>();
routeHosts.forEach(instance -> {
String url;
if (myGateWayProperties.getSwagger3Set().contains(instance)) {
// 拼接url. /serviceId/v3/api-docs
url = myGateWayProperties.getApiPrefix() + "/" + instance.toLowerCase() + V3_API_DOCS;
} else {
// 拼接url. /serviceId/v2/api-docs
url = myGateWayProperties.getApiPrefix() + "/" + instance.toLowerCase() + V2_API_DOCS;
}
if (!docServerSet.contains(url)) {
docServerSet.add(url);
SwaggerResource swaggerResource = new SwaggerResource();
swaggerResource.setUrl(url);
swaggerResource.setName(instance);
resources.add(swaggerResource);
}
});
return resources;
}
}
网关添加全局过滤器,发现以访问swagger3后缀的url则重新返回体,携带上bathPath
/**
* @author lei
* @create 2022-05-26 14:36
* @desc swagger过滤器
* 解决swagger3 响应缺少bathPath,请求无法动态路由到服务的问题
**/
@Log4j2
@Component
public class SwaggerGlobalFilter implements GlobalFilter, Ordered {
public static final String BASE_PATH = "basePath";
private final MyGateWayProperties myGateWayProperties;
public SwaggerGlobalFilter(MyGateWayProperties myGateWayProperties) {
this.myGateWayProperties = myGateWayProperties;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String path = exchange.getRequest().getPath().toString();
if (!path.endsWith(SwaggerProvider.V3_API_DOCS)) {
return chain.filter(exchange);
}
path = path.replace(myGateWayProperties.getApiPrefix(), "");
String[] pathArray = path.split("/");
String basePath = pathArray[1];
ServerHttpResponse originalResponse = exchange.getResponse();
ServerHttpResponseDecorator newResponse = new ServerHttpResponseDecorator(originalResponse) {
@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
if (!Objects.equals(getStatusCode(), HttpStatus.OK)) {
return super.writeWith(body);
}
if (body instanceof Flux) {
Flux<? extends DataBuffer> fluxBody = Flux.from(body);
return super.writeWith(fluxBody.buffer().map(dataBuffers -> {
List<String> list = new ArrayList<>();
dataBuffers.forEach(dataBuffer -> {
byte[] content = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(content);
DataBufferUtils.release(dataBuffer);
list.add(new String(content, StandardCharsets.UTF_8));
});
String jsonStr = listToString(list);
JSONObject jsonObject = JSON.parseObject(jsonStr);
jsonObject.put(BASE_PATH, myGateWayProperties.getApiPrefix() + "/" + basePath);
jsonStr = jsonObject.toString();
return bufferFactory().wrap(jsonStr.getBytes());
}));
}
return super.writeWith(body);
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.putAll(super.getHeaders());
//由于修改了请求体的body,导致content-length长度不确定,因此使用分块编码
httpHeaders.remove(HttpHeaders.CONTENT_LENGTH);
httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
return httpHeaders;
}
private String listToString(List<String> list) {
StringBuilder stringBuilder = new StringBuilder();
for (String s : list) {
stringBuilder.append(s);
}
return stringBuilder.toString();
}
};
return chain.filter(exchange.mutate().response(newResponse).build());
}
@Override
public int getOrder() {
return -2;
}
}
网关获取到文档访问列表
访问具体服务的文档,触发全局过滤器
改写响应数据,访问swagger3文档,带上了bathPath
访问swagger2文档时,不受影响