在微服务架构中,随着业务模块不断拆分,一个中型系统可能包含几十个微服务。每个服务都有自己的API文档,开发人员要记住所有服务的文档地址几乎是不可能的。想象一下,每次调试接口都要在不同服务的Swagger页面之间来回切换,效率有多低。
我经历过一个真实项目,有15个微服务分散在不同服务器上。每次联调时,前端同事都要挨个问后端:"用户服务的文档地址是什么?订单服务的Swagger能发我一下吗?"这种沟通成本高得吓人。后来我们引入Spring Cloud Gateway做文档聚合,所有服务的接口在一个页面就能查看,开发效率直接翻倍。
Swagger3(OpenAPI 3.0)是目前最流行的API文档规范,但原生Swagger UI只能展示单个服务的文档。通过网关聚合后,你可以:
首先确保你的Spring Cloud Gateway已经正常运行。我推荐使用Spring Boot 2.6.x + Spring Cloud 2021.x的组合,这是目前最稳定的版本。在网关服务的pom.xml中添加关键依赖:
xml复制<!-- Swagger聚合核心依赖 -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>
<!-- 网关需要webflux支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
注意一个常见坑点:很多教程还在用老版本的springfox-swagger2,这个库已经不维护了。我们直接用springfox-boot-starter,它内置了对OpenAPI 3.0的支持。
创建Swagger资源提供者类,这是聚合功能的核心。我优化过的版本增加了服务发现自动注册功能:
java复制@Primary
@Component
@RequiredArgsConstructor
public class GatewaySwaggerProvider implements SwaggerResourcesProvider {
private final RouteLocator routeLocator;
private final DiscoveryClient discoveryClient;
@Override
public List<SwaggerResource> get() {
List<SwaggerResource> resources = new ArrayList<>();
// 从网关路由获取服务列表
routeLocator.getRoutes()
.filter(route -> route.getUri().getHost() != null)
.subscribe(route -> {
String serviceName = route.getUri().getHost();
String path = route.getPredicates().stream()
.filter(p -> p.getName().equals("Path"))
.findFirst()
.map(p -> p.getArgs().get("pattern"))
.orElse("/" + serviceName + "/");
resources.add(swaggerResource(serviceName,
path.replace("**", "v3/api-docs")));
});
return resources;
}
private SwaggerResource swaggerResource(String name, String url) {
SwaggerResource resource = new SwaggerResource();
resource.setName(name);
resource.setUrl(url);
resource.setSwaggerVersion("3.0");
return resource;
}
}
这个版本相比基础实现有三个改进:
生产环境绝对不能裸奔Swagger文档!我见过太多因为文档泄露导致的安全事故。最简单的防护是添加基础认证:
java复制@Configuration
@EnableWebFluxSecurity
public class SwaggerSecurityConfig {
@Bean
public SecurityWebFilterChain securityFilterChain(ServerHttpSecurity http) {
return http
.authorizeExchange()
.pathMatchers(
"/swagger-ui.html",
"/webjars/**",
"/v3/api-docs/**",
"/swagger-resources/**"
).authenticated()
.anyExchange().permitAll()
.and()
.httpBasic()
.and()
.formLogin()
.and()
.csrf().disable()
.build();
}
@Bean
public MapReactiveUserDetailsService userDetailsService() {
UserDetails user = User.builder()
.username("admin")
.password("{bcrypt}$2a$10$N9qo8uLOickgx2ZMRZoMy...")
.roles("DEV")
.build();
return new MapReactiveUserDetailsService(user);
}
}
这里有几个安全最佳实践:
对于中大型企业,我推荐集成OAuth2。最近刚帮一个金融客户实现了Keycloak对接:
java复制@Bean
public SecurityWebFilterChain securityFilterChain(ServerHttpSecurity http) {
http.oauth2ResourceServer()
.jwt()
.jwtAuthenticationConverter(jwtConverter());
return http
.authorizeExchange()
.pathMatchers("/swagger*/**").hasAuthority("SCOPE_swagger")
.anyExchange().permitAll()
.and()
.build();
}
private Converter<Jwt, ? extends Mono<? extends AbstractAuthenticationToken>> jwtConverter() {
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(jwt -> {
List<String> scopes = jwt.getClaimAsStringList("scope");
return scopes.stream()
.map(scope -> new SimpleGrantedAuthority("SCOPE_" + scope))
.collect(Collectors.toList());
});
return new ReactiveJwtAuthenticationConverterAdapter(converter);
}
这样配置后:
网关频繁请求下游服务的API文档会影响性能。我们可以添加缓存层:
java复制@Bean
public WebClient webClient() {
return WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(
HttpClient.create()
.responseTimeout(Duration.ofSeconds(3))
))
.filter(ExchangeFilterFunctions
.cacheRequestBodyAndResponse(CacheControl.maxAge(1, TimeUnit.HOURS)))
.build();
}
同时修改资源提供类,加入缓存逻辑:
java复制@Cacheable(value = "swagger-resources", key = "#serviceName")
public SwaggerResource getSwaggerResource(String serviceName) {
// 获取资源逻辑
}
实测这个优化能让文档加载速度提升5倍以上,特别是在服务数量多的时候。
不同团队应该看到不同的文档。我们可以在网关层实现字段级过滤:
java复制public Mono<JsonNode> filterApiDocs(JsonNode original, String role) {
ObjectNode filtered = objectMapper.createObjectNode();
// 复制基础信息
filtered.set("openapi", original.get("openapi"));
filtered.set("info", original.get("info"));
// 根据角色过滤paths
ArrayNode paths = objectMapper.createArrayNode();
original.get("paths").fields().forEachRemaining(entry -> {
if (hasPermission(entry.getKey(), role)) {
paths.set(entry.getKey(), entry.getValue());
}
});
filtered.set("paths", paths);
return Mono.just(filtered);
}
配合前端实现效果:
一定要给文档访问加上监控!我习惯用Prometheus统计访问日志:
java复制@Bean
public MeterRegistryCustomizer<PrometheusMeterRegistry> configureMetrics() {
return registry -> registry.config().commonTags("application", "api-gateway");
}
@GetMapping("/v3/api-docs/{service}")
@Timed(value = "swagger.request",
extraTags = {"service", "{service}"})
public Mono<ResponseEntity<String>> getApiDocs(@PathVariable String service) {
// 获取文档逻辑
}
这样可以在Grafana中看到:
网关挂了不能影响文档访问。我的方案是:
备份脚本示例:
bash复制#!/bin/bash
curl -s http://localhost:8080/v3/api-docs | \
aws s3 cp - s3://my-backup-bucket/swagger-$(date +%Y%m%d).json
加到crontab每天凌晨执行一次。