Spring Cloud:Gateway整合Springfox Swagger 3.0

前言

2020年7月份Springfox 发布了3.0.0,增加了对Webflux、OpenApi 3的支持,适应Gateway。在微服务系统中,每个业务模块使用swagger管理接口文档,同时,可以使用业务网关聚合管理各微服务的接口文档,本文重点说明Spring Cloud Gateway集成Springfox swagger 3.0,以供参考。

框架版本

  1. Spring Boot 2.4.4

  2. Spring Cloud 2020.0.2

  3. Spring Cloud Alibaba 2021.1 (注册中心和配置中心使用Nacos 2021.1)

  4. Spring Cloud Gateway 3.0.2

  5. Springfox 3.0.0

注意:这里使用的版本比较新,特别是Spring Cloud 2020和Springfox 3.0.0,这两个都是大版本升级,使用方式和以前存在差异,网上很多教程都是基于以前的版本。

开始整合

微服务端

网关聚合管理是基于微服务的接口实现的,所以在启动网关之前,必须先配置各微服务的接口和swagger。

关于Spring Boot如何整合SpirngFox Swagger 3.0,可以查看另一篇文章:Spring Boot:整合Swagger3.0,这里不做拓展。

引入依赖

<!-- 只需要引入一个依赖即可,2.x版本需要引入两个 -->
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-boot-starter</artifactId>
    <version>${springfox-boot-starter.version}</version>
</dependency>

添加配置

Springfox 3.0移除了@EnableSwagger2WebMvc和@EnableSwagger2WebFlux等注解,统一使用@EnableOpenApi

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({OpenApiDocumentationConfiguration.class})
public @interface EnableOpenApi {
}

实现统一配置文件,下面做了自定义配置的封装,重点是SwaggerAutoConfiguration#api方法:

@Data
@ConfigurationProperties("swagger")
public class SwaggerProperties {

    /**
     * 是否开启swagger
     */
    private Boolean enabled;

    /**
     * 分组名
     **/
    private String groupName = "default";

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

    /**
     * swagger会解析的url规则
     **/
    private List<String> basePath = new ArrayList<>();

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

    /**
     * 标题
     **/
    private String title = "";

    /**
     * 描述
     **/
    private String description = "";

    /**
     * 版本
     **/
    private String version = "";

    /**
     * 许可证
     **/
    private String license = "";

    /**
     * 许可证URL
     **/
    private String licenseUrl = "";

    /**
     * 服务条款URL
     **/
    private String termsOfServiceUrl = "";

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

    /**
     * 联系人信息
     */
    private Contact contact = new Contact();

    @Data
    @NoArgsConstructor
    public static class Contact {

        /**
         * 联系人
         **/
        private String name = "";

        /**
         * 联系人url
         **/
        private String url = "";

        /**
         * 联系人email
         **/
        private String email = "";

    }
}
@EnableOpenApi
@Configuration
@ConditionalOnProperty(name = "swagger.enabled", matchIfMissing = true)
public class SwaggerAutoConfiguration {

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

