RestTemplate 为每个请求设置超时

RestTemplate set timeout per request

我有一个 @Service 有几个方法,每个方法使用不同的网络 api。每个调用都应该有一个自定义读取超时。 拥有一个 RestTemplate 实例并在每个方法中通过工厂更改超时是否是线程安全的

((HttpComponentsClientHttpRequestFactory)restTemplate.getRequestFactory())
.setReadTimeout(customMillis);

我担心的是我正在更改工厂超时,它不像 RequestConfig。考虑到这些方法可能会被多个用户同时调用,这种方法是否是线程安全的?或者每个方法都应该有自己的 RestTemplate?

方案一:多个RestTemplate

如果要更改所创建连接的属性,则每个配置都需要一个 RestTemplate。我最近遇到了同样的问题,并且有两个版本的 RestTemplate,一个用于 "short timeout",一个用于 "long timeout"。在每个组 (short/long) 中,我能够分享 RestTemplate

让您的呼叫更改超时设置,创建连接,并希望最好的是等待发生的竞争条件。我会谨慎行事并创建多个 RestTemplate.

示例:

@Configuration
public class RestTemplateConfigs {
    @Bean("shortTimeoutRestTemplate")
    public RestTemplate shortTimeoutRestTemplate() {
       // Create template with short timeout, see docs.
    }
    @Bean("longTimeoutRestTemplate")
    public RestTemplate longTimeoutRestTemplate() {
       // Create template with short timeout, see docs.
    }
}

然后您可以根据需要将它们连接到您的服务中:

@Service
public class MyService {
    private final RestTemplate shortTimeout;
    private final RestTemplate longTimeout;

    @Autowired
    public MyService(@Qualifier("shortTimeoutRestTemplate") RestTemplate shortTimeout, 
                     @Qualifier("longTimeoutRestTemplate") RestTemplate longTimeout) {
        this.shortTimeout = shortTimeout;
        this.longTimeout = longTimeout;
    }

    // Your business methods here...
}

选项 2:在断路器中包装调用

如果您正在调用外部服务,您可能 should be using a circuit breaker 这样做。 Spring Boot 与 Hystrix 配合得很好,Hystrix 是断路器模式的一种流行实现。使用 hystrix,您可以控制调用的每个服务的回退和超时。

假设服务 A 有两种选择:1) 便宜但有时速度慢 2) 价格昂贵但速度快。您可以使用 Hystrix 放弃 Cheap/Slow 并在真正需要时使用 Expensive/Fast。或者你可以没有备份,只是让 Hystrix 调用一个提供合理默认值的方法。

未经测试的示例:

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

@Service
public class MyService {
    private final RestTemplate restTemplate;

    public BookService(RestTemplate rest) {
        this.restTemplate = rest;
    }

    @HystrixCommand(
        fallbackMethod = "fooMethodFallback",
        commandProperties = { 
            @HystrixProperty(
                 name = "execution.isolation.thread.timeoutInMilliseconds", 
                 value="5000"
            )
        }
    )
    public String fooMethod() {
        // Your logic here.
        restTemplate.exchange(...); 
    }

    public String fooMethodFallback(Throwable t) {
        log.error("Fallback happened", t);
        return "Sensible Default Here!"
    }
}

后备方法也有选项。您可以使用 @HystrixCommand 注释 that 方法并尝试另一个服务调用。或者,您可以只提供一个合理的默认值。

我假设您想要读取超时以防响应时间过长。

一种可能的解决方案是,如果请求未在给定时间内完成,则通过取消请求自行实施超时。

为此,您可以改用 AsyncRestTemplate,它内置了对超时和取消等异步操作的支持。

这使您可以更好地控制每个请求的超时时间,例如:

ListenableFuture<ResponseEntity<Potato>> future =
                asyncRestTemplate.getForEntity(url, Potato.class);

ResponseEntity<Potato> response = future.get(5, TimeUnit.SECONDS);

