spring gateway webflux配置knife4j详解

2,149 阅读1分钟

1. 添加pom依赖

1.1. 根pom下

添加统一的版本管理

<properties>
   <knife4j.version>3.0.3</knife4j.version>
</properties>

<dependencyManagement>
   <dependencies>
      <dependency>
         <groupId>com.github.xiaoymin</groupId>
         <artifactId>knife4j-micro-spring-boot-starter</artifactId>
         <version>${knife4j.version}</version>
      </dependency>
      <dependency>
         <groupId>com.github.xiaoymin</groupId>
         <artifactId>knife4j-spring-boot-starter</artifactId>
         <version>${knife4j.version}</version>
      </dependency>
   </dependencies>
</dependencyManagement>

1.2. gateway的pom

<dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>knife4j-spring-boot-starter</artifactId>
</dependency>

1.3. 其他服务的pom

<dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>knife4j-micro-spring-boot-starter</artifactId>
</dependency>

与gateway的区别就在于这个包少了ui依赖knife4j-spring-ui

2. 配置gateway服务

2.1. 配置swagger核心配置 docket

package com.holland.gateway.swagger;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc;

@Configuration
@EnableSwagger2WebMvc
public class Knife4jConfiguration {

    @Value("${spring.application.name}")
    private String name;

    @Bean
    public Docket defaultApi2() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(groupApiInfo(name))
                //分组名称  想要网关被记录到swagger就不要开分组
//                .groupName("2.X版本")
                .select()
                //这里指定Controller扫描包路径
                .apis(RequestHandlerSelectors.basePackage("com.holland." + name + ".controller"))
                .paths(PathSelectors.any())
                .build();
    }

    private ApiInfo groupApiInfo(String name) {
        return new ApiInfoBuilder()
                .title("后端接口文档-" + name)
                .description("<div style='font-size:14px;color:red;'>description</div>")
                .termsOfServiceUrl("N")
                .contact(new Contact("HollanZang", "https://juejin.cn/user/352263461681214", "zhn.pop@gmail.com"))
                .version("1.0")
                .build();
    }
}

2.2. 配置swagger接口

package com.holland.gateway.swagger;

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.RestController;
import reactor.core.publisher.Mono;
import springfox.documentation.swagger.web.*;

import java.util.Optional;


@RestController
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("/swagger-resources/configuration/security")
    public Mono<ResponseEntity<SecurityConfiguration>> securityConfiguration() {
        return Mono.just(new ResponseEntity<>(
                Optional.ofNullable(securityConfiguration).orElse(SecurityConfigurationBuilder.builder().build()), HttpStatus.OK));
    }

    @GetMapping("/swagger-resources/configuration/ui")
    public Mono<ResponseEntity<UiConfiguration>> uiConfiguration() {
        return Mono.just(new ResponseEntity<>(
                Optional.ofNullable(uiConfiguration).orElse(UiConfigurationBuilder.builder().build()), HttpStatus.OK));
    }

    @GetMapping("/swagger-resources")
    public Mono<ResponseEntity<?>> swaggerResources() {
        return Mono.just((new ResponseEntity<>(swaggerResources.get(), HttpStatus.OK)));
    }
}

2.3. 配置过滤器

2.3.1 官方demo做法

package com.xiaominfo.swagger.service.doc.config;

import org.apache.commons.lang.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;


/**
 * @author fsl
 * @description: SwaggerHeaderFilter
 * @date 2019-06-0310:47
 */
@Component
public class SwaggerHeaderFilter extends AbstractGatewayFilterFactory {
    private static final String HEADER_NAME = "X-Forwarded-Prefix";

    private static final String 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,URI )) {
                return chain.filter(exchange);
            }
            String basePath = path.substring(0, path.lastIndexOf(URI));
            ServerHttpRequest newRequest = request.mutate().header(HEADER_NAME, basePath).build();
            ServerWebExchange newExchange = exchange.mutate().request(newRequest).build();
            return chain.filter(newExchange);
        };
    }
}

2.3.2 我的项目配置

package com.holland.gateway.swagger;

import org.apache.commons.lang.StringUtils;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;

public class SwaggerRouteFilter {
    private static final String HEADER_NAME = "X-Forwarded-Prefix";

    private static final String URI = "/v2/api-docs";

    public static WebFilter getWebFilter() {
        return (exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();
            String path = request.getURI().getPath();
            if (!StringUtils.endsWithIgnoreCase(path, URI)) {
                return chain.filter(exchange);
            }

            String basePath = path.substring(0, path.lastIndexOf(URI));
            ServerHttpRequest newRequest = request.mutate().header(HEADER_NAME, basePath).build();
            ServerWebExchange newExchange = exchange.mutate().request(newRequest).build();
            return chain.filter(newExchange);
        };
    }
}
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
    http
            .addFilterAfter(SwaggerRouteFilter.getWebFilter(), SecurityWebFiltersOrder.FIRST)
            .addFilterAfter(tokenFilter(), SecurityWebFiltersOrder.AUTHENTICATION)
            .addFilterBefore(logFilter(), SecurityWebFiltersOrder.LAST)
            .csrf(ServerHttpSecurity.CsrfSpec::disable);
    return http.build();
}

2.4. 重点:获取其他服务的信息,才能集成其他服务的swagger

2.4.1. 从配置文件里面获取的服务信息

配置文件比如说如下,那么我们就能集成filesystem的swagger信息