    /**
     * 配置swagger2的一些基本的内容,比如扫描的包等等
     *
     * @return Docket
     */
    @Bean
    public Docket api(SwaggerProperties swaggerProperties) {

        // base-package处理
        if (swaggerProperties.getBasePackage().isEmpty()) {
            swaggerProperties.setBasePackage(SwaggerConstants.BASE_PACKAGE);
        }

        // base-path处理
        if (swaggerProperties.getBasePath().isEmpty()) {
            swaggerProperties.getBasePath().add(SwaggerConstants.DEFAULT_BASE_PATH);
        }
        // noinspection unchecked
        List<Predicate<String>> basePath = new ArrayList<>();
        swaggerProperties.getBasePath().forEach(path -> basePath.add(PathSelectors.ant(path)));

        // exclude-path处理
        if (swaggerProperties.getExcludePath().isEmpty()) {
            swaggerProperties.getExcludePath().addAll(SwaggerConstants.DEFAULT_EXCLUDE_PATH);
        }
        List<Predicate<String>> excludePath = new ArrayList<>();
        swaggerProperties.getExcludePath().forEach(path -> excludePath.add(PathSelectors.ant(path)));


        ApiSelectorBuilder builder = new Docket(DocumentationType.SWAGGER_2)
                .enable(swaggerProperties.getEnabled())
                .host(swaggerProperties.getHost())
                .apiInfo(apiInfo(swaggerProperties))
                .select()
                //此包路径下的类,才生成接口文档
                .apis(RequestHandlerSelectors.basePackage(swaggerProperties.getBasePackage()))
                //加了Api注解的类,才生成接口文档
                .apis(RequestHandlerSelectors.withClassAnnotation(Api.class));

        swaggerProperties.getBasePath().forEach(p -> builder.paths(PathSelectors.ant(p)));
        swaggerProperties.getExcludePath().forEach(p -> builder.paths(PathSelectors.ant(p).negate()));

        return builder
                .build()
                .securitySchemes(securitySchemes())
                .securityContexts(securityContexts())
                .pathMapping("/");
    }


    /**
     * 安全模式,这里指定token通过Authorization头请求头传递
     */
    private List<SecurityScheme> securitySchemes() {
        SecurityScheme scheme = new ApiKey(SwaggerConstants.ACCESS_TOKEN_KEY, SwaggerConstants.ACCESS_TOKEN_KEY, "header");
        return Collections.singletonList(scheme);
    }

    /**
     * 配置默认的全局鉴权策略的开关,通过正则表达式进行匹配;默认匹配所有URL
     */
    private List<SecurityContext> securityContexts() {
        SecurityContext context = SecurityContext.builder()
                .securityReferences(defaultAuth())
                .forPaths(PathSelectors.regex("^(?!auth).*$"))
                .build();
        return Collections.singletonList(context);
    }

    /**
     * 默认的全局鉴权策略
     *
     * @return
     */
    private List<SecurityReference> defaultAuth() {
        AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");
        AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
        authorizationScopes[0] = authorizationScope;
        return Collections.singletonList(
                new SecurityReference(SwaggerConstants.ACCESS_TOKEN_KEY, authorizationScopes));
    }

    /**
     * api文档的详细信息函数,注意这里的注解引用的是哪个
     *
     * @return
     */
    private ApiInfo apiInfo(SwaggerProperties swaggerProperties) {
        return new ApiInfoBuilder()
                .title(swaggerProperties.getTitle())
                .description(swaggerProperties.getDescription())
                .license(swaggerProperties.getLicense())
                .licenseUrl(swaggerProperties.getLicenseUrl())
                .termsOfServiceUrl(swaggerProperties.getTermsOfServiceUrl())
                .contact(new Contact(swaggerProperties.getContact().getName(), swaggerProperties.getContact().getUrl(), swaggerProperties.getContact().getEmail()))
                .version(swaggerProperties.getVersion())
                .build();
    }
}

然后只需要在启动程序添加@EnableOpenApi即可,访问地址:http://ip:port/swagger-ui/index.html

网关端

引入依赖

        <!--gateway 网关依赖,内置webflux 依赖-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
		
		<dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-boot-starter</artifactId>
            <version>${springfox-boot-starter.version}</version>
		</dependency>

... 其他依赖,如Nacos注册中心

添加gateway路由配置

spring:
  application:
    # 应用名称
    name: spring2go-gateway
  profiles:
    # 环境配置
    active: dev
  cloud:
    #nacos
    nacos:
      discovery:
        # 服务注册地址
        server-addr: 127.0.0.1:8848
      config:
        # 配置中心地址
        server-addr: ${spring.cloud.nacos.discovery.server-addr}
        # 配置文件格式
        file-extension: yml
    gateway:
      enabled: true
      # @link:https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#the-loadbalancerclient-filter
      # 当service实例不存在时默认返回503,显示配置返回404
      loadbalancer:
        use404: true
      # 根据注册中心自动配置routes
      # @link :https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#the-discoveryclient-route-definition-locator
      # 效果:/serviceId/path1/args ==> /path1/args
