The article shows how to easily document multiple Spring-based REST applications using the SpringFox Swagger-UI library in a microservices environment.
1. Problem
As you know, it is very easy to document a Spring REST Service application using the SpringFox Swagger-UI library, but there is a problem that arises when working in an environment where we have a lot of REST application that is typically a microservices environment. Most of us manage a separate Swagger-UI for each service, which means that each service will need a separate endpoint to access Swagger-UI and we have to have different URLs for different services. .
2. Solution
To be able to access all API documents from a single URL we can implement the following solution:
- Retrieve all registered services list from service registry, here is Eureka server
- For each registered service, pull Swagger Definition JSON to a centralized API document service and store it locally. Specifically here will use Concurrent Map to save JSON in local memory
- Refresh in-memory context periodically to automatically update the JSON definition when services are added, updated, or removed from the service registry
- Provide a single endpoint to access the centralized API document service, where all the API documents of the entire service in the system are centralized.
3. Implementation
For example, we have a system of microservices consisting of 2 main services, Person Service and Employee Service. Now let’s implement a document service to centralize all API documents from Person Service and Employee Service. The service model will look like this:
- central-docs-eureka-server: Service registry provided by Netflix Eureka
- employee-service and person-service: 2 main REST services have integrated Swagger-UI
- documentation-service: service collects all API documents from other services and provides Document UI with a single endpoint
3.1 SwaggerUIConfiguration
Spring configuration class registers instance of SwaggerResourcesProvider, this class reads swagger-api JSON files from ServiceDefinitionsContext
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | package com.satish.central.docs.config.swagger; import java.util.ArrayList; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Primary; import org.springframework.web.client.RestTemplate; import springfox.documentation.swagger.web.InMemorySwaggerResourcesProvider; import springfox.documentation.swagger.web.SwaggerResource; import springfox.documentation.swagger.web.SwaggerResourcesProvider; @Configuration public class SwaggerUIConfiguration { @Autowired private ServiceDefinitionsContext definitionContext; @Bean public RestTemplate configureTempalte(){ return new RestTemplate(); } @Primary @Bean @Lazy public SwaggerResourcesProvider swaggerResourcesProvider(InMemorySwaggerResourcesProvider defaultResourcesProvider, RestTemplate temp) { return () -> { List<SwaggerResource> resources = new ArrayList<>(defaultResourcesProvider.get()); resources.clear(); resources.addAll(definitionContext.getSwaggerDefinitions()); return resources; }; } } |
3.2 ServiceDefinitionController
Adds API that returns JSON definition information by service id from ServiceDefinitionsContext
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | package com.satish.central.docs.web; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; import com.satish.central.docs.config.swagger.ServiceDefinitionsContext; @RestController public class ServiceDefinitionController { @Autowired private ServiceDefinitionsContext definitionContext; @GetMapping("/service/{servicename}") public String getServiceDefinition(@PathVariable("servicename") String serviceName) { return definitionContext.getSwaggerDefinition(serviceName); } } |
3.3 ServiceDefinitionsContext
Component stores all JSON definitions in memory
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | package com.satish.central.docs.config.swagger; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Component; import springfox.documentation.swagger.web.SwaggerResource; @Component @Scope(scopeName = ConfigurableBeanFactory.SCOPE_SINGLETON) public class ServiceDefinitionsContext { private final ConcurrentHashMap < String, String > serviceDescriptions; private ServiceDefinitionsContext() { serviceDescriptions = new ConcurrentHashMap < String, String > (); } public void addServiceDefinition(String serviceName, String serviceDescription) { serviceDescriptions.put(serviceName, serviceDescription); } public String getSwaggerDefinition(String serviceId) { return this.serviceDescriptions.get(serviceId); } public List < SwaggerResource > getSwaggerDefinitions() { return serviceDescriptions.entrySet().stream().map(serviceDefinition -> { SwaggerResource resource = new SwaggerResource(); resource.setLocation("/service/" + serviceDefinition.getKey()); resource.setName(serviceDefinition.getKey()); resource.setSwaggerVersion("2.0"); return resource; }).collect(Collectors.toList()); } } |
3.4 ServiceDescriptionUpdater
This is the most important component, this component is responsible for pulling all the JSON definitions from the services registered on the service registry and storing them in the ServiceDefinitionsContext.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 | package com.satish.central.docs.config.swagger; import java.time.LocalDate; import java.util.List; import java.util.Optional; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; @Component public class ServiceDescriptionUpdater { private static final Logger logger = LoggerFactory.getLogger(ServiceDescriptionUpdater.class); private static final String DEFAULT_SWAGGER_URL = "/v2/api-docs"; private static final String KEY_SWAGGER_URL = "swagger_url"; @Autowired private DiscoveryClient discoveryClient; private final RestTemplate template; public ServiceDescriptionUpdater() { this.template = new RestTemplate(); } @Autowired private ServiceDefinitionsContext definitionContext; @Scheduled(fixedDelayString = "${swagger.config.refreshrate}") public void refreshSwaggerConfigurations() { logger.debug("Starting Service Definition Context refresh"); discoveryClient.getServices().stream().forEach(serviceId -> { logger.debug("Attempting service definition refresh for Service : {} ", serviceId); List < ServiceInstance > serviceInstances = discoveryClient.getInstances(serviceId); if (serviceInstances == null || serviceInstances.isEmpty()) { //Should not be the case kept for failsafe logger.info("No instances available for service : {} ", serviceId); } else { ServiceInstance instance = serviceInstances.get(0); String swaggerURL = getSwaggerURL(instance); Optional < Object > jsonData = getSwaggerDefinitionForAPI(serviceId, swaggerURL); if (jsonData.isPresent()) { String content = getJSON(serviceId, jsonData.get()); definitionContext.addServiceDefinition(serviceId, content); } else { logger.error("Skipping service id : {} Error : Could not get Swagegr definition from API ", serviceId); } logger.info("Service Definition Context Refreshed at : {}", LocalDate.now()); } }); } private String getSwaggerURL(ServiceInstance instance) { String swaggerURL = instance.getMetadata().get(KEY_SWAGGER_URL); return swaggerURL != null ? instance.getUri() + swaggerURL : instance.getUri() + DEFAULT_SWAGGER_URL; } private Optional < Object > getSwaggerDefinitionForAPI(String serviceName, String url) { logger.debug("Accessing the SwaggerDefinition JSON for Service : {} : URL : {} ", serviceName, url); try { Object jsonData = template.getForObject(url, Object.class); return Optional.of(jsonData); } catch (RestClientException ex) { logger.error("Error while getting service definition for service : {} Error : {} ", serviceName, ex.getMessage()); return Optional.empty(); } } public String getJSON(String serviceId, Object jsonData) { try { return new ObjectMapper().writeValueAsString(jsonData); } catch (JsonProcessingException e) { logger.error("Error : {} ", e.getMessage()); return ""; } } } |
4. Summary
All source code in the article can be found on Github