整合gateway+openapi3

文章讲述了如何在SpringCloudGateway中整合Swagger配置,自动识别LB模块的API文档,并解决Swagger页面跨域问题的方法,包括修改`/v3/api-docs`接口返回数据以添加网关地址。
摘要由CSDN通过智能技术生成

依赖

springboot 2.x 版本

<springdoc.version>1.8.0</springdoc.version> 
<!-- swagger3 接口文档生成器 -->
            <dependency>
                <groupId>org.springdoc</groupId>
                <artifactId>springdoc-openapi-ui</artifactId>
                <version>${springdoc.version}</version>
            </dependency>
            <dependency>
                <groupId>org.springdoc</groupId>
                <artifactId>springdoc-openapi-webflux-ui</artifactId>
                <version>${springdoc.version}</version>
            </dependency>
            <dependency>
                <groupId>org.springdoc</groupId>
                <artifactId>springdoc-openapi-webmvc-core</artifactId>
                <version>${springdoc.version}</version>
            </dependency>
            <dependency>
                <groupId>org.springdoc</groupId>
                <artifactId>springdoc-openapi-common</artifactId>
                <version>${springdoc.version}</version>
            </dependency>


springboot 3.x 版本

 <springdoc.version>2.6.0</springdoc.version>
  <!-- swagger3 接口文档生成器 -->
            <dependency>
                <groupId>org.springdoc</groupId>
                <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
                <version>${springdoc.version}</version>
            </dependency>
            <!--webflux-ui用于webflux项目(此处我是为了给gateway网关服务使用) -->
            <dependency>
                <groupId>org.springdoc</groupId>
                <artifactId>springdoc-openapi-starter-webflux-ui</artifactId>
                <version>${springdoc.version}</version>
            </dependency>
            <dependency>
                <groupId>org.springdoc</groupId>
                <artifactId>springdoc-openapi-starter-common</artifactId>
                <version>${springdoc.version}</version>
            </dependency>

一、整合配置

springboot2.x

package com.tly.common.gateway.config;

import org.apache.commons.lang3.StringUtils;
import org.springdoc.core.AbstractSwaggerUiConfigProperties;
import org.springdoc.core.Constants;
import org.springdoc.core.SwaggerUiConfigProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.gateway.handler.predicate.PredicateDefinition;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionLocator;
import org.springframework.context.annotation.Configuration;

import javax.annotation.PostConstruct;
import java.util.*;
import java.util.stream.Collectors;

import static org.springdoc.core.Constants.SPRINGDOC_ENABLED;

/**
 * @author Tian liyuan
 */
@Configuration
@ConditionalOnProperty(name = SPRINGDOC_ENABLED, matchIfMissing = true)
public class SwaggerConfig {
    @Autowired
    private SwaggerUiConfigProperties swaggerUiConfigProperties;
    @Autowired
    private RouteDefinitionLocator locator;


    @PostConstruct
    public void apis() {
        //获取所有的路径配置
        List<RouteDefinition> definitions = locator.getRouteDefinitions().collectList().block();
        //过滤,只要lb模块式
        List<RouteDefinition> serviceRoutes = definitions.stream().filter(route -> null != route.getUri() && route.getUri().getScheme().equals("lb")).collect(Collectors.toList());
        //按根据PredicateDefinition参数值和/** 确定是否路径匹配
        Set<AbstractSwaggerUiConfigProperties.SwaggerUrl> lbRouteUrl = new HashSet<>();
        Optional.ofNullable(serviceRoutes).orElse(Collections.emptyList()).forEach(route -> {
            AbstractSwaggerUiConfigProperties.SwaggerUrl swaggerUrl = new AbstractSwaggerUiConfigProperties.SwaggerUrl();
            if (route != null && route.getId() != null && !"ReactiveCompositeDiscoveryClient".equals(route.getId().split("_")[0])) {
                //获取路径前缀
                List<PredicateDefinition> predicates = route.getPredicates();
                if (null == predicates || predicates.size() <= 0) {
                    return;
                }
                String prefix = "";
                for (PredicateDefinition predicate : predicates) {
                    String predicateName = predicate.getName();
                    if ("path".equalsIgnoreCase(predicateName)) {
                        for (String regex : predicate.getArgs().values()) {
                            if (regex.endsWith("/**")) {
                                prefix = regex.substring(0, regex.length() - 3);
                                continue;
                            }
                        }

                    }
                }
                //不是路径匹配的路由,跳过
                if (StringUtils.isBlank(prefix)) {
                    return;
                }

                swaggerUrl.setUrl(prefix + Constants.DEFAULT_API_DOCS_URL);
                swaggerUrl.setName(prefix);
                swaggerUrl.setDisplayName(route.getId());
                lbRouteUrl.add(swaggerUrl);
            }
        });

        //添加swaggerUI服务集成匹配
        if (lbRouteUrl.size() > 0) {
            Set<AbstractSwaggerUiConfigProperties.SwaggerUrl> propertiesUrls = swaggerUiConfigProperties.getUrls();
            if (null == propertiesUrls || propertiesUrls.size() <= 0) {
                propertiesUrls = lbRouteUrl;
            } else {
                propertiesUrls.addAll(lbRouteUrl);
            }
            swaggerUiConfigProperties.setUrls(propertiesUrls);
        }
    }
}

