gateway整合swagger3.0+knife4j增强(完整版)

之前写的整合文章还有些缺陷,本此全部处理。
参考资料:

官方文档地址:knife4j (xiaominfo.com) (谷歌打不开就用ie)
github项目:microservices-platform-master
地址:https://github.com/Aisii/microservices-platform-master
博客:https://blog.csdn.net/qq_39878940/article/details/123181951
博客:https://www.jianshu.com/p/aef7d953ae70

思路:

  1. 将swagger模块抽出为一个公共模块,或者是自定义springboot starter模块(本文采用后者),供其他模块快速整合swagger。
  2. gateway服务配置所有服务swagger文档聚合

实现:

step1:自定义 swagger-springboot-starter
1.1 引入依赖


        

        
        
        
            com.github.xiaoymin
            knife4j-spring-boot-starter
            3.0.3
        
        

        
        
            org.springframework.boot
            spring-boot-starter
        

        
        
            org.springframework.boot
            spring-boot-configuration-processor
            true
        

        
            org.springframework
            spring-web
            true
        

    

1.2 swagger 属性配置实体类

/**
 * swagger2 属性配置
 *
 * @author zlt Not registered via @EnableConfigurationProperties, marked as Spring component, or scanned via @ConfigurationPropertiesScan
 * @date 2018/11/18 9:17
 */
@Data
@ConfigurationProperties("swagger")
public class SwaggerProperties {
    /**
     * 是否开启swagger
     **/
    private Boolean enabled;
    /**
     * 标题
     **/
    private String title = "";
    /**
     * 描述
     **/
    private String description = "";
    /**
     * 版本
     **/
    private String version = "";
    /**
     * 许可证
     **/
    private String license = "";
    /**
     * 许可证URL
     **/
    private String licenseUrl = "";
    /**
     * 服务条款URL
     **/
    private String termsOfServiceUrl = "";
    /**
     * 联系人
     **/
    private Contact contact = new Contact();

    /**
     * swagger会解析的包路径
     **/
    private String basePackage = "";

    /**
     * swagger会解析的url规则
     **/
    private List basePath = new ArrayList<>();
    /**
     * 在basePath基础上需要排除的url规则
     **/
    private List excludePath = new ArrayList<>();

    /**
     * 分组文档
     **/
    private Map docket = new LinkedHashMap<>();

    /**
     * host信息
     **/
    private String host = "";

    /**
     * 全局参数配置
     **/
    private List globalOperationParameters;

    @Setter
    @Getter
    public static class GlobalOperationParameter {
        /**
         * 参数名
         **/
        private String name;

        /**
         * 描述信息
         **/
        private String description;

        /**
         * 指定参数类型 ScalarType.STRING
         **/
        private String modelRef;

        /**
         * 参数放在哪个地方
         * QUERY("query"),
         * HEADER("header"),
         * PATH("path"),
         * COOKIE("cookie"),
         * FORM("form"),
         * FORMDATA("formData"),
         * BODY("body");
         **/
        private String parameterType;

        /**
         * 参数是否必须传
         **/
        private String required;
    }

    @Data
    public static class DocketInfo {
        /**
         * 标题
         **/
        private String title = "";
        /**
         * 描述
         **/
        private String description = "";
        /**
         * 版本
         **/
        private String version = "";
        /**
         * 许可证
         **/
        private String license = "";
        /**
         * 许可证URL
         **/
        private String licenseUrl = "";
        /**
         * 服务条款URL
         **/
        private String termsOfServiceUrl = "";

        private Contact contact = new Contact();

        /**
         * swagger会解析的包路径
         **/
        private String basePackage = "";

        /**
         * swagger会解析的url规则
         **/
        private List basePath = new ArrayList<>();
        /**
         * 在basePath基础上需要排除的url规则
         **/
        private List excludePath = new ArrayList<>();

        private List globalOperationParameters;
    }

    @Data
    public static class Contact {
        /**
         * 联系人
         */
        private String name = "";
        /**
         * 联系人url
         */
        private String url = "";
        /**
         * 联系人email
         */
        private String email = "";
    }
}

1.3 构建文档配置

package com.ly.tulip.commons.swagger.config;

