在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 [email protected]
* @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)));
}
}
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 [email protected]
* @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);
};
}
}
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;
最後就是我們能從註冊中心發現服務的地方了 SwaggerProvider這個類可以從註冊中心拉取服務列表 我們通過配置 將註冊服務的路由轉換成swagger-api
/**
* @Description
* @Author [email protected]
* @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 [email protected]
* @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());
});
List<ForwardedHeadersFilter.Forwarded> forwardeds = parse(input.get("Forwarded"));
Iterator var7 = forwardeds.iterator();
while(var7.hasNext()) {
ForwardedHeadersFilter.Forwarded f = (ForwardedHeadersFilter.Forwarded)var7.next();
updated.add("Forwarded", f.toString());
}
URI uri = request.getURI();
String host = input.getFirst("Host");
ForwardedHeadersFilter.Forwarded forwarded = (new ForwardedHeadersFilter.Forwarded()).put("host", host).put("proto", uri.getScheme());
InetSocketAddress remoteAddress = request.getRemoteAddress();
//改了這裏
if (remoteAddress != null) {
String forValue = remoteAddress.getAddress().getHostAddress();
int port = remoteAddress.getPort();
if (port >= 0) {
forValue = forValue + ":" + port;
}
forwarded.put("for", forValue);
}
updated.add("Forwarded", forwarded.toHeaderValue());
return updated;
}
static List<ForwardedHeadersFilter.Forwarded> parse(List<String> values) {
ArrayList<ForwardedHeadersFilter.Forwarded> forwardeds = new ArrayList();
if (CollectionUtils.isEmpty(values)) {
return forwardeds;
} else {
Iterator var2 = values.iterator();
while(var2.hasNext()) {
String value = (String)var2.next();
ForwardedHeadersFilter.Forwarded forwarded = parse(value);
forwardeds.add(forwarded);
}
return forwardeds;
}
}
static ForwardedHeadersFilter.Forwarded parse(String value) {
String[] pairs = StringUtils.tokenizeToStringArray(value, ";");
LinkedCaseInsensitiveMap<String> result = splitIntoCaseInsensitiveMap(pairs);
if (result == null) {
return null;
} else {
ForwardedHeadersFilter.Forwarded forwarded = new ForwardedHeadersFilter.Forwarded(result);
return forwarded;
}
}
static LinkedCaseInsensitiveMap<String> splitIntoCaseInsensitiveMap(String[] pairs) {
if (ObjectUtils.isEmpty(pairs)) {
return null;
} else {
LinkedCaseInsensitiveMap<String> result = new LinkedCaseInsensitiveMap();
String[] var2 = pairs;
int var3 = pairs.length;
for(int var4 = 0; var4 < var3; ++var4) {
String element = var2[var4];
String[] splittedElement = StringUtils.split(element, "=");
if (splittedElement != null) {
result.put(splittedElement[0].trim(), splittedElement[1].trim());
}
}
return result;
}
}
static class Forwarded {
private static final char EQUALS = '=';
private static final char SEMICOLON = ';';
private final Map<String, String> values;
public Forwarded() {
this.values = new HashMap();
}
public Forwarded(Map<String, String> values) {
this.values = values;
}
public ForwardedHeadersFilter.Forwarded put(String key, String value) {
this.values.put(key, value);
return this;
}
private String quoteIfNeeded(String s) {
return s != null && s.contains(":") ? "\"" + s + "\"" : s;
}
public String get(String key) {
return (String)this.values.get(key);
}
Map<String, String> getValues() {
return this.values;
}
public String toString() {
return "Forwarded{values=" + this.values + '}';
}
public String toHeaderValue() {
StringBuilder builder = new StringBuilder();
Map.Entry entry;
for(Iterator var2 = this.values.entrySet().iterator(); var2.hasNext(); builder.append((String)entry.getKey()).append('=').append((String)entry.getValue())) {
entry = (Map.Entry)var2.next();
if (builder.length() > 0) {
builder.append(';');
}
}
return builder.toString();
}
}
}
這樣傳入的值就是能被正確的解析爲int類型。