RestTemplate 初始化之后从工厂更改超时只是一个等待发生的竞争条件(就像 Todd )。 RestTemplate 的真正设计目的是使用预配置的超时来构建,并且这些超时在初始化后保持不变。如果您使用 Apache HttpClient 那么是的,您可以为每个请求设置一个 RequestConfig,我认为这是正确的设计。

我们已经在我们的项目中到处使用 RestTemplate,我们目前真的负担不起重构,一个 http 客户端切换将随之而来。

现在我得到了一个 RestTemplate 池解决方案,我创建了一个名为 RestTemplateManager 的 class 并赋予它创建模板和池化它们的所有责任。该管理器具有按服务和 readTimeout 分组的模板本地缓存。想象一下具有以下结构的缓存哈希图:

ServiceA|1000 -> RestTemplate

ServiceA|3000 -> RestTemplate

ServiceB|1000 -> RestTemplate

key中的数字是以毫秒为单位的readTimeout(稍后可以修改key以支持超过readTimeout)。因此,当 ServiceA 请求一个具有 1000 毫秒读取超时的模板时,管理器将 return 缓存的实例,如果它不存在,它将被创建并 returned.

在这种方法中,我避免了预先定义 RestTemplates,我只需要向上面的管理器请求一个 RestTemplate。这也使初始化保持在最低限度。

在我有时间放弃 RestTemplate 并使用更合适的解决方案之前,这将一直有效。

我自己刚遇到这个问题,四处搜索并没有找到任何我认为行之有效的解决方案。这是我的解决方案和背后的思考过程。

您使用 HttpComponentsClientHttpRequestFactory 为 RestTemplate 设置了超时。每次发出请求时,它都会在内部调用 requestFactory 上的 createRequest 函数。 RequestConfig 在这里设置了超时和一些特定于请求的属性。然后在 HttpContext 上设置此 RequestConfig。以下是尝试构建此 RequestConfig 和 HttpContext

所采取的步骤(按顺序)
  1. 调用 HttpComponentsClientHttpRequestFactory 中的 createHttpContext 函数,默认情况下不执行任何操作并且 returns 为 null。
  2. 从 HttpUriRequest 获取 RequestConfig(如果它存在)并将其添加到 HttpContext。
  3. 调用HttpComponentsClientHttpRequestFactory中的createRequestConfig函数,内部从HttpClient获取RequestConfig,与requestFactory内部构建的RequestConfig合并,添加到HttpContext中。 (默认情况下会发生这种情况)

在我看来,所有这 3 个都可以围绕它们构建解决方案。我相信最简单和最可靠的解决方案是围绕#1 构建解决方案。我最终创建了自己的 HttpComponentsRequestFactory 并覆盖了 createHttpContext 函数,该函数在内部具有逻辑以查看请求 URI 的路径是否与我为该 pathPattern 提供的指定超时匹配的 pathPattern。

public class PathTimeoutHttpComponentsClientHttpRequestFactory extends HttpComponentsClientHttpRequestFactory {
  private List<PathPatternTimeoutConfig> pathPatternTimeoutConfigs = new ArrayList<>();

  protected HttpContext createHttpContext(HttpMethod httpMethod, URI uri) {
    for (PathPatternTimeoutConfig config : pathPatternTimeoutConfigs) {
      if (httpMethod.equals(config.getHttpMethod())) {
        final Matcher matcher = config.getPattern().matcher(uri.getPath());
        if (matcher.matches()) {
          HttpClientContext context = HttpClientContext.create();
          RequestConfig requestConfig = createRequestConfig(getHttpClient());  // Get default request config and modify timeouts as specified
          requestConfig = RequestConfig.copy(requestConfig)
              .setSocketTimeout(config.getReadTimeout())
              .setConnectTimeout(config.getConnectionTimeout())
              .setConnectionRequestTimeout(config.getConnectionRequestTimeout())
              .build();
          context.setAttribute(HttpClientContext.REQUEST_CONFIG, requestConfig);
          return context;
        }
      }
    }

    // Returning null allows HttpComponentsClientHttpRequestFactory to continue down normal path for populating the context
    return null;
  }