import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j;
import com.google.common.collect.Lists;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import springfox.bean.validators.configuration.BeanValidatorPluginsConfiguration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.builders.RequestParameterBuilder;
import springfox.documentation.oas.annotations.EnableOpenApi;
import springfox.documentation.schema.ScalarType;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.ApiKey;
import springfox.documentation.service.AuthorizationScope;
import springfox.documentation.service.Contact;
import springfox.documentation.service.RequestParameter;
import springfox.documentation.service.SecurityReference;
import springfox.documentation.service.SecurityScheme;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.contexts.SecurityContext;
import springfox.documentation.spring.web.plugins.Docket;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;

/**
 * 类说明: swagger 配置类
 *
 * @author wqf
 * @date 2022/7/26 10:23
 */
@Configuration
@EnableOpenApi     //开启 Swagger3,可以不写
@EnableKnife4j     //开启 knife4j,可以不写
@Import(BeanValidatorPluginsConfiguration.class)
public class SwaggerAutoConfiguration implements BeanFactoryAware {

    private static final String AUTH_KEY = "Authorization";

    private BeanFactory beanFactory;

    @Bean
    @ConditionalOnMissingBean
    public SwaggerProperties swaggerProperties() {
        return new SwaggerProperties();
    }

    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnProperty(name = "tulip.swagger.enabled", matchIfMissing = true)
    public List createRestApi(SwaggerProperties swaggerProperties) {
        ConfigurableBeanFactory configurableBeanFactory = (ConfigurableBeanFactory) beanFactory;
        List docketList = new LinkedList<>();

        // 没有分组
        if (swaggerProperties.getDocket().size() == 0) {
            final Docket docket = createDocket(swaggerProperties);
            configurableBeanFactory.registerSingleton("defaultDocket", docket);
            docketList.add(docket);
            return docketList;
        }

        // 分组创建
        for (String groupName : swaggerProperties.getDocket().keySet()) {
            SwaggerProperties.DocketInfo docketInfo = swaggerProperties.getDocket().get(groupName);

            ApiInfo apiInfo = new ApiInfoBuilder()
                    .title(docketInfo.getTitle().isEmpty() ? swaggerProperties.getTitle() : docketInfo.getTitle())
                    .description(docketInfo.getDescription().isEmpty() ? swaggerProperties.getDescription() : docketInfo.getDescription())
                    .version(docketInfo.getVersion().isEmpty() ? swaggerProperties.getVersion() : docketInfo.getVersion())
                    .license(docketInfo.getLicense().isEmpty() ? swaggerProperties.getLicense() : docketInfo.getLicense())
                    .licenseUrl(docketInfo.getLicenseUrl().isEmpty() ? swaggerProperties.getLicenseUrl() : docketInfo.getLicenseUrl())
                    .contact(
                            new Contact(
                                    docketInfo.getContact().getName().isEmpty() ? swaggerProperties.getContact().getName() : docketInfo.getContact().getName(),
                                    docketInfo.getContact().getUrl().isEmpty() ? swaggerProperties.getContact().getUrl() : docketInfo.getContact().getUrl(),
                                    docketInfo.getContact().getEmail().isEmpty() ? swaggerProperties.getContact().getEmail() : docketInfo.getContact().getEmail()
                            )
                    )
                    .termsOfServiceUrl(docketInfo.getTermsOfServiceUrl().isEmpty() ? swaggerProperties.getTermsOfServiceUrl() : docketInfo.getTermsOfServiceUrl())
                    .build();


            Docket docket = new Docket(DocumentationType.OAS_30)
                    .host(swaggerProperties.getHost())
                    .apiInfo(apiInfo)
                    .globalRequestParameters(assemblyGlobalOperationParameters(swaggerProperties.getGlobalOperationParameters(),
                            docketInfo.getGlobalOperationParameters()))
                    .groupName(groupName)
                    .select()
                    .apis(RequestHandlerSelectors.basePackage(docketInfo.getBasePackage()))
                    .paths(buildPredicateSelector(docketInfo))
                    .build()
                    .securitySchemes(securitySchemes())
                    .securityContexts(securityContexts());

            configurableBeanFactory.registerSingleton(groupName, docket);
            docketList.add(docket);
        }
        return docketList;
    }

