How can a Spring Cloud Gateway expose its routes through a Swagger UI? Here's a minimalistic static Gateway:
package com.example.gatewaydemo.routing;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
@Configuration
public class MyRoutingConfig {
@Bean
public RouteLocator routeLocator(RouteLocatorBuilder routeLocatorBuilder) {
return routeLocatorBuilder.routes()
.route(predicateSpec -> predicateSpec
.path("/uuid")
.and()
.method(HttpMethod.GET)
.uri("https://httpbin.org")
).build();
}
}
In a regular Spring web app, adding Swagger dependencies is enough
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>3.0.0</version>
</dependency>
It's not the case with Gateway
My Gateway uses Spring Boot 3 and Java 17 so I included this dependency instead
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.0.2</version>
</dependency>
However, it's clear I have to provide some configuration because if I simply include that springdoc
dependency and include the @OpenApiDefinition
annotation
package by.afinny.apigateway;
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.info.Info;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@OpenAPIDefinition(info = @Info(title = "API Gateway", version = "1.0", description = "Documentation API Gateway v1.0"))
public class ApiGatewayV2Application {
public static void main(String[] args) {
SpringApplication.run(ApiGatewayV2Application.class, args);
}
}
I get this response when making a request to /swagger-ui.html
(which gets redirected to /webjars/swagger-ui/index.html
). Sorry, it's ui after all so I have to include a screenshot
Failed to load remote configuration
Please note that I retrieve services and their docs dynamically so static solutions like
springdoc:
enable-native-support: true
api-docs:
enabled: true
swagger-ui:
enabled: true
path: /swagger-ui.html
config-url: /v3/api-docs/swagger-config
urls:
- url: /v3/api-docs
name: API Gateway Service
primaryName: API Gateway Service
- url: /product-service/v3/api-docs
name: Product Service
primaryName: Product Service
- url: /price-service/v3/api-docs
name: Price Service
primaryName: Price Service
won't fit. That is, at startup my Gateway doesn't know whether there are product-service
or price-service
running
In a sense, I guess, my question boils down to this: how do I write Java config for Swagger UI in a Spring Cloud Gateway application?
Here's what I tried
springdoc:
api-docs:
enabled: false
swagger-ui:
enabled: true
path: /swagger-ui.html
config-url: /swagger-ui-config
package by.afinny.apigateway.controller;
import by.afinny.apigateway.model.uiConfig.SwaggerUiConfig;
import by.afinny.apigateway.service.SwaggerUiConfigProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
@RestController
@RequiredArgsConstructor
public class SwaggerUiConfigController {
private final SwaggerUiConfigProvider configProvider;
@GetMapping("/swagger-ui-config")
public Mono<SwaggerUiConfig> getConfig() {
return configProvider.getSwaggerUiConfig();
}
}
package by.afinny.apigateway.model.uiConfig;
import by.afinny.apigateway.model.documentedApplication.SwaggerApplication;
import by.afinny.apigateway.service.SwaggerApplicationSerializer;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.util.Collection;
@NoArgsConstructor
@Getter
public class SwaggerUiConfig {
@JsonProperty("urls")
@JsonSerialize(contentUsing = SwaggerApplicationSerializer.class) // because SwaggerApplication contains a ton of other properties
private Collection<SwaggerApplication> swaggerApplications;
public SwaggerUiConfig(Collection<SwaggerApplication> swaggerApplications) {
this.swaggerApplications = swaggerApplications;
}
public static SwaggerUiConfig from(Collection<SwaggerApplication> swaggerApplications) {
return new SwaggerUiConfig(swaggerApplications);
}
}
package by.afinny.apigateway.service;
import by.afinny.apigateway.model.documentedApplication.SwaggerApplication;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import lombok.SneakyThrows;
import java.io.IOException;
public class SwaggerApplicationSerializer extends JsonSerializer<SwaggerApplication> {
@Override
@SneakyThrows
public void serialize(SwaggerApplication swaggerApplication, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeStartObject();
jsonGenerator.writeStringField("url", swaggerApplication.getUrl());
jsonGenerator.writeStringField("name", swaggerApplication.getName());
jsonGenerator.writeEndObject();
}
}
The returned JSON looks correct
{
"urls": [
{
"url": "/HELLOWORLD/v3/api-docs",
"name": "HELLOWORLD"
}
]
}
However, I still get the same redirect and the same "failed to load configuration" message, nothing changed. What is my mistake?
Your Gateway should serve the docs too
package com.example.dynamicgateway.controller;
import com.example.dynamicgateway.model.uiConfig.SwaggerUiConfig;
import com.example.dynamicgateway.service.swaggerUiSupport.SwaggerUiSupport;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
@RestController
@RequiredArgsConstructor
public class SwaggerUiConfigController {
private final SwaggerUiSupport uiSupport;
@GetMapping("/swagger-ui-config")
public Mono<SwaggerUiConfig> getConfig() {
return uiSupport.getSwaggerUiConfig();
}
}
package com.example.dynamicgateway.controller;
import com.example.dynamicgateway.service.swaggerUiSupport.SwaggerUiSupport;
import io.swagger.v3.oas.models.OpenAPI;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
@RestController
@RequiredArgsConstructor
public class SwaggerDocController {
private final SwaggerUiSupport uiSupport;
@GetMapping("{application-name}/doc")
public Mono<OpenAPI> getSwaggerAppDoc(@PathVariable("application-name") String applicationName) {
// return cached OpenAPI object right away
return uiSupport.getSwaggerAppDoc(applicationName);
}
}
package com.example.dynamicgateway.model.uiConfig;
import com.example.dynamicgateway.model.documentedApplication.SwaggerApplication;
import com.example.dynamicgateway.service.swaggerUiSupport.SwaggerUiConfigSerializer;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.util.Collection;
@NoArgsConstructor
@Getter
public class SwaggerUiConfig {
@JsonProperty("urls")
@JsonSerialize(contentUsing = SwaggerUiConfigSerializer.class)
private Collection<SwaggerApplication> swaggerApplications;
public SwaggerUiConfig(Collection<SwaggerApplication> swaggerApplications) {
this.swaggerApplications = swaggerApplications;
}
public static SwaggerUiConfig from(Collection<SwaggerApplication> swaggerApplications) {
return new SwaggerUiConfig(swaggerApplications);
}
}
package com.example.dynamicgateway.service.swaggerUiSupport;
import com.example.dynamicgateway.model.documentedApplication.SwaggerApplication;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import lombok.SneakyThrows;
import java.text.MessageFormat;
public class SwaggerUiConfigSerializer extends JsonSerializer<SwaggerApplication> {
@Override
@SneakyThrows
public void serialize(SwaggerApplication swaggerApplication, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) {
jsonGenerator.writeStartObject();
jsonGenerator.writeStringField("url", MessageFormat.format("/{0}/doc", swaggerApplication.getName()));
jsonGenerator.writeStringField("name", swaggerApplication.getName());
jsonGenerator.writeEndObject();
}
}
gateway:
servers:
- url: https://localhost:8080
description: Api-Gateway-V2
v1Prefix: /api/v1
springdoc:
swagger-ui:
enabled: true
path: /swagger-ui.html
config-url: /swagger-ui-config
I won't include the entire code, but here's my SwaggerUiSupport
. EndpointCollector
is an interface that represent an endpoint cache (all endpoints collected from Eureka applications, assuming they have a Swagger/OpenApi/Springdoc dependency)
package com.example.dynamicgateway.service.swaggerUiSupport;
import com.example.dynamicgateway.model.documentedApplication.DocumentedApplication;
import com.example.dynamicgateway.model.documentedApplication.SwaggerApplication;
import com.example.dynamicgateway.model.documentedEndpoint.DocumentedEndpoint;
import com.example.dynamicgateway.model.documentedEndpoint.SwaggerEndpoint;
import com.example.dynamicgateway.model.gatewayMeta.GatewayMeta;
import com.example.dynamicgateway.model.uiConfig.SwaggerUiConfig;
import com.example.dynamicgateway.service.endpointCollector.EndpointCollector;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.PathItem;
import io.swagger.v3.oas.models.Paths;
import io.swagger.v3.parser.core.models.SwaggerParseResult;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.text.MessageFormat;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.stream.Collectors;
@Component
@Slf4j
@RequiredArgsConstructor
public class BasicSwaggerUiSupport implements SwaggerUiSupport {
private final EndpointCollector<SwaggerEndpoint> endpointCollector;
private final GatewayMeta gatewayMeta;
@Override
public Mono<SwaggerUiConfig> getSwaggerUiConfig() {
Set<SwaggerApplication> swaggerApps = endpointCollector.getKnownEndpoints().stream()
.map(DocumentedEndpoint::getDeclaringApp)
.collect(Collectors.toSet());
return Mono.just(SwaggerUiConfig.from(swaggerApps));
}
@Override
@SneakyThrows
public Mono<OpenAPI> getSwaggerAppDoc(String appName) {
return Mono.just(
endpointCollector.getKnownEndpoints().stream()
.map(DocumentedEndpoint::getDeclaringApp)
.filter(documentedApplication -> documentedApplication.getName().equals(appName))
.map(DocumentedApplication::getNativeDoc)
.map(SwaggerParseResult::getOpenAPI)
.peek(this::setGatewayPrefixes)
.peek(this::setGatewayServers)
.findFirst()
.orElseThrow(() -> new IllegalArgumentException(MessageFormat.format(
"No service with name {0} is known to this Gateway", appName
)))
);
}
private void setGatewayPrefixes(OpenAPI openAPI) {
Paths newPaths = new Paths();
for (Map.Entry<String, PathItem> pathItemEntry : openAPI.getPaths().entrySet()) {
String servicePath = pathItemEntry.getKey();
PathItem pathItem = pathItemEntry.getValue();
String prefixedPath = servicePath;
if (servicePath != null && !servicePath.startsWith(gatewayMeta.v1Prefix())) {
String nonprefixedPath = endpointCollector.getKnownEndpoints().stream()
.filter(documentedEndpoint -> documentedEndpoint.getDetails().getPath().equals(servicePath))
.map(documentedEndpoint -> documentedEndpoint.getDetails().getNonPrefixedPath())
.findFirst()
.orElseThrow(() -> new NoSuchElementException(MessageFormat.format(
"No endpoint found. Requested path: {0}", servicePath
)));
prefixedPath = gatewayMeta.v1Prefix() + nonprefixedPath;
}
newPaths.put(prefixedPath, pathItem);
}
openAPI.setPaths(newPaths);
}
private void setGatewayServers(OpenAPI openAPI) {
openAPI.setServers(gatewayMeta.servers());
}
}