简化版(只发现活跃的服务,无需下方yaml中配置)

package com.tly.gateway.config;

import org.springdoc.core.AbstractSwaggerUiConfigProperties;
import org.springdoc.core.Constants;
import org.springdoc.core.SwaggerUiConfigProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.context.annotation.Configuration;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.Collections;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;

import static org.springdoc.core.Constants.SPRINGDOC_ENABLED;

/**
 * @author Tian liyuan
 */
@Configuration
@ConditionalOnProperty(name = SPRINGDOC_ENABLED, matchIfMissing = true)
public class SwaggerConfig {
    @Autowired
    private SwaggerUiConfigProperties swaggerUiConfigProperties;

    @Resource
    private DiscoveryClient discoveryClient;


    @PostConstruct
    public void apis() {
        Set<AbstractSwaggerUiConfigProperties.SwaggerUrl> lbRouteUrl = new HashSet<>();
        Optional.ofNullable(discoveryClient.getServices()).orElse(Collections.emptyList()).forEach(i -> {
            if (!"base-gateway".equals(i)) {
                AbstractSwaggerUiConfigProperties.SwaggerUrl swaggerUrl = new AbstractSwaggerUiConfigProperties.SwaggerUrl();
                swaggerUrl.setUrl("/" + i + Constants.DEFAULT_API_DOCS_URL);
                swaggerUrl.setName("/" + i);
                swaggerUrl.setDisplayName(i);
                lbRouteUrl.add(swaggerUrl);
            }
        });
        //添加swaggerUI服务集成匹配
        if (lbRouteUrl.size() > 0) {
            Set<AbstractSwaggerUiConfigProperties.SwaggerUrl> propertiesUrls = swaggerUiConfigProperties.getUrls();
            if (null == propertiesUrls || propertiesUrls.size() <= 0) {
                propertiesUrls = lbRouteUrl;
            } else {
                propertiesUrls.addAll(lbRouteUrl);
            }
            swaggerUiConfigProperties.setUrls(propertiesUrls);
        }
    }
}
package com.tly.gateway.controller;

import io.swagger.v3.oas.annotations.Operation;
import jakarta.annotation.Resource;
import org.springdoc.core.SwaggerUiConfigParameters;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

/**
 * @author Tian liyuan
 * ip访问swagger ui配置
 * swagger文档聚合配置文件
 */
@RestController
public class SwaggerController {
    @Resource
    private DiscoveryClient discoveryClient;

    //url前缀,一般反向代理会配置前缀为/api
    @Value("${server.servlet.context-path:}")
    private String prefix;

    @Operation(hidden = true)
    @GetMapping("/api/swagger-config.json")
    public Map<String, Object> swaggerConfig() {
        Map<String, Object> config = new LinkedHashMap<>();
        List<SwaggerUiConfigParameters.SwaggerUrl> urls = new LinkedList<>();
        discoveryClient.getServices().forEach(serviceName ->
                discoveryClient.getInstances(serviceName).forEach(serviceInstance -> {
                            if (!"base-gateway".equals(serviceName)) {
                                urls.add(new SwaggerUiConfigParameters.SwaggerUrl(serviceName, prefix + "/" + serviceName + "/v3/api-docs", serviceName));
                            }
//                            else {
//                                urls.add(new SwaggerUiConfigParameters.SwaggerUrl(serviceName, prefix + "/v3/api-docs", serviceName));
//                            }
                        }
                )
        );
        config.put("urls", urls);
        return config;
    }
}