    /**
     * 创建 Docket对象
     *
     * @param swaggerProperties swagger配置
     * @return Docket
     */
    private Docket createDocket(final SwaggerProperties swaggerProperties) {
        ApiInfo apiInfo = new ApiInfoBuilder()
                .title(swaggerProperties.getTitle())
                .description(swaggerProperties.getDescription())
                .version(swaggerProperties.getVersion())
                .license(swaggerProperties.getLicense())
                .licenseUrl(swaggerProperties.getLicenseUrl())
                .contact(new Contact(swaggerProperties.getContact().getName(),
                        swaggerProperties.getContact().getUrl(),
                        swaggerProperties.getContact().getEmail()))
                .termsOfServiceUrl(swaggerProperties.getTermsOfServiceUrl())
                .build();

        SwaggerProperties.DocketInfo dockerInfo = new SwaggerProperties.DocketInfo();
        dockerInfo.setBasePackage(swaggerProperties.getBasePackage());
        dockerInfo.setExcludePath(swaggerProperties.getExcludePath());

        return new Docket(DocumentationType.OAS_30)
                .host(swaggerProperties.getHost())
                .apiInfo(apiInfo)
                .enable(swaggerProperties.isEnabled())
                .globalRequestParameters(buildGlobalOperationParametersFromSwaggerProperties(
                        swaggerProperties.getGlobalOperationParameters()))
                .select()
                .apis(RequestHandlerSelectors.basePackage(swaggerProperties.getBasePackage()))
                .paths(buildPredicateSelector(dockerInfo))
                .build()
                .securitySchemes(securitySchemes())
                .securityContexts(securityContexts());
    }

    private List securityContexts() {
        List contexts = new ArrayList<>(1);
        SecurityContext securityContext = SecurityContext.builder()
                .securityReferences(defaultAuth())
                .build();
        contexts.add(securityContext);
        return contexts;
    }

    private List defaultAuth() {
        AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");
        AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
        authorizationScopes[0] = authorizationScope;
        List references = new ArrayList<>(1);
        references.add(new SecurityReference(AUTH_KEY, authorizationScopes));
        return references;
    }

    private List securitySchemes() {
        List apiKeys = new ArrayList<>(1);
        ApiKey apiKey = new ApiKey(AUTH_KEY, AUTH_KEY, "header");
        apiKeys.add(apiKey);
        return apiKeys;
    }

    /**
     * 生成swagger全局通用参数 如:请求token
     */
    private List buildGlobalOperationParametersFromSwaggerProperties(
            List globalOperationParameters) {

        List parameters = Lists.newArrayList();
        if (Objects.isNull(globalOperationParameters)) {
            return parameters;
        }
        for (SwaggerProperties.GlobalOperationParameter globalOperationParameter : globalOperationParameters) {
            parameters.add(new RequestParameterBuilder()
                    //参数名称
                    .name(globalOperationParameter.getName())
                    //参数说明
                    .description(globalOperationParameter.getDescription())
                    //参数存放位置
                    .in(globalOperationParameter.getParameterType())
                    //参数数据类型 暂时写死为string 有其他需求再改
                    .query(q -> q.model(m -> m.scalarModel(ScalarType.STRING)))
                    //参数是否必须
                    .required(Boolean.parseBoolean(globalOperationParameter.getRequired()))
                    .build());
        }
        return parameters;
    }


    /**
     * 局部参数按照name覆盖局部参数
     *
     * @param globalOperationParameters
     * @param docketOperationParameters
     * @return
     */
    private List assemblyGlobalOperationParameters(
            List globalOperationParameters,
            List docketOperationParameters) {

        if (Objects.isNull(docketOperationParameters) || docketOperationParameters.isEmpty()) {
            return buildGlobalOperationParametersFromSwaggerProperties(globalOperationParameters);
        }

        Set docketNames = docketOperationParameters.stream()
                .map(SwaggerProperties.GlobalOperationParameter::getName)
                .collect(Collectors.toSet());

        List resultOperationParameters = Lists.newArrayList();

        if (Objects.nonNull(globalOperationParameters)) {
            for (SwaggerProperties.GlobalOperationParameter parameter : globalOperationParameters) {
                if (!docketNames.contains(parameter.getName())) {
                    resultOperationParameters.add(parameter);
                }
            }
        }

        resultOperationParameters.addAll(docketOperationParameters);
        return buildGlobalOperationParametersFromSwaggerProperties(resultOperationParameters);
    }

