spring cloud gateway聚合swagger

在spring cloud 的使用的时候,我发现测试起来很不方便,需要使用Postman等类似的工具来调用我们的接口,这显然是很麻烦的,那么有没有一种方式可以让我们在gateway里使用swagger来测试呢。本文基于Finchley.RELEASE和最新版的Finchley.SR2,这两个版本有所改动,后面介绍。

答案是肯定的,我查阅资料发现了之前有人实现了zuul网关的聚合swagger,通过他的思路我自己写了一些类,首先需要,在gateway网关中创建三个类,下面贴出来

SwaggerHandler

package com.cyd.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@cyd.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.cyd.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@cyd.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.cyd.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@cyd.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.cyd.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@cyd.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的服务了。

image.png

这时,由于我们使用的是服务发现的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@cyd.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());

        });

        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类型。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343

推荐阅读更多精彩内容