依赖
springboot 2.x 版本
<springdoc.version>1.8.0</springdoc.version>
<!-- swagger3 接口文档生成器 -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>${springdoc.version}</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-webflux-ui</artifactId>
<version>${springdoc.version}</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-webmvc-core</artifactId>
<version>${springdoc.version}</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-common</artifactId>
<version>${springdoc.version}</version>
</dependency>
springboot 3.x 版本
<springdoc.version>2.6.0</springdoc.version>
<!-- swagger3 接口文档生成器 -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${springdoc.version}</version>
</dependency>
<!--webflux-ui用于webflux项目(此处我是为了给gateway网关服务使用) -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webflux-ui</artifactId>
<version>${springdoc.version}</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-common</artifactId>
<version>${springdoc.version}</version>
</dependency>
一、整合配置
springboot2.x
package com.tly.common.gateway.config;
import org.apache.commons.lang3.StringUtils;
import org.springdoc.core.AbstractSwaggerUiConfigProperties;
import org.springdoc.core.Constants;
import org.springdoc.core.SwaggerUiConfigProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.gateway.handler.predicate.PredicateDefinition;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionLocator;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
import java.util.*;
import java.util.stream.Collectors;
import static org.springdoc.core.Constants.SPRINGDOC_ENABLED;
/**
* @author Tian liyuan
*/
@Configuration
@ConditionalOnProperty(name = SPRINGDOC_ENABLED, matchIfMissing = true)
public class SwaggerConfig {
@Autowired
private SwaggerUiConfigProperties swaggerUiConfigProperties;
@Autowired
private RouteDefinitionLocator locator;
@PostConstruct
public void apis() {
//获取所有的路径配置
List<RouteDefinition> definitions = locator.getRouteDefinitions().collectList().block();
//过滤,只要lb模块式
List<RouteDefinition> serviceRoutes = definitions.stream().filter(route -> null != route.getUri() && route.getUri().getScheme().equals("lb")).collect(Collectors.toList());
//按根据PredicateDefinition参数值和/** 确定是否路径匹配
Set<AbstractSwaggerUiConfigProperties.SwaggerUrl> lbRouteUrl = new HashSet<>();
Optional.ofNullable(serviceRoutes).orElse(Collections.emptyList()).forEach(route -> {
AbstractSwaggerUiConfigProperties.SwaggerUrl swaggerUrl = new AbstractSwaggerUiConfigProperties.SwaggerUrl();
if (route != null && route.getId() != null && !"ReactiveCompositeDiscoveryClient".equals(route.getId().split("_")[0])) {
//获取路径前缀
List<PredicateDefinition> predicates = route.getPredicates();
if (null == predicates || predicates.size() <= 0) {
return;
}
String prefix = "";
for (PredicateDefinition predicate : predicates) {
String predicateName = predicate.getName();
if ("path".equalsIgnoreCase(predicateName)) {
for (String regex : predicate.getArgs().values()) {
if (regex.endsWith("/**")) {
prefix = regex.substring(0, regex.length() - 3);
continue;
}
}
}
}
//不是路径匹配的路由,跳过
if (StringUtils.isBlank(prefix)) {
return;
}
swaggerUrl.setUrl(prefix + Constants.DEFAULT_API_DOCS_URL);
swaggerUrl.setName(prefix);
swaggerUrl.setDisplayName(route.getId());
lbRouteUrl.add(swaggerUrl);
}
});
//添加swaggerUI服务集成匹配
if (lbRouteUrl.size() > 0) {
Set<AbstractSwaggerUiConfigProperties.SwaggerUrl> propertiesUrls = swaggerUiConfigProperties.getUrls();
if (null == propertiesUrls || propertiesUrls.size() <= 0) {
propertiesUrls = lbRouteUrl;
} else {
propertiesUrls.addAll(lbRouteUrl);
}
swaggerUiConfigProperties.setUrls(propertiesUrls);
}
}
}
简化版(只发现活跃的服务,无需下方yaml中配置)
package com.tly.gateway.config;
import org.springdoc.core.AbstractSwaggerUiConfigProperties;
import org.springdoc.core.Constants;
import org.springdoc.core.SwaggerUiConfigProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.Collections;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import static org.springdoc.core.Constants.SPRINGDOC_ENABLED;
/**
* @author Tian liyuan
*/
@Configuration
@ConditionalOnProperty(name = SPRINGDOC_ENABLED, matchIfMissing = true)
public class SwaggerConfig {
@Autowired
private SwaggerUiConfigProperties swaggerUiConfigProperties;
@Resource
private DiscoveryClient discoveryClient;
@PostConstruct
public void apis() {
Set<AbstractSwaggerUiConfigProperties.SwaggerUrl> lbRouteUrl = new HashSet<>();
Optional.ofNullable(discoveryClient.getServices()).orElse(Collections.emptyList()).forEach(i -> {
if (!"base-gateway".equals(i)) {
AbstractSwaggerUiConfigProperties.SwaggerUrl swaggerUrl = new AbstractSwaggerUiConfigProperties.SwaggerUrl();
swaggerUrl.setUrl("/" + i + Constants.DEFAULT_API_DOCS_URL);
swaggerUrl.setName("/" + i);
swaggerUrl.setDisplayName(i);
lbRouteUrl.add(swaggerUrl);
}
});
//添加swaggerUI服务集成匹配
if (lbRouteUrl.size() > 0) {
Set<AbstractSwaggerUiConfigProperties.SwaggerUrl> propertiesUrls = swaggerUiConfigProperties.getUrls();
if (null == propertiesUrls || propertiesUrls.size() <= 0) {
propertiesUrls = lbRouteUrl;
} else {
propertiesUrls.addAll(lbRouteUrl);
}
swaggerUiConfigProperties.setUrls(propertiesUrls);
}
}
}
package com.tly.gateway.controller;
import io.swagger.v3.oas.annotations.Operation;
import jakarta.annotation.Resource;
import org.springdoc.core.SwaggerUiConfigParameters;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
/**
* @author Tian liyuan
* ip访问swagger ui配置
* swagger文档聚合配置文件
*/
@RestController
public class SwaggerController {
@Resource
private DiscoveryClient discoveryClient;
//url前缀,一般反向代理会配置前缀为/api
@Value("${server.servlet.context-path:}")
private String prefix;
@Operation(hidden = true)
@GetMapping("/api/swagger-config.json")
public Map<String, Object> swaggerConfig() {
Map<String, Object> config = new LinkedHashMap<>();
List<SwaggerUiConfigParameters.SwaggerUrl> urls = new LinkedList<>();
discoveryClient.getServices().forEach(serviceName ->
discoveryClient.getInstances(serviceName).forEach(serviceInstance -> {
if (!"base-gateway".equals(serviceName)) {
urls.add(new SwaggerUiConfigParameters.SwaggerUrl(serviceName, prefix + "/" + serviceName + "/v3/api-docs", serviceName));
}
// else {
// urls.add(new SwaggerUiConfigParameters.SwaggerUrl(serviceName, prefix + "/v3/api-docs", serviceName));
// }
}
)
);
config.put("urls", urls);
return config;
}
}
springboot3.x
package com.tly.gateway.config;
import jakarta.annotation.PostConstruct;
import org.springdoc.core.properties.AbstractSwaggerUiConfigProperties;
import org.springdoc.core.properties.SwaggerUiConfigProperties;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionLocator;
import org.springframework.context.annotation.Configuration;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import static org.springdoc.core.utils.Constants.SPRINGDOC_ENABLED;
/**
* @author Tian liyuan
* 本地访问swagger配置
*/
@Configuration
@ConditionalOnProperty(name = SPRINGDOC_ENABLED, matchIfMissing = true)
public class SwaggerConfig {
protected final SwaggerUiConfigProperties swaggerUiConfigProperties;
protected final RouteDefinitionLocator routeDefinitionLocator;
public SwaggerConfig(SwaggerUiConfigProperties swaggerUiConfigProperties, RouteDefinitionLocator routeDefinitionLocator) {
this.swaggerUiConfigProperties = swaggerUiConfigProperties;
this.routeDefinitionLocator = routeDefinitionLocator;
}
@PostConstruct
public void autoInitSwaggerUrls() {
List<RouteDefinition> definitions = routeDefinitionLocator.getRouteDefinitions().collectList().block();
if (definitions != null && !definitions.isEmpty()) {
definitions.forEach(routeDefinition -> {
AbstractSwaggerUiConfigProperties.SwaggerUrl swaggerUrl = new AbstractSwaggerUiConfigProperties.SwaggerUrl(
routeDefinition.getId().replace("ReactiveCompositeDiscoveryClient_", "").toLowerCase(),
routeDefinition.getUri().toString().replace("lb://", "").toLowerCase() + "/v3/api-docs",
routeDefinition.getId().replace("ReactiveCompositeDiscoveryClient_", "").toLowerCase()
);
Set<AbstractSwaggerUiConfigProperties.SwaggerUrl> urls = swaggerUiConfigProperties.getUrls();
if (urls == null) {
urls = new LinkedHashSet<>();
swaggerUiConfigProperties.setUrls(urls);
}
urls.add(swaggerUrl);
});
}
}
}
bootstrap.yml配置增加(注:配置类读取路由是从网关配置中的来的)
spring:
main:
lazy-initialization: true
lifecycle:
timeout-per-shutdown-phase: 10s
cloud:
gateway:
discovery:
locator:
enabled: true
routes:
- id: base-system
uri: lb://base-system
predicates:
- Path= base-system/**
filters:
- StripPrefix=1
- id: base-test
uri: lb://base-test
predicates:
- Path= base-test/**
filters:
- StripPrefix=1
springdoc:
api-docs:
path: /v3/api-docs
enabled: true
webjars:
# 设置为空,不要前缀
prefix:
swagger-ui:
path: /swagger-ui/index.html
persistAuthorization: true
二、出现cros跨域问题
整合配置完之后,发现swagger页面是能出来了,但是每个接口包括放行的接口都爆跨域错误,这是由于网关配置中的路由问题
解决方法:
package com.tly.gateway.filter;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.reactivestreams.Publisher;
import org.springdoc.core.SpringDocConfigProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.io.IOException;
import java.io.StringWriter;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.springdoc.core.Constants.SPRINGDOC_ENABLED;
/**
* 处理服务 /v3/api-docs接口返回的数据,在servers里添加可以通过网关直接访问的地址
*/
@Slf4j
@Component
@ConditionalOnProperty(name = SPRINGDOC_ENABLED, matchIfMissing = true)
public class DocServiceUrlModifyGatewayFilter implements GlobalFilter, Ordered {
@Autowired
private SpringDocConfigProperties springDocConfigProperties;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//直接用配置类的值,默认值是 /v3/api-docs
String apiPath = springDocConfigProperties.getApiDocs().getPath();
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
URI uri = request.getURI();
//非正常状态跳过
if (response.getStatusCode().value() != HttpStatus.OK.value()) {
return chain.filter(exchange);
}
//非springdoc文档不拦截
if (!(StringUtils.isNotBlank(uri.getPath()) && uri.getPath().endsWith(apiPath))) {
return chain.filter(exchange);
}
String uriString = uri.toString();
String gatewayUrl = uriString.substring(0, uriString.lastIndexOf(apiPath));
DataBufferFactory bufferFactory = response.bufferFactory();
ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(response) {
@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
//不处理
if (!(body instanceof Flux)) {
return super.writeWith(body);
}
//处理SpringDoc-OpenAPI
Flux<? extends DataBuffer> fluxBody = (Flux<? extends DataBuffer>) body;
return super.writeWith(fluxBody.buffer().map(dataBuffers -> {
DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory();
DataBuffer join = dataBufferFactory.join(dataBuffers);
byte[] content = new byte[join.readableByteCount()];
join.read(content);
DataBufferUtils.release(join);
try {
// 流转为字符串
String responseData = new String(content, StandardCharsets.UTF_8);
Map<String, Object> map = JsonUtils.json2Object(responseData, Map.class);
//处理返回的数据
Object serversObject = map.get("servers");
if (null != serversObject) {
List<Map<String, Object>> servers = (List<Map<String, Object>>) serversObject;
Map<String, Object> gatewayServer = new HashMap<>();
servers.clear();
//网关地址
gatewayServer.put("url", gatewayUrl);
gatewayServer.put("description", "Gateway server url");
//添加到第1个
servers.add(0, gatewayServer);
map.put("servers", servers);
responseData = JsonUtils.object2Json(map);
byte[] uppedContent = responseData.getBytes(StandardCharsets.UTF_8);
response.getHeaders().setContentLength(uppedContent.length);
return bufferFactory.wrap(uppedContent);
}
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return bufferFactory.wrap(content);
}));
}
};
// replace response with decorator
return chain.filter(exchange.mutate().response(decoratedResponse).build());
}
@Override
public int getOrder() {
return -2;
}
class JsonUtils {
public static final ObjectMapper OBJECT_MAPPER = createObjectMapper();
/**
* 初始化ObjectMapper
*
* @return
*/
private static ObjectMapper createObjectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);
objectMapper.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true);
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true);
// objectMapper.registerModule(new Hibernate4Module().enable(Hibernate4Module.Feature.FORCE_LAZY_LOADING));
objectMapper.registerModule(new JavaTimeModule());
return objectMapper;
}
public static String object2Json(Object o) {
StringWriter sw = new StringWriter();
JsonGenerator gen = null;
try {
gen = new JsonFactory().createGenerator(sw);
OBJECT_MAPPER.writeValue(gen, o);
} catch (IOException e) {
throw new RuntimeException("不能序列化对象为Json", e);
} finally {
if (null != gen) {
try {
gen.close();
} catch (IOException e) {
throw new RuntimeException("不能序列化对象为Json", e);
}
}
}
return sw.toString();
}
/**
* 将 json 字段串转换为 对象.
*
* @param json 字符串
* @param clazz 需要转换为的类
* @return
*/
public static <T> T json2Object(String json, Class<T> clazz) {
try {
return OBJECT_MAPPER.readValue(json, clazz);
} catch (IOException e) {
throw new RuntimeException("将 Json 转换为对象时异常,数据是:" + json, e);
}
}
}
}