- vừa được xem lúc

Centralized API Documentation trong microservice sử dụng SpringBoot, SpringFox Swagger-UI và Eureka

0 0 26

Người đăng: BSOD

Theo Viblo Asia

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:

  1. Truy xuất tất cả danh sách services đã được đăng ký từ service registry, ở đây là Eureka server
  2. 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
  3. 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
  4. 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:

  1. central-docs-eureka-server: Service registry cung cấp bởi Netflix Eureka
  2. employee-service và person-service: 2 REST service chính đã được tích hợp Swagger-UI
  3. 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

Bình luận

Bài viết tương tự

- vừa được xem lúc

Tổng hợp các bài hướng dẫn về Design Pattern - 23 mẫu cơ bản của GoF

Link bài viết gốc: https://gpcoder.com/4164-gioi-thieu-design-patterns/. Design Patterns là gì. Design Patterns không phải là ngôn ngữ cụ thể nào cả.

0 0 302

- vừa được xem lúc

Học Spring Boot bắt đầu từ đâu?

1. Giới thiệu Spring Boot. 1.1.

0 0 278

- vừa được xem lúc

Cần chuẩn bị gì để bắt đầu học Java

Cần chuẩn bị những gì để bắt đầu lập trình Java. 1.1. Cài JDK hay JRE.

0 0 50

- vừa được xem lúc

Sử dụng ModelMapper trong Spring Boot

Bài hôm nay sẽ là cách sử dụng thư viện ModelMapper để mapping qua lại giữa các object trong Spring nhé. Trang chủ của ModelMapper đây http://modelmapper.org/, đọc rất dễ hiểu dành cho các bạn muốn tìm hiểu sâu hơn. 1.

0 0 194

- vừa được xem lúc

[Java] 1 vài tip nhỏ khi sử dụng String hoặc Collection part 1

. Hello các bạn, hôm nay mình sẽ chia sẻ về mẹo check String null hay full space một cách tiện lợi. Mình sẽ sử dụng thư viện Lớp StringUtils download file jar để import vào thư viện tại (link).

0 0 71

- vừa được xem lúc

Deep Learning với Java - Tại sao không?

Muốn tìm hiểu về Machine Learning / Deep Learning nhưng với background là Java thì sẽ như thế nào và bắt đầu từ đâu? Để tìm được câu trả lời, hãy đọc bài viết này - có thể kỹ năng Java vốn có sẽ giúp bạn có những chuyến phiêu lưu thú vị. DJL là tên viết tắt của Deep Java Library - một thư viện mã ng

0 0 139