cloud:
  gateway:
    discovery:
      locator:
        enabled: true
    routes:
      - id: filesystem
        uri: http://localhost:8763
        predicates:
          - Path=/filesystem/**
        filters:
          - StripPrefix=1

Config写法

package com.holland.gateway.swagger;

import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;
import springfox.documentation.swagger.web.SwaggerResource;
import springfox.documentation.swagger.web.SwaggerResourcesProvider;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;

@Component
@Primary
public class SwaggerResourceConfig implements SwaggerResourcesProvider {

    /**
     * 此写法只能获取配置文件里面的路由规则
     */
    @Resource
    private RouteLocator routeLocator;
    @Resource
    private GatewayProperties gatewayProperties;

    @Override
    public List<SwaggerResource> get() {
        List<SwaggerResource> resources = new ArrayList<>();

        List<String> routes = new ArrayList<>();
        routeLocator.getRoutes().subscribe(route -> routes.add(route.getId()));
        gatewayProperties.getRoutes().stream().filter(routeDefinition -> routes.contains(routeDefinition.getId())).forEach(route -> {
            route.getPredicates().stream()
                    .filter(predicateDefinition -> ("Path").equalsIgnoreCase(predicateDefinition.getName()))
                    .forEach(predicateDefinition -> resources.add(swaggerResource(route.getId(),
                            predicateDefinition.getArgs().get(NameUtils.GENERATED_NAME_PREFIX + "0")
                                    .replace("**", "v2/api-docs"))));
        });
        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;
    }
}

2.4.2. 通过eureka获取的服务信息

只用修改public List<SwaggerResource> get()方法

@Override
public List<SwaggerResource> get() {

    List<SwaggerResource> resources = new ArrayList<>();

    discoveryClient.getServices()
            .stream()
            //排除掉不需要swagger的模块和自身模块
            .filter(s -> !"eureka".equals(s) && !"admin".equals(s) && !"gateway".equals(s))
            .forEach(appName -> resources.add(swaggerResource(appName, appName + "/swagger/v2/api-docs")));

    return resources;
}

2.4.3. 将gateway自身集成到swagger

只需要改动一点点代码

List<SwaggerResource> resources = new ArrayList<>() {{
    add(swaggerResource("gateway", "/v2/api-docs"));
}};

2.5. 注意事项

访问/doc.html时会调用api/swagger-resources。底层会调用SwaggerResourceConfig.get()方法。

所以如果这里报错了,务必要考虑情况:

  • api/swagger-resources获取的数组不能为空;即,必须要获得其他服务的信息。
  • 至少其他服务得有knife4j-micro-spring-boot-starter依赖,才能保证调用接口/v2/api-docs不会404。

另外,务必要注意url前缀的过滤情况,也可能是/v2/api-docs抛404的原因。

3. 配置其他服务

3.1. 配置swagger核心配置 docket

package com.holland.gateway.swagger;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc;

@Configuration
@EnableSwagger2WebMvc
public class Knife4jConfiguration {

    @Value("${spring.application.name}")
    private String name;

    @Bean
    public Docket defaultApi2() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(groupApiInfo(name))
                .select()
                //这里指定Controller扫描包路径
                .apis(RequestHandlerSelectors.basePackage("com.holland." + name + ".controller"))
                .paths(PathSelectors.any())
                .build();
    }

    private ApiInfo groupApiInfo(String name) {
        return new ApiInfoBuilder()
                .title("后端接口文档-" + name)
                .description("<div style='font-size:14px;color:red;'>description</div>")
                .termsOfServiceUrl("N")
                .contact(new Contact("HollanZang", "https://juejin.cn/user/352263461681214", "zhn.pop@gmail.com"))
                .version("1.0")
                .build();
    }
}

3.2. 配置swagger转发接口

这里需要添加转发接口的原因是:/doc.html页面里面调试其他服务的接口时,访问路径不正确。但是没有找到比较官方的解决办法,所以采用这种方式。

如果有好的实现方式希望可以评论留言交流~

package com.holland.filesystem.swagger;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.springframework.http.ResponseEntity;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import springfox.documentation.spring.web.json.Json;
import springfox.documentation.swagger2.web.Swagger2ControllerWebFlux;

import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;

/**
 * swagger转发控制器
 * 为knife4j配置url前缀
 */
@Controller
public class SwaggerForwardController {

    @Resource
    private Swagger2ControllerWebFlux swagger2ControllerWebFlux;

    @GetMapping("/swagger/v2/api-docs")
    public ResponseEntity<?> forward(@RequestParam(value = "group", required = false) String swaggerGroup,
                                     ServerHttpRequest request) {
        final ResponseEntity<Json> documentation = swagger2ControllerWebFlux.getDocumentation(swaggerGroup, request);

        final Map<String, Object> map = JSON.parseObject(documentation.getBody().value(), Map.class);

        map.computeIfPresent("basePath", (k, pathsObj) -> "/filesystem");
        map.computeIfPresent("paths", (k, pathsObj) -> {
            final Map<String, JSONObject> paths = ((JSONObject) pathsObj).toJavaObject(Map.class);
            final Map<String, JSONObject> res = new HashMap<>(paths.size());
            paths.forEach((k1, v) -> res.put("/filesystem" + k1, v));
            return res;
        });

        return ResponseEntity.ok(map);
    }
}

4. 访问接口文档

大功告成!

image.png