SpringCloud整合Knife4j实现接口文档

SpringBoot可以通过整合knife4j来实现在线接口文档功能,但在微服务环境下,每个服务的接口文档访问地址都不相同,访问起来十分麻烦,因此我们可以在gateway成对各个微服务的接口文档进行整合,实现访问网关即可任意切换查看各个微服务的接口文档。

一、微服务整合knife4j接口文档

1. 引入依赖

<parent>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-parentartifactId>
    <version>2.6.11version>
    <relativePath/> 
parent>

<dependencies>
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starterartifactId>
    dependency>
    
    <dependency>
        <groupId>com.github.xiaoymingroupId>
        <artifactId>knife4j-spring-boot-starterartifactId>
        <version>3.0.3version>
    dependency>
    
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-webartifactId>
        <optional>trueoptional>
    dependency>
    
    <dependency>
        <groupId>com.alibaba.cloudgroupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
    dependency>

    <dependency>
        <groupId>com.alibaba.cloudgroupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-configartifactId>
    dependency>

    <dependency>
        <groupId>org.springframework.cloudgroupId>
        <artifactId>spring-cloud-starter-loadbalancerartifactId>
    dependency>

dependencies>

2. 编写配置类

@ConfigurationProperties(prefix = "swagger.doc")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SwaggerProperties {

    private boolean enable = true;

    /**
     * 作者
     */
    private String author;

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

    /**
     * 项目描述
     */
    private String description;

    /**
     * 官网地址
     */
    private String url;

    /**
     * 邮箱地址
     */
    private String email;
}

3. 编写配置类配置knife4j

@ConditionalOnClass(EnableSwagger2.class)
@Configuration
@EnableSwagger2
@EnableKnife4j
@Import(BeanValidatorPluginsConfiguration.class)
@EnableConfigurationProperties(SwaggerProperties.class)
public class Knife4jConfiguration implements WebMvcConfigurer {

    @Autowired
    private SwaggerProperties swaggerProperties;

    @Autowired
    private ApplicationInfo applicationInfo;


    @Bean
    public static BeanPostProcessor springfoxHandlerProviderBeanPostProcessor() {
        return new BeanPostProcessor() {

            @Override
            public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {

                //spring boot 2.6以上版本使用 PATH_PATTERN_PARSER,swagger并没有兼容,所以替换为 ANT_PATH_MATCHER
                if(bean instanceof WebMvcProperties){
                    WebMvcProperties pathmatch = (WebMvcProperties)bean;
                    pathmatch.getPathmatch().setMatchingStrategy(WebMvcProperties.MatchingStrategy.ANT_PATH_MATCHER);
                }

                if (bean instanceof WebMvcRequestHandlerProvider || bean instanceof WebFluxRequestHandlerProvider) {
                    customizeSpringfoxHandlerMappings(getHandlerMappings(bean));
                }

                return bean;
            }

            private <T extends RequestMappingInfoHandlerMapping> void customizeSpringfoxHandlerMappings(List<T> mappings) {
                List<T> copy = mappings.stream()
                        .filter(mapping -> mapping.getPatternParser() == null)
                        .collect(Collectors.toList());
                mappings.clear();
                mappings.addAll(copy);
            }

            @SuppressWarnings("unchecked")
            private List<RequestMappingInfoHandlerMapping> getHandlerMappings(Object bean) {
                try {
                    Field field = ReflectionUtils.findField(bean.getClass(), "handlerMappings");
                    field.setAccessible(true);
                    return (List<RequestMappingInfoHandlerMapping>) field.get(bean);
                } catch (IllegalArgumentException | IllegalAccessException e) {
                    throw new IllegalStateException(e);
                }
            }
        };
    }


    @Bean
    public Docket createRestApi() {
        Docket docket = new Docket(DocumentationType.OAS_30).pathMapping("/")
                .groupName(SpringUtil.getApplicationName())
                // 定义是否开启swagger,false为关闭,可以通过变量控制
                .enable(true)
                // 将api的元信息设置为包含在json ResourceListing响应中。
                .apiInfo(apiInfo())
                // 选择哪些接口作为swagger的doc发布
                .select()
                //指定某个路径才能生成swagger
                .paths(PathSelectors.any())
                .paths(s -> !PathSelectors.regex("/error/*").test(s))
                .build()
                //是否启用
                .enable(swaggerProperties.isEnable())
                // 支持的通讯协议集合
                .protocols(new HashSet<>(Arrays.asList("https", "http")));
        return docket;
    }

