spring cloud gateway聚合swagger

在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> 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)));

    }

}

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 [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";

    private final RouteLocator routeLocator;

    private final GatewayProperties gatewayProperties;

    public SwaggerProvider(RouteLocator routeLocator, GatewayProperties gatewayProperties) {

        this.routeLocator = routeLocator;

        this.gatewayProperties = gatewayProperties;

    }

    @Override

    public List get() {

        List resources = new ArrayList<>();

        List 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 [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);

        };

    }

}

之后你只需要在你的路由上添加一个配置,这里贴出我的路由配置,这是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 [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 get() {

        List resources = new ArrayList<>();

        List 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的服务了。

spring cloud gateway聚合swagger_第1张图片

这时,由于我们使用的是服务发现的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 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 parse(List values) {

        ArrayList 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 result = splitIntoCaseInsensitiveMap(pairs);

        if (result == null) {

            return null;

        } else {

            ForwardedHeadersFilter.Forwarded forwarded = new ForwardedHeadersFilter.Forwarded(result);

            return forwarded;

        }

    }

    static LinkedCaseInsensitiveMap splitIntoCaseInsensitiveMap(String[] pairs) {

        if (ObjectUtils.isEmpty(pairs)) {

            return null;

        } else {

            LinkedCaseInsensitiveMap 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 values;

        public Forwarded() {

            this.values = new HashMap();

        }

        public Forwarded(Map 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 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类型。

你可能感兴趣的:(spring cloud gateway聚合swagger)