在spring cloud 的使用的时候,我发现测试起来很不方便,需要使用Postman等类似的工具来调用我们的接口,这显然是很麻烦的,那么有没有一种方式可以让我们在gateway里使用swagger来测试呢。本文基于Finchley.RELEASE和最新版的Finchley.SR2,这两个版本有所改动,后面介绍。
答案是肯定的,我查阅资料发现了之前有人实现了zuul网关的聚合swagger,通过他的思路我自己写了一些类,首先需要,在gateway网关中创建三个类,下面贴出来
SwaggerHandler
package com.e6yun.ms.config;
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;
/**
* @Description
* @Author changyandong@e6yun.com
* @Created Date: 2018/8/16 11:52
* @ClassName SwaggerHandler
* @Version: 1.0
*/
@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<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> swaggerResources() {
return Mono.just((new ResponseEntity<>(swaggerResources.get(), HttpStatus.OK)));
}
}
SwaggerProvider
package com.e6yun.ms.config;
import org.springframework.cloud.gateway.config.GatewayProperties;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;
import springfox.documentation.swagger.web.SwaggerResource;
import springfox.documentation.swagger.web.SwaggerResourcesProvider;
import java.util.ArrayList;
import java.util.List;
/**
* @Description
* @Author changyandong@e6yun.com
* @Created Date: 2018/8/15 16:04
* @ClassName SwaggerProvider
* @Version: 1.0
*/
@Component
@Primary
public class SwaggerProvider implements SwaggerResourcesProvider {
public static final String API_URI = "/v2/api-docs";
private final RouteLocator routeLocator;
private final GatewayProperties gatewayProperties;
public SwaggerProvider(RouteLocator routeLocator, GatewayProperties gatewayProperties) {
this.routeLocator = routeLocator;
this.gatewayProperties = gatewayProperties;
}
@Override
public List<SwaggerResource> get() {
List<SwaggerResource> resources = new ArrayList<>();
List<String> routes = new ArrayList<>();
//取出gateway的route
routeLocator.getRoutes().subscribe(route -> routes.add(route.getId()));
//结合配置的route-路径(Path),和route过滤,只获取有效的route节点
gatewayProperties.getRoutes().stream().filter(routeDefinition -> routes.contains(routeDefinition.getId()))
.forEach(routeDefinition -> routeDefinition.getPredicates().stream()
.filter(predicateDefinition -> ("Path").equalsIgnoreCase(predicateDefinition.getName()))
.forEach(predicateDefinition -> resources.add(swaggerResource(routeDefinition.getId(),
predicateDefinition.getArgs().get(""pattern"")
.replace("/**", API_URI)))));
return resources;
}
private SwaggerResource swaggerResource(String name, String location) {
SwaggerResource swaggerResource = new SwaggerResource();
swaggerResource.setName(name);
swaggerResource.setLocation(location);
swaggerResource.setSwaggerVersion("2.0");
return swaggerResource;
}
}
SwaggerHeaderFilter 这个类只是在Finchley.RELEASE版本需要实现,SR2版本无需实现。
package com.e6yun.ms.config;
import org.apache.commons.lang3.StringUtils;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
/**
* @Description
* @Author changyandong@e6yun.com
* @Created Date: 2018/8/16 12:29
* @ClassName SwaggerHeaderFilter
* @Version: 1.0
*/
@Component
public class SwaggerHeaderFilter extends AbstractGatewayFilterFactory {
private static final String HEADER_NAME = "X-Forwarded-Prefix";
private static final String HOST_NAME = "X-Forwarded-Host";
@Override
public GatewayFilter apply(Object config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
String path = request.getURI().getPath();
if (!StringUtils.endsWithIgnoreCase(path, SwaggerProvider.API_URI)) {
return chain.filter(exchange);
}
String basePath = path.substring(0, path.lastIndexOf(SwaggerProvider.API_URI));
ServerHttpRequest newRequest = request.mutate().header(HEADER_NAME, basePath).build();
ServerWebExchange newExchange = exchange.mutate().request(newRequest).build();
return chain.filter(newExchange);
};
}
}
之后你只需要在你的路由上添加一个配置,这里贴出我的路由配置,这是properties的配置,当然yml的配置也是可以的,我这里就不贴了,直接去官方api里查,官方api中没有properties的配置方式,我在这里贴出
spring.cloud.gateway.routes[0].id = terminal-rpc-impl
spring.cloud.gateway.routes[0].uri = lb://RPC-IMPL-TERMINAL/
spring.cloud.gateway.routes[0].predicates[0].name = Path
spring.cloud.gateway.routes[0].predicates[0].args["pattern"] = /terminal-rpc-impl/**
#SR2版本删除这个
spring.cloud.gateway.routes[0].filters[0] = SwaggerHeaderFilter
spring.cloud.gateway.routes[0].filters[1] = StripPrefix=1
spring.cloud.gateway.routes[1].id = terminal-api-web
spring.cloud.gateway.routes[1].uri = lb://TERMINAL-API-WEB/
spring.cloud.gateway.routes[1].predicates[0].name = Path
spring.cloud.gateway.routes[1].predicates[0].args["pattern"] = /terminal-api-web/**
spring.cloud.gateway.routes[1].filters[0] = SwaggerHeaderFilter
spring.cloud.gateway.routes[1].filters[1] = StripPrefix=1
当然这比较麻烦,每次新写一个接口还要去新增一组配置,我又实现了一种基于eureka服务注册发现机制的实现,只需要重写上面的SwaggerProvider这个类,就可以通过DiscoveryClientRouteDefinitionLocator这个服务发现的路由处理器,来为我们服务。下面贴上代码。
package com.e6yun.ms.config;
import org.springframework.cloud.gateway.config.GatewayProperties;
import org.springframework.cloud.gateway.discovery.DiscoveryClientRouteDefinitionLocator;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;
import springfox.documentation.swagger.web.SwaggerResource;
import springfox.documentation.swagger.web.SwaggerResourcesProvider;
import java.util.ArrayList;
import java.util.List;
/**
* @Description
* @Author changyandong@e6yun.com
* @Created Date: 2018/8/15 16:04
* @ClassName SwaggerProvider
* @Version: 1.0
*/
@Component
@Primary
public class SwaggerProvider implements SwaggerResourcesProvider {
public static final String API_URI = "/v2/api-docs";
public static final String EUREKA_SUB_PRIX = "CompositeDiscoveryClient_";
private final DiscoveryClientRouteDefinitionLocator routeLocator;
public SwaggerProvider(DiscoveryClientRouteDefinitionLocator routeLocator) {
this.routeLocator = routeLocator;
}
@Override
public List<SwaggerResource> get() {
List<SwaggerResource> resources = new ArrayList<>();
List<String> routes = new ArrayList<>();
//从DiscoveryClientRouteDefinitionLocator 中取出routes,构造成swaggerResource
routeLocator.getRouteDefinitions().subscribe(routeDefinition -> {
resources.add(swaggerResource(routeDefinition.getId().substring(EUREKA_SUB_PRIX.length()),routeDefinition.getPredicates().get(0).getArgs().get("pattern").replace("/**", API_URI)));
});
return resources;
}
private SwaggerResource swaggerResource(String name, String location) {
SwaggerResource swaggerResource = new SwaggerResource();
swaggerResource.setName(name);
swaggerResource.setLocation(location);
swaggerResource.setSwaggerVersion("2.0");
return swaggerResource;
}
}
这样写完后,页面就可以发现注册到eureka的服务了。
这时,由于我们使用的是服务发现的routes,我们写的SwaggerHeaderFilter 不再生效了,所以这里访问会丢失服务名,这时我们需要在配置文件中添加一条语句,这里追加一个default-filters即可。SR2版本无需写
spring.cloud.gateway.default-filters[0]=SwaggerHeaderFilter
我这里还重写了spring cloud gateway的ForwardedHeadersFilter这是由于我们使用的swagger版本是2.6.1,新版的代码中它修复了这个bug 它里面的源码有一处是这么写的
package springfox.documentation.swagger2.web;
import javax.servlet.http.HttpServletRequest;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import org.springframework.web.util.UriComponents;
public class HostNameProvider {
public HostNameProvider() {
throw new UnsupportedOperationException();
}
static UriComponents componentsFrom(HttpServletRequest request) {
ServletUriComponentsBuilder builder = ServletUriComponentsBuilder.fromServletMapping(request);
ForwardedHeader forwarded = ForwardedHeader.of(request.getHeader(ForwardedHeader.NAME));
String proto = StringUtils.hasText(forwarded.getProto()) ? forwarded.getProto() : request.getHeader("X-Forwarded-Proto");
String forwardedSsl = request.getHeader("X-Forwarded-Ssl");
if (StringUtils.hasText(proto)) {
builder.scheme(proto);
} else if (StringUtils.hasText(forwardedSsl) && forwardedSsl.equalsIgnoreCase("on")) {
builder.scheme("https");
}
String host = forwarded.getHost();
host = StringUtils.hasText(host) ? host : request.getHeader("X-Forwarded-Host");
if (!StringUtils.hasText(host)) {
return builder.build();
} else {
String[] hosts = StringUtils.commaDelimitedListToStringArray(host);
String hostToUse = hosts[0];
if (hostToUse.contains(":")) {
String[] hostAndPort = StringUtils.split(hostToUse, ":");
builder.host(hostAndPort[0]);
builder.port(Integer.parseInt(hostAndPort[1]));
} else {
builder.host(hostToUse);
builder.port(-1);
}
String port = request.getHeader("X-Forwarded-Port");
if (StringUtils.hasText(port)) {
// 这里他写了对post转int的操作,但是gateway传入的port是一个String类型的,导致转换异常
builder.port(Integer.parseInt(port));
}
return builder.build();
}
}
}
为此我们有两种方案:
1.重写这个类
2.重写gateway中传过来port的类
我最终选择了2号方案,原因是我们做的这个聚合swagger gateway只是用来做开发测试使用,所以这个gateway和我们正式的gateway不是一个东西,但是你去重写了swagger的源码,将会导致所有的服务的swagger源码都被修改,没有必要。所以我重写了ForwardedHeadersFilter
package org.springframework.cloud.gateway.filter.headers;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedCaseInsensitiveMap;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import java.net.InetSocketAddress;
import java.net.URI;
import java.util.*;
/**
* @Description
* @Author changyandong@e6yun.com
* @Created Date: 2018/8/16 17:14
* @ClassName ForwardedHeadersFilter
* @Version: 1.0
*/
@Component
public class ForwardedHeadersFilter implements HttpHeadersFilter, Ordered {
public static final String FORWARDED_HEADER = "Forwarded";
public ForwardedHeadersFilter() {
}
@Override
public int getOrder() {
return 0;
}
@Override
public HttpHeaders filter(HttpHeaders input, ServerWebExchange exchange) {
ServerHttpRequest request = exchange.getRequest();
HttpHeaders updated = new HttpHeaders();
input.entrySet().stream().filter((entry) -> {
return !((String)entry.getKey()).toLowerCase().equalsIgnoreCase("Forwarded");
}).forEach((entry) -> {
updated.addAll((String)entry.getKey(), (List)entry.getValue());