  public void addPathTimeout(HttpMethod httpMethod, String pathPattern, int connectionTimeout, int connectionRequestTimeout, int readTimeout) {
    Assert.hasText(pathPattern, "pathPattern must not be null, empty, or blank");
    final PathPatternTimeoutConfig pathPatternTimeoutConfig = new PathPatternTimeoutConfig(httpMethod, pathPattern, connectionTimeout, connectionRequestTimeout, readTimeout);
    pathPatternTimeoutConfigs.add(pathPatternTimeoutConfig);
  }

  private class PathPatternTimeoutConfig {
    private HttpMethod httpMethod;
    private String pathPattern;
    private int connectionTimeout;
    private int connectionRequestTimeout;
    private int readTimeout;
    private Pattern pattern;

    public PathPatternTimeoutConfig(HttpMethod httpMethod, String pathPattern, int connectionTimeout, int connectionRequestTimeout, int readTimeout) {
      this.httpMethod = httpMethod;
      this.pathPattern = pathPattern;
      this.connectionTimeout = connectionTimeout;
      this.connectionRequestTimeout = connectionRequestTimeout;
      this.readTimeout = readTimeout;
      this.pattern = Pattern.compile(pathPattern);
    }

    public HttpMethod getHttpMethod() {
      return httpMethod;
    }

    public String getPathPattern() {
      return pathPattern;
    }

    public int getConnectionTimeout() {
      return connectionTimeout;
    }

    public int getConnectionRequestTimeout() { return connectionRequestTimeout; }

    public int getReadTimeout() {
      return readTimeout;
    }

    public Pattern getPattern() {
      return pattern;
    }
  }
}

然后,如果您愿意,可以使用默认超时创建此请求工厂的实例,并像这样为特定路径指定自定义超时

@Bean
public PathTimeoutHttpComponentsClientHttpRequestFactory requestFactory() {
  final PathTimeoutHttpComponentsClientHttpRequestFactory factory = new PathTimeoutHttpComponentsClientHttpRequestFactory();
  factory.addPathTimeout(HttpMethod.POST, "\/api\/groups\/\d+\/users\/\d+", 1000, 1000, 30000); // 30 second read timeout instead of 5
  factory.setConnectionRequestTimeout(1000);
  factory.setConnectTimeout(1000);
  factory.setReadTimeout(5000);
  return factory;
}

@Bean
public RestTemplate restTemplate() {
  final RestTemplate restTemplate = new RestTemplate();
  restTemplate.setRequestFactory(requestFactory());
  ...
  return restTemplate;
}

这种方法是高度可重用的,不需要为每个唯一的超时创建单独的 RestTemplate,据我所知它是线程安全的。

类似于@Todd 的回答

我们可以这样考虑:RestTemplate一旦构建就可以认为是线程安全的。 Is RestTemplate thread safe?

让我们有一个 RestTemplates 的缓存,就像一个工厂。

由于不同的方法require的超时时间不同,我们可以在需要的时候懒惰的获取指定的rest模板。

class GlobalClass{
    ....
    private static Map<Integer, RestTemplate> timeoutToTemplateMap =
          new ConcurrentHashMap<>();
    ...
      public static getRestTemplate(Integer readTimeout){
       return timeoutToTemplateMap.computeIfAbsent(readTimeout, 
                            key->Utility.createRestTemplate(key)
      }
    }

@Service
.....
serviceMethodA(Integer readTimeout){
    GlobalClass.getRestTemplate(readTimeout).exchange()
}
....



@Utility
.....
static createRestTemplate(Integer timeout){
   HttpComponentsClientHttpRequestFactory factory = getFactory() 
   factory.setReadTimeout(timeout);
   return new RestTemplate(factory);
  
   // rest template is thread safe once created as no public methods change 
   // the fields of the rest template
}
.....

这类似于 Todd 的方法,但是这将扩展到任何类型的读取超时,并将使用对象缓存,可能是享元兼工厂模式。如果我错了,请纠正我。