    private java.util.function.Predicate buildPredicateSelector(SwaggerProperties.DocketInfo config) {
        // 当没有配置任何path的时候,解析/**
        if (config.getBasePath().isEmpty()) {
            config.getBasePath().add("/**");
        }
        List> basePath = new ArrayList<>();
        for (String path : config.getBasePath()) {
            basePath.add(PathSelectors.ant(path));
        }

        // exclude-path处理
        List> excludePath = new ArrayList<>();
        for (String path : config.getExcludePath()) {
            excludePath.add(PathSelectors.ant(path));
        }

        // 当没有配置任何path的时候,解析/.*
        if (config.getBasePath().isEmpty() || config.getBasePath() == null) {
            return PathSelectors.any().and(excludePath.stream().reduce(each -> true, (a, b) -> a.and(b.negate())));
        }

        //组装 base-path 和 exclude-path each为false原因是,如果是true,有任何or,都不会走右边
        return basePath.stream().reduce(each -> false, Predicate::or)
                .and(excludePath.stream().reduce(each -> true, (a, b) -> a.and(b.negate())));

    }


    @Override
    public void setBeanFactory(BeanFactory beanFactory) {
        this.beanFactory = beanFactory;
    }
}

1.3 springboot注定装配配置
resources目录下先建文件夹 META-INF
文件夹下创建 spring.factories 文件 ,内容如下

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.ly.tulip.commons.swagger.config.SwaggerAutoConfiguration

step1 做完,其他服务引入该公共模块即整合了swagger3.0
1.3,1.4两个文件可以根据需求搞一个简单的配置,这个配置网上都有,官方文档也有,我也是参考其他项目的,仅当学习

step2:gateway聚合配置
2.1.引入step1 的模块
2.2. 引入两个核心配置文件

SwaggerProvider

import lombok.AllArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
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;

/**
 * 类说明: 聚合系统接口
 *
 * @author wqf
 * @date 2022/7/26 16:47
 */
@Component
@Primary
@AllArgsConstructor
public class SwaggerProvider implements SwaggerResourcesProvider {

    // SWAGGER3默认的URL后缀
    public static final String SWAGGER3URL = "/v3/api-docs";
    public static final String SWAGGER_VERSION = "3.0";

    // 网关路由
    @Autowired
    private RouteLocator routeLocator;

    @Autowired
    private GatewayProperties gatewayProperties;

    // 聚合其他服务接口
    @Override
    public List get() {
        List resourceList = new ArrayList<>();
        List routes = new ArrayList<>();
        // 获取网关中配置的route
        routeLocator.getRoutes().subscribe(route -> routes.add(route.getId()));

        gatewayProperties.getRoutes().stream().filter(routeDefinition -> routes.contains(routeDefinition.getId()))
                .forEach(routeDefinition -> routeDefinition.getPredicates().stream()
                        .filter(predicateDefinition -> ("Path").equalsIgnoreCase(predicateDefinition.getName()))
                        .forEach(predicateDefinition -> {
                                    String id = routeDefinition.getId();
                                    String location = id + SWAGGER3URL;
                                    resourceList.add(swaggerResource(id, location));

                                }
                        ));
        return resourceList;
    }

    private SwaggerResource swaggerResource(String name, String location) {
        SwaggerResource swaggerResource = new SwaggerResource();
        swaggerResource.setName(name);
        swaggerResource.setLocation(location);
        swaggerResource.setSwaggerVersion(SWAGGER_VERSION);
        return swaggerResource;
    }
}

SwaggerHandler


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.SecurityConfiguration;
import springfox.documentation.swagger.web.SecurityConfigurationBuilder;
import springfox.documentation.swagger.web.SwaggerResourcesProvider;
import springfox.documentation.swagger.web.UiConfiguration;
import springfox.documentation.swagger.web.UiConfigurationBuilder;

import java.util.Optional;

@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));
    }

    @SuppressWarnings("rawtypes")
    @GetMapping("")
    public Mono swaggerResources() {
        return Mono.just((new ResponseEntity<>(swaggerResources.get(), HttpStatus.OK)));
    }
}

