RestTemplate设置每个请求的超时时间

10

我有一个使用了多个方法的@Service,每个方法都会调用不同的web api。每次调用都需要自定义读取超时时间。 在每个方法中通过工厂更改超时时间,使用一个RestTemplate实例是否线程安全?

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

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

5个回答

9

选项1:使用多个RestTemplate

如果您要更改创建的连接的属性,则需要每个配置一个RestTemplate。最近我也遇到了这个问题,我有两个版本的RestTemplate,一个用于“短超时”,另一个用于“长超时”。在每个组(短/长)内,我能够共享那个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: 使用熔断器封装调用

如果你正在调用外部服务,你可能应该使用一个 熔断器。Spring Boot 与 Hystrix 良好兼容,它是熔断器模式的流行实现。使用 Hystrix,你可以控制每个服务的回退和超时处理。

假设你有两种选择 Service 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注释该方法并尝试另一个服务调用。或者,您可以提供一个合理的默认值。


我同意你所说的,但我仍然对这个设计有疑虑,因为它很快就会变得混乱。我有6个消耗Web API的服务,每个服务都需要一个唯一的RestTemplate..现在随着超时要求的增加,我将不得不为每个服务创建3个RestTemplates,例如ServiceARestTemplateHighTimeout``ServiceARestTemplateMidTimeout``ServiceARestTemplateLowTimeout..并且这应该复制到每个6个服务..这是18个RestTemplates,如果需要新的唯一超时,我将不得不创建更多的RestTemplates。对此有何想法? - prettyvoid
@prettyvoid 是的,那听起来确实很痛苦!不过你考虑过像 Hystrix 这样的断路器来包装你的调用并管理超时吗?我已经更新了我的答案,展示了一个例子。 - Todd
你的断路器建议很有启发性,我们可以在项目中进行一些重构时将这个设计纳入其中。请查看我的帖子以了解我最终的结果。非常感谢你的帮助和教导我一些东西 :) - prettyvoid
@Todd,创建一个restTemplate的包装器怎么样?我需要一个单独的restTemplate,但是在Spring上下文创建期间,我无法从数据库中读取超时值。我的想法是创建一个bean来包装restTemplate,然后替换包装器中的实例。当然,这个包装器将有适当的同步。 - Wojciech Piotrowiak

6
RestTemplate初始化之后更改工厂中的超时时间只会导致竞争条件的发生(就像Todd在这里所解释的那样)。 RestTemplate 的设计初衷是使用预配置的超时时间来构建,并且这些超时时间在初始化后不会被更改。如果您使用Apache HttpClient,那么是可以为每个请求设置一个RequestConfig的,我认为这是正确的设计方式。
我们已经在项目中到处使用RestTemplate,而且我们目前无法承担进行http客户端切换所需的重构。
现在,我采用了一个RestTemplate池化的解决方案,我创建了一个名为RestTemplateManager的类,将所有创建模板和池化它们的责任都交给它。这个管理器有一个本地缓存的模板,按服务和读取超时分组。想象一下一个带有以下结构的缓存哈希表:

ServiceA|1000 -> RestTemplate

ServiceA|3000 -> RestTemplate

ServiceB|1000 -> RestTemplate

键中的数字是以毫秒为单位的读取超时时间(稍后可以调整键以支持超过读取超时时间)。因此,当ServiceA请求具有1000ms读取超时的模板时,管理器将返回缓存的实例,如果不存在,则将创建并返回。
通过这种方法,我避免了预定义RestTemplates,我只需要从上述管理器请求一个RestTemplate。这也使初始化保持最小化。
在我有时间放弃RestTemplate并使用更合适的解决方案之前,这个方案就可以了。

4

我假设您希望在响应时间过长时使用读取超时。

一种可能的解决方案是通过在给定时间内取消请求来自行实现超时。

为了实现这一点,您可以使用内置支持异步操作如超时和取消的AsyncRestTemplate

这使您对每个请求的超时有更多的控制,例如:

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

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

2
在Spring 5中,AsyncRestTemplate已被弃用,推荐使用org.springframework.web.reactive.function.client.WebClient - xonya

3
我刚遇到了这个问题,搜索并没有找到任何解决方案。下面是我的解决方案和思考过程。
使用HttpComponentsClientHttpRequestFactory为RestTemplate设置超时时间。每次发出请求时,它会在requestFactory上调用createRequest函数。在此函数中设置了RequestConfig,其中包含超时时间和一些特定于请求的属性。然后将该RequestConfig设置在HttpContext上。以下是尝试构建此RequestConfig和HttpContext的步骤(按顺序):
1. 在HttpComponentsClientHttpRequestFactory内部调用createHttpContext函数,默认情况下不执行任何操作并返回null。 2. 从HttpUriRequest获取RequestConfig(如果存在),并将其添加到HttpContext中。 3. 在HttpComponentsClientHttpRequestFactory内部调用createRequestConfig函数,该函数从HttpClient获取RequestConfig,将其与requestFactory内部构建的RequestConfig合并,并将其添加到HttpContext中。(默认情况下会执行此操作)
我认为这三个步骤都可以有针对性的解决方案。我相信最简单且最可靠的解决方案是围绕第一个步骤构建解决方案。我最终创建了自己的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,据我所知,它是线程安全的。


1

与@Todd的答案类似

我们可以考虑这个:RestTemplate一旦构建完成就可以被视为线程安全的。RestTemplate是否线程安全?

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

由于不同方法需要不同的超时时间,因此我们可以在需要时惰性地获取指定的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的方法,但可以扩展到任何类型的读取超时,并使用对象缓存,可能是轻量级组合工厂模式。如果我有错,请纠正我。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接