SpringBoot可以通过整合knife4j来实现在线接口文档功能,但在微服务环境下,每个服务的接口文档访问地址都不相同,访问起来十分麻烦,因此我们可以在gateway成对各个微服务的接口文档进行整合,实现访问网关即可任意切换查看各个微服务的接口文档。
org.springframework.boot spring-boot-starter-parent 2.6.11
org.springframework.boot spring-boot-starter com.github.xiaoymin knife4j-spring-boot-starter 3.0.3 org.springframework.boot spring-boot-starter-web true com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery com.alibaba.cloud spring-cloud-starter-alibaba-nacos-config org.springframework.cloud spring-cloud-starter-loadbalancer
@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;
}
@ConditionalOnClass(EnableSwagger2.class)
@Configuration
@EnableSwagger2
@EnableKnife4j
@Import(BeanValidatorPluginsConfiguration.class)
@EnableConfigurationProperties(SwaggerProperties.class)
public class Knife4jConfiguration implements WebMvcConfigurer {@Autowiredprivate SwaggerProperties swaggerProperties;@Autowiredprivate ApplicationInfo applicationInfo;@Beanpublic static BeanPostProcessor springfoxHandlerProviderBeanPostProcessor() {return new BeanPostProcessor() {@Overridepublic Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {//spring boot 2.6以上版本使用 PATH_PATTERN_PARSER,swagger并没有兼容,所以替换为 ANT_PATH_MATCHERif(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 void customizeSpringfoxHandlerMappings(List mappings) {List copy = mappings.stream().filter(mapping -> mapping.getPatternParser() == null).collect(Collectors.toList());mappings.clear();mappings.addAll(copy);}@SuppressWarnings("unchecked")private List getHandlerMappings(Object bean) {try {Field field = ReflectionUtils.findField(bean.getClass(), "handlerMappings");field.setAccessible(true);return (List) field.get(bean);} catch (IllegalArgumentException | IllegalAccessException e) {throw new IllegalStateException(e);}}};}@Beanpublic 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();}
}
编写ResponseBodyAdvice拦截swagger接口的请求,当通过网关访问swagger接口文档时需要拼接微服务访问前缀,否则网关的web页面访问会404
@Slf4j
@RestControllerAdvice
@ConditionalOnClass({ResponseBodyAdvice.class, EnableSwagger2.class})
public class SwaggerResponseBodyAdvice implements ResponseBodyAdvice {@Autowiredprivate ApplicationInfo applicationInfo;@Overridepublic 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"));}@Overridepublic 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);}
}
org.springframework.boot spring-boot-starter-parent 2.6.11
org.springframework.boot spring-boot-starter com.github.xiaoymin knife4j-spring-boot-starter 3.0.3 com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery com.alibaba.cloud spring-cloud-starter-alibaba-nacos-config org.springframework.cloud spring-cloud-starter-gateway org.springframework.cloud spring-cloud-starter-loadbalancer
SwaggerResourceConfig
@Primary
@Configuration
public class SwaggerResourceConfig implements SwaggerResourcesProvider {@Autowiredprivate RouteLocator routeLocator;// 网关应用名称@Value("${spring.application.name}")private String applicationName;//接口地址private static final String API_URI = "/v3/api-docs";@Overridepublic List get() {//接口资源列表List resources = new ArrayList<>();//服务名称列表List 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 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";@Overridepublic 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;@Autowiredpublic 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));}@GetMapping()public Mono swaggerResources() {return Mono.just((new ResponseEntity<>(swaggerResources.get(), HttpStatus.OK)));}}
整合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 {@Autowiredprivate ApplicationEventPublisher applicationEventPublisher;@Autowiredprivate RouteDefinitionWriter routeDefinitionWriter;@Autowiredprivate ReactiveDiscoveryClient discoveryClient;@Autowiredprivate DiscoveryLocatorProperties discoveryLocatorProperties;/*** 用于存放已经加载的路由配置* */private ConcurrentHashSet routeSet = new ConcurrentHashSet<>(16);@PostConstructpublic 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 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);}}
}