时间:2022-11-02 11:01:14 | 栏目:JAVA代码 | 点击:次
当系统中微服务数量越来越多时,如果任由这些服务散落在各处,那么最终管理每个项目的接口文档将是一件十分麻烦的事情,单是记住所有微服务的接口文档访问地址就是一件苦差事了。当如果能够将所有微服务项目的接口文档都统一汇总在同一个可视化页面,那么将大大减少我们的接口文档管理维护工作,为此,我们可以基于 Spring Cloud Gateway 网关 + nacos + knife4j 对所有微服务项目的接口文档进行聚合,从而实现我们想要的文档管理功能
注:本案例需要 springboot 提前整合 nacos 作为注册中心,springcloud 整合 nacos 注册中心部分内容欢迎阅读这篇文章:Nacos注册中心的部署与用法详细介绍
随着我们的系统架构不断地发展,系统中微服务的数量肯定会越来越多,我们不可能每添加一个服务,就在网关配置一个新的路由规则,这样的维护成本很大;特别在很多种情况,我们在请求路径中会携带一个路由标识方便进行转发,而这个路由标识一般都是服务在注册中心中的服务名,因此这是我们就可以开启 spring cloud gateway 的自动路由功能,网关自动根据注册中心的服务名为每个服务创建一个router,将以服务名开头的请求路径转发到对应的服务,配置如下:
# 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
这里需要注意的是,如果我们的配置了 server.servlet.context-path 属性,这会导致自动路由失败的问题,因此我们需要做如下两个修改:
# 重写过滤链,解决项目设置了 server.servlet.context-path 导致 locator.enabled=true 默认路由策略404的问题 spring.cloud.gateway.discovery.locator.filters[0] = PreserveHostHeader
@Configuration public class GatewayConfig { @Value ("${server.servlet.context-path}") private String prefix; /** * 过滤 server.servlet.context-path 属性配置的项目路径,防止对后续路由策略产生影响,因为 gateway 网关不支持 servlet */ @Bean @Order (-1) public WebFilter apiPrefixFilter() { return (exchange, chain) -> { ServerHttpRequest request = exchange.getRequest(); String path = request.getURI().getRawPath(); path = path.startsWith(prefix) ? path.replaceFirst(prefix, "") : path; ServerHttpRequest newRequest = request.mutate().path(path).build(); return chain.filter(exchange.mutate().request(newRequest).build()); }; } }
至此,网关将自动根据注册中心的服务名为每个服务创建一个router,将以服务名开头的请求路径转发到对应的服务。
<dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>knife4j-spring-boot-starter</artifactId> <version>2.0.4</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> </dependency>
在集成 Spring Cloud Gateway 网关的时候,会出现没有 basePath 的情况,例如定义的 /user、/order 等微服务前缀,因此我们需要在 Gateway 网关添加一个 Filter 过滤器
import org.apache.commons.lang3.StringUtils; import org.springframework.cloud.gateway.filter.GatewayFilter; import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory; import org.springframework.context.annotation.Configuration; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.web.server.ServerWebExchange; @Configuration 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)) { 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); }; } }
在使用 SpringBoot 等单体架构集成 swagger 时,我们是基于包路径进行业务分组,然后在前端进行不同模块的展示,而在微服务架构下,一个服务就类似于原来我们写的一个业务组。springfox-swagger 提供的分组接口是 swagger-resource,返回的是分组接口名称、地址等信息,而在Spring Cloud微服务架构下,我们需要重写该接口,改由通过网关的注册中心动态发现所有的微服务文档,代码如下:
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.cloud.gateway.route.RouteLocator; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import springfox.documentation.swagger.web.SwaggerResource; import springfox.documentation.swagger.web.SwaggerResourcesProvider; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; /** * 使用Spring Boot单体架构集成swagger时,是通过包路径进行业务分组,然后在前端进行不同模块的展示,而在微服务架构下,单个服务类似于原来业务组; * springfox-swagger提供的分组接口是swagger-resource,返回的是分组接口名称、地址等信息; * 在Spring Cloud微服务架构下,需要swagger-resource重写接口,由网关的注册中心动态发现所有的微服务文档 */ @Primary @Configuration public class SwaggerResourceConfig implements SwaggerResourcesProvider { @Autowired private RouteLocator routeLocator; // 网关应用名称 @Value ("${spring.application.name}") private String applicationName; //接口地址 private static final String API_URI = "/v2/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; //不存在则添加 if (!existsServer.contains(url)) { existsServer.add(url); SwaggerResource swaggerResource = new SwaggerResource(); swaggerResource.setUrl(url); swaggerResource.setName(host); resources.add(swaggerResource); } }); return resources; } }
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.*; import java.util.Optional; /** * 获取api接口信息 */ @RestController @RequestMapping ("/swagger-resources") public class SwaggerHandler { @Autowired(required = false) private SecurityConfiguration securityConfiguration; 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))); }
<!-- knife4j文档,微服务架构 --> <dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>knife4j-micro-spring-boot-starter</artifactId> <version>2.0.4</version> </dependency>
import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.context.request.async.DeferredResult; import springfox.documentation.builders.*; import springfox.documentation.service.Contact; import springfox.documentation.spi.DocumentationType; import springfox.documentation.spring.web.paths.RelativePathProvider; import springfox.documentation.spring.web.plugins.Docket; import springfox.documentation.swagger2.annotations.EnableSwagger2; import javax.servlet.ServletContext; /** * @description: swagger配置文件 **/ @Configuration @EnableSwagger2 @EnableKnife4j public class Swagger2Config { @Value("${spring.profiles.active}") private String env; @Value("${spring.application.name}") private String serviceName; @Value("${gateway.service.name}") private String serviceNameForGateway; @Bean public Docket createDocket(ServletContext servletContext) { Docket docket = new Docket(DocumentationType.SWAGGER_2) .genericModelSubstitutes(DeferredResult.class) .forCodeGeneration(true) .pathMapping("/") .select() .build() .apiInfo(new ApiInfoBuilder() .title(serviceName + "接口文档") .version("1.0") .contact(new Contact("xxx","","")) .license("XXX有限公司") .build()) // 如果为生产环境,则不创建swagger .enable(!"real".equals(env)); // 在knife4j前端页面的地址路径中添加gateway网关的项目名,解决在调试接口、发送请求出现404的问题 docket.pathProvider(new RelativePathProvider(servletContext) { @Override public String getApplicationBasePath() { return "/" + serviceNameForGateway + super.getApplicationBasePath(); } }); return docket; } }
文章的最后,再介绍 knife4j 官方提供的另一种接口文档聚合的方式:Aggregation微服务聚合组件,官方地址:https://doc.xiaominfo.com/knife4j/resources/,感兴趣的读者可以去官方看下如何使用