每个请求的RestTemplate设置超时

时间:2018-01-20 22:36:49

标签: spring spring-boot resttemplate

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

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

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

5 个答案:

答案 0 :(得分:2)

选项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:在断路器中包裹呼叫

如果您正在呼叫外部服务,则可能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注释方法并尝试其他服务调用。或者,您可以提供合理的默认值。

答案 1 :(得分:2)

我假设您希望在响应时间过长的情况下读取超时。

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

为实现这一目标,您可以改用<Value>,它内置支持超时和取消等异步操作。

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

AsyncRestTemplate

答案 2 :(得分:2)

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

我们已经在项目的任何地方使用RestTemplate,我们现在无法承受重构,http客户端切换会随之发生。

现在我最终得到了一个RestTemplate池解决方案,我创建了一个名为RestTemplateManager的类,我全权负责创建模板并汇集它们。此管理器具有按服务和readTimeout分组的模板的本地缓存。想象一下具有以下结构的缓存哈希映射:

  

ServiceA | 1000 - &gt; RestTemplate

     

ServiceA | 3000 - &gt; RestTemplate

     

ServiceB | 1000 - &gt; RestTemplate

密钥中的数字是readTimeout(以毫秒为单位)(密钥可以适应以后支持多于readTimeout)。因此,当ServiceA请求具有1000ms读取超时的模板时,管理器将返回缓存的实例,如果它不存在则将创建并返回。

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

直到我有时间抛弃RestTemplate并使用更合适的解决方案。

答案 3 :(得分:1)

我自己遇到了这个问题,到处搜索并没有提出我认为效果很好的解决方案。这是我背后的解决方案和思考过程。

您可以使用HttpComponentsClientHttpRequestFactory为RestTemplate设置超时。每次发出请求时,内部都会在requestFactory上调用createRequest函数。在这里,具有超时的RequestConfig和一些请求特定的属性被设置。然后在HttpContext上设置此RequestConfig。以下是尝试构建此RequestConfig和HttpContext的步骤(按顺序)

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

我认为,所有这三种解决方案都可以围绕它们构建解决方案。我认为,最简单,最可靠的解决方案是围绕#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,并且据我所知是线程安全的。

答案 4 :(得分:0)

类似于@Todd的答案

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

让我们缓存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的方法,但是它将扩展到任何类型的读取超时,并将使用对象的缓存,可能是flyweight-cum-factory模式。如果我错了,请纠正我。