В этом мире микросервисов мы всегда подчеркиваем передачу любого HTTP-запроса через уровень шлюза API/сервиса, который соединяет несколько микросервисов, с минимальным требованием ведения журнала всех запросов и ответов каждого сервиса для более четкой видимости.
Мы можем рассмотреть возможность написания нашего обратного прокси-слоя в следующем сценарии.
1. Предположим, служба API написала что-то на другом языке, таком как «PHP» или «Python», в котором нам нужно частично преобразовать запрос.
2. Добавить авторизацию или реализовать повторную попытку, когда служба не может ответить на запрос.
3. Зарегистрировать все запросы и ответы, проходящие через этот обратный прокси-уровень, а затем отправить их в некоторый стек журналов, например ELK
4. Внедрить нашу пользовательскую логику отслеживания для распространения службы.
5. Или Изобретите колесо заново просто для удовольствия
Давайте начнем с очень простого примера простого уровня переадресации запросов, который регистрирует запросы и ответы.
В этом примере я использую Springboot и его встроенный сервер Tomcat. И Spring Spring Retry Dependency для реализации логики повторных попыток при сбое запросов.
StartPoint:
Давайте Wring Контроллер Springboot перехватывает все запросы и передает их прокси-сервисному классу.
package com.ashrithgn.example.proxyApp;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.net.URISyntaxException; import java.util.UUID;
@RestController public class ProxyController { @Autowired ProxyService service; @RequestMapping("/**") public ResponseEntity<String> sendRequestToSPM(@RequestBody(required = false) String body, HttpMethod method, HttpServletRequest request, HttpServletResponse response) throws URISyntaxException { return service.processProxyRequest(body,method,request,response, UUID.randomUUID().toString()); } }
ProxySerivce
Когда мы получим запрос от контроллера, Simple RestTemplate выполнит HTTP-запрос к требуемому домену.
И это класс, который можно расширить до наших требований, например выполнение пользовательского ведения журнала или, как в этом примере, реализация повторных попыток при сбоях в HTTP-вызовах.
package com.ashrithgn.example.proxyApp;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.ThreadContext; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; import org.springframework.http.client.BufferingClientHttpRequestFactory; import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.http.client.SimpleClientHttpRequestFactory; import org.springframework.retry.annotation.Backoff; import org.springframework.retry.annotation.Recover; import org.springframework.retry.annotation.Retryable; import org.springframework.stereotype.Service; import org.springframework.web.client.HttpStatusCodeException; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.net.URI; import java.net.URISyntaxException; import java.util.Collections; import java.util.Enumeration;
@Service public class ProxyService { String domain = "example.com"; private final static Logger logger = LogManager.getLogger(ProxyService.class);
@Retryable(exclude = { HttpStatusCodeException.class}, include = Exception.class, backoff = @Backoff(delay = 5000, multiplier = 4.0), maxAttempts = 4) public ResponseEntity<String> processProxyRequest(String body, HttpMethod method, HttpServletRequest request, HttpServletResponse response, String traceId) throws URISyntaxException { ThreadContext.put("traceId", traceId); String requestUrl = request.getRequestURI();
//log if required in this line URI uri = new URI("https", null, domain, -1, null, null, null);
// replacing context path form urI to match actual gateway URI uri = UriComponentsBuilder.fromUri(uri) .path(requestUrl) .query(request.getQueryString()) .build(true).toUri();
HttpHeaders headers = new HttpHeaders(); Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) { String headerName = headerNames.nextElement(); headers.set(headerName, request.getHeader(headerName)); }
headers.set("TRACE", traceId); headers.remove(HttpHeaders.ACCEPT_ENCODING);
HttpEntity<String> httpEntity = new HttpEntity<>(body, headers); ClientHttpRequestFactory factory = new BufferingClientHttpRequestFactory(new SimpleClientHttpRequestFactory()); RestTemplate restTemplate = new RestTemplate(factory); try {
ResponseEntity<String> serverResponse = restTemplate.exchange(uri, method, httpEntity, String.class); HttpHeaders responseHeaders = new HttpHeaders(); responseHeaders.put(HttpHeaders.CONTENT_TYPE, serverResponse.getHeaders().get(HttpHeaders.CONTENT_TYPE)); logger.info(serverResponse); return serverResponse;
} catch (HttpStatusCodeException e) { logger.error(e.getMessage()); return ResponseEntity.status(e.getRawStatusCode()) .headers(e.getResponseHeaders()) .body(e.getResponseBodyAsString()); }
}
@Recover public ResponseEntity<String> recoverFromRestClientErrors(Exception e, String body, HttpMethod method, HttpServletRequest request, HttpServletResponse response, String traceId) { logger.error("retry method for the following url " + request.getRequestURI() + " has failed" + e.getMessage()); logger.error(e.getStackTrace()); throw new RuntimeException("There was an error trying to process you request. Please try again later"); } }
Основной класс приложения
package com.ashrithgn.example.proxyApp;
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.retry.annotation.EnableRetry;
@SpringBootApplication @EnableRetry public class ProxyAppApplication {
public static void main(String[] args) { SpringApplication.run(ProxyAppApplication.class, args); }
}
POM для справки
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.6.1</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.ashrithgn.example</groupId> <artifactId>proxyApp</artifactId> <version>0.0.1-SNAPSHOT</version> <name>proxyApp</name> <description>Demo project for Spring Boot</description> <properties> <java.version>17</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
<dependency> <groupId>org.springframework.retry</groupId> <artifactId>spring-retry</artifactId> <version>1.2.5.RELEASE</version> </dependency>
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> <version>5.2.8.RELEASE</version> </dependency>
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
</project>