问题
I have a @Service
with several methods, each method consumes a different web api. Each call should have a custom read timeout.
Is it thread-safe to have one RestTemplate instance and change the timeout via the factory in each method like so
((HttpComponentsClientHttpRequestFactory)restTemplate.getRequestFactory())
.setReadTimeout(customMillis);
My concern is that I'm changing the timeout on the factory and its not like a RequestConfig
. Will this approach be thread-safe considering these methods might get called by multiple users at the same time? Or each method should have its own RestTemplate
?
回答1:
Option 1: More than one RestTemplate
If you are changing the properties of the connections created, you will need to have one RestTemplate
per configuration. I had this very same problem recently and had two versions of RestTemplate
, one for "short timeout" and one for "long timeout". Within each group (short/long) I was able to share that RestTemplate
.
Having your calls change the timeout settings, create a connection, and hope for the best is a race condition waiting to happen. I would play this safe and create more than one RestTemplate
.
Example:
@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.
}
}
And then you can wire them in to your services as needed:
@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...
}
Option 2: Wrap calls in a Circuit Breaker
If you are calling out to external services, you probably should be using a circuit breaker for this. Spring Boot works well with Hystrix, a popular implementation of the circuit breaker pattern. Using hystrix you can control the fallback for each service you call out to, and the timeouts.
Suppose you have two options for Service A: 1) Cheap but sometimes slow 2) Expensive but fast. You can use Hystrix to give up on Cheap/Slow and use Expensive/Fast when you really need to. Or you can have no backup and just have Hystrix call a method that provides a sensible default.
Untested example:
@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!"
}
}
The fallback method has options too. You could annotate that method with @HystrixCommand
and attempt another service call. Or, you could just provide a sensible default.
回答2:
I assume you want read timeouts in case the response takes too long.
A possible solution would be to implement the timeout yourself by canceling the request if it hasn't completed in the given time.
To achieve this, you could use an AsyncRestTemplate
instead, which has builtin support for async operations like timeout and cancellation.
This gives you more control over the timeout for each request, example:
ListenableFuture<ResponseEntity<Potato>> future =
asyncRestTemplate.getForEntity(url, Potato.class);
ResponseEntity<Potato> response = future.get(5, TimeUnit.SECONDS);
回答3:
Changing timeouts from the factory after RestTemplate
initialization is just a race condition waiting to occur (Like Todd explained). RestTemplate
was really designed to be built with pre-configured timeouts and for those timeouts to stay untouched after initialization. If you use Apache HttpClient
then yes you can set a RequestConfig
per request and that is the proper design in my opinion.
We are already using RestTemplate
everywhere in our project and we can't really afford the refactoring at the moment, that an http client switch would ensue.
For now I ended up with a RestTemplate
pooling solution, I created a class called RestTemplateManager and I gave it all responsibility of creating templates and pooling them. This manager have a local cache of templates grouped by service and readTimeout. Imagine a cache hashmap with the following structure:
ServiceA|1000 -> RestTemplate
ServiceA|3000 -> RestTemplate
ServiceB|1000 -> RestTemplate
The number in the key is the readTimeout in milliseconds (key can be adapted to support more than readTimeout later on). So when ServiceA requests a template with 1000ms read timeout, the manager will return the cached instance, if it doesn't exist it will be created and returned.
In this approach I saved myself from pre-defining RestTemplates, I only have to request a RestTemplate from the manager above. This also keeps initializations at a minimum.
This shall do until I have the time to ditch RestTemplate and use a more appropriate solution.
回答4:
I just encountered this issue myself and searching around didn't bring up any solutions I felt worked well. Here's my solution and thought process behind it.
You set timeouts for RestTemplate by using HttpComponentsClientHttpRequestFactory. Every time you make a request, internally it calls the createRequest function on the requestFactory. It is in here the RequestConfig which has the timeouts and some request specific properties are set. This RequestConfig is then set on the HttpContext. Below are the steps (in order) that are taken to try and build up this RequestConfig and HttpContext
- Calls the createHttpContext function within HttpComponentsClientHttpRequestFactory which by default does nothing and returns null.
- Gets the RequestConfig if it exists from the HttpUriRequest and adding it to the HttpContext.
- Calls the createRequestConfig function within HttpComponentsClientHttpRequestFactory which internally gets the RequestConfig from the HttpClient, merges it with the RequestConfig built internally in the requestFactory and adds it to the HttpContext. (By default this is what happens)
In my opinion, all 3 of these can have solutions built around them. I believe that the easiest and most robust solution is by building a solution around #1. I ended up creating my own HttpComponentsRequestFactory and just overrided the createHttpContext function which had logic internally to see if the request URI's path matched a pathPattern I provided with specified timeouts for that 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;
}
}
}
You can then create an instance of this request factory with a default timeout if you'd like and specify custom timeouts for specific paths like this
@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;
}
This approach is highly reusable, doesn't require creating separate RestTemplate's for each unique timeout and as far as I can tell is threadsafe.
回答5:
Similar to @Todd's answer
We can consider this: RestTemplate once constructed can be considered thread safe. Is RestTemplate thread safe?
Let's have a cache of RestTemplates, something like a factory.
As different methods require have different timeouts, we can get the specified rest template lazily when required.
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
}
.....
This is similar to Todd's method but this will extend to any kind of read timeouts and will use a cache of objects, may be flyweight-cum-factory pattern. Correct me on this if i am wrong.
来源:https://stackoverflow.com/questions/48361794/resttemplate-set-timeout-per-request