2.3 yml文件配置转发路由

spring:
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
  #配置网关路由规则 按服务名进行路由
    gateway:
      routes:
        # 唯一标识 - 系统核心服务
        - id: base-server
          uri: lb://base-server
          #断言,路径相匹配的进行路由,配置服务端的方法路
          predicates:
            - Path=/base-server/**
          filters:
            - StripPrefix=0
        # 唯一标识 - 系统用户服务
        - id: auth-server
          uri: lb://auth-server
          predicates:
            - Path=/auth-server/**
          filters:
            - StripPrefix=0

以上基本配置完成,但是测试时发现,接口调试时请求地址缺少转发路由地址,解决配置如下


import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.reactivestreams.Publisher;
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.DataBufferUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
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.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

/**
 * 类说明: 处理gateway集成knife4j后接口请求没带上服务名称问题
 *
 * @author wqf
 * @date 2022/8/11 10:17
 */
@Slf4j
@Component
public class SwaggerGlobalFilter implements GlobalFilter, Ordered {
    @Override
    public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String path=exchange.getRequest().getPath().toString();
        if (!path.endsWith("/v3/api-docs")){
            return chain.filter(exchange);
        }
        String[] pathArray=path.split("/");
        String basePath=pathArray[1];
        ServerHttpResponse originalResponse = exchange.getResponse();
        // 定义新的消息头
        HttpHeaders headers = new HttpHeaders();
        headers.putAll(exchange.getResponse().getHeaders());
        ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(originalResponse) {
            @Override
            public Mono writeWith(Publisher body) {
                if (Objects.equals(getStatusCode(), HttpStatus.OK) && body instanceof Flux) {
                    Flux fluxBody = Flux.from(body);
                    return super.writeWith(fluxBody.buffer().map(dataBuffers -> {
                        List list = new ArrayList();
                        dataBuffers.forEach(dataBuffer -> {
                            byte[] content = new byte[dataBuffer.readableByteCount()];
                            dataBuffer.read(content);
                            DataBufferUtils.release(dataBuffer);
                            list.add(new String(content, StandardCharsets.UTF_8));
                        });
                        String s = listToString(list);
                        int length = s.getBytes().length;
                        headers.setContentLength(length);
                        JSONObject jsonObject= JSONUtil.parseObj(s);
                        jsonObject.set("basePath",basePath);
                        s=jsonObject.toString();
                        return bufferFactory().wrap(s.getBytes());
                    }));
                }
                return super.writeWith(body);
            };
            @Override
            public HttpHeaders getHeaders() {
                HttpHeaders httpHeaders = new HttpHeaders();
                httpHeaders.putAll(super.getHeaders());
                //由于修改了请求体的body,导致content-length长度不确定,因此使用分块编码
                httpHeaders.remove(HttpHeaders.CONTENT_LENGTH);
                httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
                return httpHeaders;
            }

            private String listToString(List list){
                StringBuilder stringBuilder=new StringBuilder();
                for (String s:list){
                    stringBuilder.append(s);
                }
                return stringBuilder.toString();
            }
        };
        return chain.filter(exchange.mutate().response(decoratedResponse).build());
    }

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

注意点:

  1. 网关对:/swagger-ui/**; /v3/api-docs 等swagger访问路径记得放行
  2. 网关配置文件需要配置路由规则,以下配置是按服务名称进行路由转发的,好处是服务端口改了也没事。

以上整合完毕,两个访问地址如下:

swagger 访问地址:
swagger原生版:   http://ip:端口号/swagger-ui/index.html
knife4j界面美化版:http://ip:端口号/doc.html#/home

其他问题:
1.开启配置增强,一般生产环境不需要这个文档的访问
2.设置Ui默认显示语言为中文
3.开启请求参数缓存
配置如下:

knife4j:
  #生产环境禁止访问文档
  production: true
  enable: true
  setting:
    #设置Ui默认显示语言为中文
    language: zh-CN
    #开启请求参数缓存
    enableRequestCache: true

以上基本够用了,有其他的需求再看看官方文档吧。

你可能感兴趣的:(gateway整合swagger3.0+knife4j增强(完整版))