springboot3.x

package com.tly.gateway.config;

import jakarta.annotation.PostConstruct;
import org.springdoc.core.properties.AbstractSwaggerUiConfigProperties;
import org.springdoc.core.properties.SwaggerUiConfigProperties;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionLocator;
import org.springframework.context.annotation.Configuration;

import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import static org.springdoc.core.utils.Constants.SPRINGDOC_ENABLED;

/**
 * @author Tian liyuan
 * 本地访问swagger配置
 */
@Configuration
@ConditionalOnProperty(name = SPRINGDOC_ENABLED, matchIfMissing = true)
public class SwaggerConfig {
    protected final SwaggerUiConfigProperties swaggerUiConfigProperties;
    protected final RouteDefinitionLocator routeDefinitionLocator;

    public SwaggerConfig(SwaggerUiConfigProperties swaggerUiConfigProperties, RouteDefinitionLocator routeDefinitionLocator) {
        this.swaggerUiConfigProperties = swaggerUiConfigProperties;
        this.routeDefinitionLocator = routeDefinitionLocator;
    }

    @PostConstruct
    public void autoInitSwaggerUrls() {
        List<RouteDefinition> definitions = routeDefinitionLocator.getRouteDefinitions().collectList().block();
        if (definitions != null && !definitions.isEmpty()) {
            definitions.forEach(routeDefinition -> {
                AbstractSwaggerUiConfigProperties.SwaggerUrl swaggerUrl = new AbstractSwaggerUiConfigProperties.SwaggerUrl(
                        routeDefinition.getId().replace("ReactiveCompositeDiscoveryClient_", "").toLowerCase(),
                        routeDefinition.getUri().toString().replace("lb://", "").toLowerCase() + "/v3/api-docs",
                        routeDefinition.getId().replace("ReactiveCompositeDiscoveryClient_", "").toLowerCase()
                );
                Set<AbstractSwaggerUiConfigProperties.SwaggerUrl> urls = swaggerUiConfigProperties.getUrls();
                if (urls == null) {
                    urls = new LinkedHashSet<>();
                    swaggerUiConfigProperties.setUrls(urls);
                }
                urls.add(swaggerUrl);
            });
        }
    }
}

