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
所采取的步骤(按顺序)
- 调用 HttpComponentsClientHttpRequestFactory 中的 createHttpContext 函数,默认情况下不执行任何操作并且 returns 为 null。
- 从 HttpUriRequest 获取 RequestConfig(如果它存在)并将其添加到 HttpContext。
- 调用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 的方法,但是这将扩展到任何类型的读取超时,并将使用对象缓存,可能是享元兼工厂模式。如果我错了,请纠正我。
我有一个 @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
所采取的步骤(按顺序)- 调用 HttpComponentsClientHttpRequestFactory 中的 createHttpContext 函数,默认情况下不执行任何操作并且 returns 为 null。
- 从 HttpUriRequest 获取 RequestConfig(如果它存在)并将其添加到 HttpContext。
- 调用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 的方法,但是这将扩展到任何类型的读取超时,并将使用对象缓存,可能是享元兼工厂模式。如果我错了,请纠正我。