Spring - 如果服务 returns 409 HTTP 代码则重试请求

Spring - Retry request if service returns 409 HTTP Code

我有一个 Spring + CXF 应用程序,它在另一台服务器上使用传输 API: Transmission RPC 运行。

根据 Transmission 文档,您需要发送一个在第一次请求时生成的令牌。然后服务器响应 409 http 代码以及包含令牌的 header。此令牌应在所有后续调用中发送:

2.3.1. CSRF Protection Most Transmission RPC servers require a X-Transmission-Session-Id header to be sent with requests, to prevent CSRF attacks. When your request has the wrong id -- such as when you send your first request, or when the server expires the CSRF token -- the Transmission RPC server will return an HTTP 409 error with the right X-Transmission-Session-Id in its own headers. So, the correct way to handle a 409 response is to update your X-Transmission-Session-Id and to resend the previous request.

我正在寻找使用 CXF 过滤器或拦截器的解决方案,它基本上将处理 409 响应并重试添加令牌的初始请求 header。我认为客户可以保留此令牌并在以后的调用中发送它。

我对cxf不是很熟悉所以我想知道这是否可以完成以及如何完成。任何提示都会有所帮助。

谢谢!

这里可以使用spring-retry,现在是一个独立的项目,不再是spring-batch的一部分。

正如所解释的那样 here 重试回调将有助于使用令牌更新另一个调用 header。

这种情况下的伪代码/逻辑如下所示

RetryTemplate template = new RetryTemplate();
Foo foo = template.execute(new RetryCallback<Foo>() {
    public Foo doWithRetry(RetryContext context) {
        /* 
         * 1. Check if RetryContext contains the token via hasAttribute. If available set the header else proceed
         * 2. Call the transmission API 
         * 3.a. If API responds with 409, read the token 
         *    3.a.1. Store the token in RetryContext via setAttribute method
         *    3.a.2. Throw a custom exception so that retry kicks in
         * 3.b. If API response is non 409 handle according to business logic
         * 4. Return result
         */
    }
});

确保使用合理的重试和退避策略配置 RetryTemplate 以避免任何资源争用/意外。

如有任何疑问/障碍,请在评论中告知。

N.B.RetryContext 的实现 RetryContextSupport has the hasAttribute & setAttribute method inherited from Spring core AttributeAccessor

假设您使用的是 Apache CXF JAX RS 客户端,只需创建自定义运行时异常并为其创建 ResponseExceptionMapper 即可轻松实现。所以想法是手动将 409 结果转换为一些异常,然后正确处理它们(在您的情况下重试服务调用)。

请参阅以下代码片段以获得完整的工作示例。

@SpringBootApplication
@EnableJaxRsProxyClient
public class SpringBootClientApplication {
    // This can e stored somewhere in db or elsewhere 
    private static String lastToken = "";

    public static void main(String[] args) {
        SpringApplication.run(SpringBootClientApplication.class, args);
    }

    @Bean
    CommandLineRunner initWebClientRunner(final TransmissionService service) {
        return new CommandLineRunner() {
            @Override
            public void run(String... runArgs) throws Exception {
                try {
                    System.out.println(service.sayHello(1, lastToken));
                // catch the TokenExpiredException get the new token and retry
                } catch (TokenExpiredException ex) {
                    lastToken = ex.getNewToken();
                    System.out.println(service.sayHello(1, lastToken));
                }
             }
        };
    }

    public static class TokenExpiredException extends RuntimeException {
        private String newToken;

        public TokenExpiredException(String token) {
            newToken = token;
        }

        public String getNewToken() {
            return newToken;
        }
     }

     /**
      * This is where the magic is done !!!!
     */
     @Provider
     public static class TokenExpiredExceptionMapper implements ResponseExceptionMapper<TokenExpiredException> {

        @Override
        public TokenExpiredException fromResponse(Response r) {
            if (r.getStatus() == 409) {
                return new TokenExpiredException(r.getHeaderString("X-Transmission-Session-Id"));
            }
            return null;
        }

    }

    @Path("/post")
    public interface TransmissionService {
        @GET
        @Path("/{a}")
        @Produces(MediaType.APPLICATION_JSON_VALUE)
        String sayHello(@PathParam("a") Integer a, @HeaderParam("X-Transmission-Session-Id") String sessionId)
            throws TokenExpiredException;
    }
}