bootstrap.yml配置增加(注:配置类读取路由是从网关配置中的来的

spring:
  main:
    lazy-initialization: true
  lifecycle:
    timeout-per-shutdown-phase: 10s
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
      routes:
        - id: base-system
          uri: lb://base-system
          predicates:
            - Path= base-system/**
          filters:
            - StripPrefix=1
        - id: base-test
          uri: lb://base-test
          predicates:
            - Path= base-test/**
          filters:
            - StripPrefix=1
springdoc:
  api-docs:
    path: /v3/api-docs
    enabled: true
    webjars:
      # 设置为空,不要前缀
      prefix:
  swagger-ui:
    path: /swagger-ui/index.html
    persistAuthorization: true

二、出现cros跨域问题

整合配置完之后,发现swagger页面是能出来了,但是每个接口包括放行的接口都爆跨域错误,这是由于网关配置中的路由问题

解决方法:

package com.tly.gateway.filter;

import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.reactivestreams.Publisher;
import org.springdoc.core.SpringDocConfigProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.io.IOException;
import java.io.StringWriter;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static org.springdoc.core.Constants.SPRINGDOC_ENABLED;

/**
 * 处理服务 /v3/api-docs接口返回的数据,在servers里添加可以通过网关直接访问的地址
 */
@Slf4j
@Component
@ConditionalOnProperty(name = SPRINGDOC_ENABLED, matchIfMissing = true)
public class DocServiceUrlModifyGatewayFilter implements GlobalFilter, Ordered {
    @Autowired
    private SpringDocConfigProperties springDocConfigProperties;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //直接用配置类的值,默认值是 /v3/api-docs
        String apiPath = springDocConfigProperties.getApiDocs().getPath();
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();
        URI uri = request.getURI();

        //非正常状态跳过
        if (response.getStatusCode().value() != HttpStatus.OK.value()) {
            return chain.filter(exchange);
        }

        //非springdoc文档不拦截
        if (!(StringUtils.isNotBlank(uri.getPath()) && uri.getPath().endsWith(apiPath))) {
            return chain.filter(exchange);
        }

        String uriString = uri.toString();
        String gatewayUrl = uriString.substring(0, uriString.lastIndexOf(apiPath));
        DataBufferFactory bufferFactory = response.bufferFactory();


        ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(response) {
            @Override
            public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
                //不处理
                if (!(body instanceof Flux)) {
                    return super.writeWith(body);
                }

                //处理SpringDoc-OpenAPI
                Flux<? extends DataBuffer> fluxBody = (Flux<? extends DataBuffer>) body;
                return super.writeWith(fluxBody.buffer().map(dataBuffers -> {
                    DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory();
                    DataBuffer join = dataBufferFactory.join(dataBuffers);
                    byte[] content = new byte[join.readableByteCount()];
                    join.read(content);
                    DataBufferUtils.release(join);
                    try {
                        // 流转为字符串
                        String responseData = new String(content, StandardCharsets.UTF_8);
                        Map<String, Object> map = JsonUtils.json2Object(responseData, Map.class);
                        //处理返回的数据
                        Object serversObject = map.get("servers");
                        if (null != serversObject) {
                            List<Map<String, Object>> servers = (List<Map<String, Object>>) serversObject;
                            Map<String, Object> gatewayServer = new HashMap<>();
                            servers.clear();
                            //网关地址
                            gatewayServer.put("url", gatewayUrl);
                            gatewayServer.put("description", "Gateway server url");
                            //添加到第1个
                            servers.add(0, gatewayServer);
                            map.put("servers", servers);
                            responseData = JsonUtils.object2Json(map);
                            byte[] uppedContent = responseData.getBytes(StandardCharsets.UTF_8);
                            response.getHeaders().setContentLength(uppedContent.length);
                            return bufferFactory.wrap(uppedContent);
                        }
                    } catch (Exception e) {
                        log.error(e.getMessage(), e);
                    }

                    return bufferFactory.wrap(content);
                }));
            }
        };
        // replace response with decorator
        return chain.filter(exchange.mutate().response(decoratedResponse).build());
    }

    @Override
    public int getOrder() {
        return -2;
    }


    class JsonUtils {

        public static final ObjectMapper OBJECT_MAPPER = createObjectMapper();

        /**
         * 初始化ObjectMapper
         *
         * @return
         */
        private static ObjectMapper createObjectMapper() {

            ObjectMapper objectMapper = new ObjectMapper();

            objectMapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);
            objectMapper.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true);
            objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
//        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);

            objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true);

//        objectMapper.registerModule(new Hibernate4Module().enable(Hibernate4Module.Feature.FORCE_LAZY_LOADING));
            objectMapper.registerModule(new JavaTimeModule());

            return objectMapper;
        }

        public static String object2Json(Object o) {
            StringWriter sw = new StringWriter();
            JsonGenerator gen = null;
            try {
                gen = new JsonFactory().createGenerator(sw);
                OBJECT_MAPPER.writeValue(gen, o);
            } catch (IOException e) {
                throw new RuntimeException("不能序列化对象为Json", e);
            } finally {
                if (null != gen) {
                    try {
                        gen.close();
                    } catch (IOException e) {
                        throw new RuntimeException("不能序列化对象为Json", e);
                    }
                }
            }
            return sw.toString();
        }


        /**
         * 将 json 字段串转换为 对象.
         *
         * @param json  字符串
         * @param clazz 需要转换为的类
         * @return
         */
        public static <T> T json2Object(String json, Class<T> clazz) {
            try {
                return OBJECT_MAPPER.readValue(json, clazz);
            } catch (IOException e) {
                throw new RuntimeException("将 Json 转换为对象时异常,数据是:" + json, e);
            }
        }


    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值