    /**
     * API 页面上半部分展示信息
     */
    private ApiInfo apiInfo() {
        return new ApiInfoBuilder().title(swaggerProperties.getTitle())
                .description(swaggerProperties.getDescription())
                .contact(new Contact(swaggerProperties.getAuthor(), swaggerProperties.getUrl(), swaggerProperties.getEmail()))
                .version(applicationInfo.getVersion())
                .build();
    }
}

4. 编写ResponseBodyAdvice拦截swagger接口的请求

编写ResponseBodyAdvice拦截swagger接口的请求,当通过网关访问swagger接口文档时需要拼接微服务访问前缀,否则网关的web页面访问会404

@Slf4j
@RestControllerAdvice
@ConditionalOnClass({ResponseBodyAdvice.class, EnableSwagger2.class})
public class SwaggerResponseBodyAdvice implements ResponseBodyAdvice {
    @Autowired
    private ApplicationInfo applicationInfo;

    @Override
    public boolean supports(MethodParameter methodParameter, Class converterType) {
        if(!(RequestContextHolder.getRequestAttributes() instanceof ServletRequestAttributes)){
            return false;
        }

        ServletRequestAttributes requestAttributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = requestAttributes.getRequest();
        String requestURI = request.getRequestURI();

        //拦截 /v3/api-docs 并且参数带有prefix的(网关请求有配置带上prefix参数)
        return requestURI.equals(StrUtil.replace(applicationInfo.getWebContextPath() + "/v3/api-docs","//","/")) &&
                StrUtil.isNotBlank(request.getParameter("prefix"));
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {

        //获取请求的前缀
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
        String prefix = StrUtil.addPrefixIfNot(requestAttributes.getRequest().getParameter("prefix"),"/");

        //重新生成一个paths,拼接上参数带的前缀
        Paths newPaths = new Paths();

        OpenAPI openAPI = JSONUtil.parseObj(((Json) body).value()).toBean(OpenAPI.class);

        openAPI.getPaths().forEach((path, pathObj)->{
            newPaths.put(prefix+path,pathObj);
        });

        openAPI.setPaths(newPaths);
        return JSONUtil.toJsonStr(openAPI);
    }
}

二、网关整合各微服务接口文档

1. 引入依赖

<parent>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-parentartifactId>
    <version>2.6.11version>
    <relativePath/>
parent>

<dependencies>
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starterartifactId>
    dependency>
    
    <dependency>
        <groupId>com.github.xiaoymingroupId>
        <artifactId>knife4j-spring-boot-starterartifactId>
        <version>3.0.3version>
    dependency>
    
    <dependency>
        <groupId>com.alibaba.cloudgroupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
    dependency>

    <dependency>
        <groupId>com.alibaba.cloudgroupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-configartifactId>
    dependency>

    <dependency>
        <groupId>org.springframework.cloudgroupId>
        <artifactId>spring-cloud-starter-gatewayartifactId>
    dependency>

    <dependency>
        <groupId>org.springframework.cloudgroupId>
        <artifactId>spring-cloud-starter-loadbalancerartifactId>
    dependency>

dependencies>

2. 编写配置类配置Knife4j

SwaggerResourceConfig

@Primary
@Configuration
public class SwaggerResourceConfig implements SwaggerResourcesProvider {
    @Autowired
    private RouteLocator routeLocator;

    // 网关应用名称
    @Value("${spring.application.name}")
    private String applicationName;

    //接口地址
    private static final String API_URI = "/v3/api-docs";

    @Override
    public List<SwaggerResource> get() {
        //接口资源列表
        List<SwaggerResource> resources = new ArrayList<>();
        //服务名称列表
        List<String> routeHosts = new ArrayList<>();

        // 获取所有可用的微服务
        routeLocator.getRoutes()
                .filter(route -> route.getUri().getHost() != null)
                .filter(route -> !applicationName.equals(route.getUri().getHost()))
                .subscribe(route -> {
                    routeHosts.add(route.getUri().getHost());
                });

        // 去重,多负载服务只添加一次
        Set<String> existsServer = new HashSet<>();
        routeHosts.forEach(host -> {
            // 拼接url 拼接前缀
            String url = "/" + host + API_URI+"?prefix="+host+"&group="+host;
            //不存在则添加
            if (!existsServer.contains(url)) {
                existsServer.add(url);
                SwaggerResource swaggerResource = new SwaggerResource();
                swaggerResource.setUrl(url);
                swaggerResource.setName(host);
                resources.add(swaggerResource);
            }
        });
        return resources;
    }
}

SwaggerHeaderFilter

@Configuration
public class SwaggerHeaderFilter extends AbstractGatewayFilterFactory {
    private static final String HEADER_NAME = "X-Forwarded-Prefix";
    private static final String URI = "/v3/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)) {
                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);
            }
            else {
                return chain.filter(exchange);
            }
        };
    }
}