#      discovery:
#        locator:
#          lowerCaseServiceId: true
#          enabled: true
      routes:
        # 系统模块
        - id: spring2go-upms
          uri: lb://spring2go-upms
          predicates:
            - Path=/upms/**
          filters:
            # - SwaggerHeaderFilter
            - StripPrefix=1

由于我们需要聚合管理各微服务的接口,所以我们可以通过自定义SwaggerProvider获取Api-doc(即SwaggerResources)来获取各服务的接口。

注意:Springfox 3.0开始支持webflux,所以想管理网关自身的接口,只需要在官网启动程序添加@EnableOpenApi即可。

//标记primary,让网关默认使用自定义provider
@Primary
@Component
public class SwaggerProvider implements SwaggerResourcesProvider {

    /**
     * Swagger2默认的url后缀
     */
    public static final String SWAGGER2URL = "/v2/api-docs";
    /**
     * 网关路由
     */
    @Autowired
    private RouteLocator routeLocator;

    @Autowired
    private GatewayProperties gatewayProperties;

    /**
     * 聚合其他服务接口
     *
     * @return
     */
    @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(NameUtils.GENERATED_NAME_PREFIX + "0")
                                        .replace("/**", SWAGGER2URL)))));
        return resources;
    }

    private SwaggerResource swaggerResource(String name, String url) {
        SwaggerResource swaggerResource = new SwaggerResource();
        swaggerResource.setName(name);
        swaggerResource.setLocation(url);
        swaggerResource.setSwaggerVersion("2.0");
        return swaggerResource;
    }
}

注意:这里有个坑,swagger的文档类型必须使用SWAGGER_2(即:/v2/api-docs),后面会讲解原因。

然后启动微服务,启动网关服务,访问地址:http://ip:port/swagger-ui/index.html即可看到聚合的接口。

常见问题

设置X-Forwarded-Prefix

如果网关路由为upms/test/{a},在swagger会显示为test/{a,缺少了upms这个路由节点。断点源码时发现在Swagger中会根据X-Forwarded-Prefix这个Header来获取BasePath,将它添加至接口路径与host中间,这样才能正常做接口测试,而Gateway在做转发的时候并没有这个Header添加进Request,所以发生接口调试的404错误。解决思路是在Gateway里加一个过滤器来添加这个header。

注意:Spring Boot2.0.6版本Spring将给我们添加上了这个Header,不再需要人为添加,如果重复添加将导致baseurl出现两个路由节点名称

@Component
public class SwaggerHeaderFilter extends AbstractGatewayFilterFactory {
    private static final String HEADER_NAME = "X-Forwarded-Prefix";
    private static final String SWAGGER_URI = "/v2/api-docs";

    @Override
    public GatewayFilter apply(Object config) {
        return (exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();
            String path = request.getURI().getPath();
            if (!StringUtils.endsWithIgnoreCase(path, SWAGGER_URI)) {
                return chain.filter(exchange);
            }
            String basePath = path.substring(0, path.lastIndexOf(SWAGGER_URI));
            ServerHttpRequest newRequest = request.mutate().header(HEADER_NAME, basePath).build();
            ServerWebExchange newExchange = exchange.mutate().request(newRequest).build();
            return chain.filter(newExchange);
        };
    }
}

Swagger调试返回404

Springfox升级3.0以后,提供了新的文档类型,DocumentationType.OAS_30,如果使用OAS_30,将指导BaseURL默认不加上节点名称,例如:[ Base URL: localhost:9000/upms ] ==> [ Base URL: localhost:9000 ],在这种情况下,如果网关路由使用了StripPrefix过滤器,将导致路由命中失败,我认为这是springfox的一个bug。

参考资料

 
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值