Centralized API Documentation trong microservice sử dụng SpringBoot, SpringFox Swagger-UI và Eureka
Bài viết hướng dẫn cách để dễ dàng document nhiều Spring-based REST application bằng cách sử dụng thư viện SpringFox Swagger-UI trong môi trường microservices
1. Problem
Như các bạn đã biết, rất dễ dàng để có thể document một ứng dụng Spring REST Service bằng cách sử dụng thư viện SpringFox Swagger-UI, nhưng có một vấn đề phát sinh khi làm việc trong môi trường nơi mà chúng ta có rất nhiều REST application mà tiêu biểu là môi trường microservices. Hầu hết chúng ta đều quản lý một Swagger-UI riêng biệt cho từng service, điều đó có nghĩa là mỗi service sẽ cần có một endpoint riêng để truy cập Swagger-UI và chúng ta phải có các URL khác nhau cho các service khác nhau.
2. Giải pháp
Để có thể truy cập tất cả các API document từ một URL duy nhất chúng ta có thể implement theo solution như sau:
- Truy xuất tất cả danh sách services đã được đăng ký từ service registry, ở đây là Eureka server
- Với mỗi service đã được đăng ký, pull Swagger Definition JSON về centralized API document service và chứa nó trong local. Cụ thể ở đây sẽ sử dụng Concurrent Map để lưu JSON trong local memory
- Refresh in-memory context định kỳ để tự động cập nhật JSON definition khi các service được thêm mới, cập nhật, hoặc xóa bỏ khỏi service registry
- Cung cấp một endpoint duy nhất để truy cập centralized API document service, nơi tập trung tất cả API document của toàn bộ service trong hệ thống
3. Implementation
Ví dụ chúng ta có một hệ thống microservices gồm 2 services chính là Person Service và Employee Service. Bây giờ hãy cùng implement một document service để tập trung tất cả API document từ Person Service và Employee Service. Mô hình dịch vụ sẽ trông như sau:
- central-docs-eureka-server: Service registry cung cấp bởi Netflix Eureka
- employee-service và person-service: 2 REST service chính đã được tích hợp Swagger-UI
- documentation-service: service thu thập tất cả API document từ các service khác và cung cấp Document UI với một endpoint duy nhất
3.1 SwaggerUIConfiguration
Lớp cấu hình Spring đăng ký phiên bản của SwaggerResourcesProvider, lớp này đọc các tệp JSON swagger-api từ ServiceDefinitionsContext
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
Bổ sung API trả về thông tin JSON definition theo service id từ ServiceDefinitionsContext
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 lưu trữ tất cả JSON definition trong memory
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
Đây là component quan trọng nhất, component này có nhiệm vụ pull tất cả JSON definition từ các service đã được đăng ký trên service registry và lưu trữ chúng trong ServiceDefinitionsContext
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. Tổng kết
Tất cả source code trong bài viết có thể tìm tại Github
All rights reserved