SpringBoot可以通过整合knife4j来实现在线接口文档功能,但在微服务环境下,每个服务的接口文档访问地址都不相同,访问起来十分麻烦,因此我们可以在gateway成对各个微服务的接口文档进行整合,实现访问网关即可任意切换查看各个微服务的接口文档。
<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>
@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 {
@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();
}
}
编写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);
}
}
<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>
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)));
}
}
整合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);
}
}
}