SwaggerHandler

@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<ResponseEntity<SecurityConfiguration>> securityConfiguration() {
        return Mono.just(new ResponseEntity<>(Optional.ofNullable(securityConfiguration).orElse(SecurityConfigurationBuilder.builder().build()), HttpStatus.OK));

    }

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

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

}

3. 为每个微服务自动配置一个以微服务名开头的路由配置

整合knife4j后通过网关访问某个微服务接口时前缀是以服务名开头的,因此我们需要配置路由规则,否则knife4j的接口访问会404。

SpringCloud Gateway 其实已经帮我们实现了此功能,只需要在boostrap.properties内配置即可

# enabled:默认为false,设置为true表明spring cloud gateway开启服务发现和路由的功能,网关自动根据注册中心的服务名为每个服务创建一个router,将以服务名开头的请求路径转发到对应的服务
spring.cloud.gateway.discovery.locator.enabled=true
# lowerCaseServiceId:启动 locator.enabled=true 自动路由时,路由的路径默认会使用大写ID,若想要使用小写ID,可将lowerCaseServiceId设置为true
spring.cloud.gateway.discovery.locator.lower-case-service-id=true

以下配置是通过配置类的方式实现的,如果配置文件配置方式能够生效可以忽略(本项目内实现了写自定义的配置导致配置文件方式失效,所以才通过配置类方式实现)

@Slf4j
@Configuration
@EnableScheduling
public class ApplicationRouteConfiguration  {

    @Autowired
    private ApplicationEventPublisher applicationEventPublisher;

    @Autowired
    private RouteDefinitionWriter routeDefinitionWriter;

    @Autowired
    private ReactiveDiscoveryClient discoveryClient;

    @Autowired
    private DiscoveryLocatorProperties discoveryLocatorProperties;

    /**
     * 用于存放已经加载的路由配置
     * */
    private ConcurrentHashSet<String> routeSet = new ConcurrentHashSet<>(16);

    @PostConstruct
    public void postRegisterRoute(){
        registerRoute();
    }

    /**
     * 每5秒刷新下路由配置,保证新注册的微服务也能够被配置
     */
    @Scheduled(cron = "0/5 * * * * ?")
    public void refreshRoute(){
        registerRoute();
    }

    public void registerRoute(){
        //由于网关配置了动态路由刷新,和springcloud提供的配置 spring.cloud.gateway.discovery.locator.enabled=true 冲突
        //所以我们需要手动注册路由规则
        DiscoveryLocatorProperties properties = BeanUtil.copyProperties(discoveryLocatorProperties, DiscoveryLocatorProperties.class);
        properties.setEnabled(true);

        //通过DiscoveryClientRouteDefinitionLocator生成规则
        DiscoveryClientRouteDefinitionLocator discoveryClientRouteDefinitionLocator = new DiscoveryClientRouteDefinitionLocator(discoveryClient,properties);
        //注册规则
        Flux<RouteDefinition> routeDefinitions = discoveryClientRouteDefinitionLocator.getRouteDefinitions();

        AtomicInteger addCount = new AtomicInteger(0);

        //去重循环添加
        routeDefinitions
            .filter( routeDefinition -> !routeSet.contains(routeDefinition.getPredicates().get(0).getArgs().get("pattern")))
            .subscribe( routeDefinition -> {
                //放入set,防止重复添加
                routeSet.add(routeDefinition.getPredicates().get(0).getArgs().get("pattern"));
                routeDefinitionWriter.save(Mono.just(routeDefinition)).subscribe();
                addCount.incrementAndGet();
                log.info("新增路由规则=>{}",routeDefinition);
            });

        if(addCount.get()>0){
            applicationEventPublisher.publishEvent(routeDefinitionWriter);
        }

    }
}

你可能感兴趣的:(SpringCloud,spring,cloud,java,spring,boot)