使用Spring Boot重试失败编写一个反向代理 - Ashrith

21-12-03 banq


在这个微服务世界中,我们总是强调通过 API/服务网关层传递任何 HTTP 请求,该层连接多个微服务,并有一个最低要求,即记录每个服务的所有请求和响应以获得更清晰的可见性。
我们可以考虑在以下场景中编写我们的反向代理层。

  • 1.假设具有API服务像“PHP”或“Python”等另一种语言,我们需要以部分地变换该请求。
  • 2. 添加授权或在服务未能响应请求时实现重试
  • 3. 记录所有流经此反向代理层的请求和响应,稍后推送到某些日志记录堆栈,如 ELK
  • 4. 为服务传播实现我们的自定义跟踪逻辑.
  • 5. 或者重新发明轮子只是为了好玩

在这个示例中,我使用 Springboot 及其嵌入式 Tomcat 服务器。和 Spring Spring retry Dependency 来实现请求失败时的重试逻辑。
Springboot 控制器拦截所有请求并将它们传递给代理服务类:

@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 调用失败时。

@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");
    }
}
主类:
@SpringBootApplication
@EnableRetry
public class ProxyAppApplication {
    public static void main(String[] args) {
        SpringApplication.run(ProxyAppApplication.class, args);
    }
}
pom.xml
<?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>



 

1