Spring RestTemplate连接超时无效

14

我正在尝试配置外部Web服务调用的超时时间。我在我的服务中使用Spring Rest Template调用外部Web服务。

为了进行连接超时测试,外部Web服务已停止,应用服务器也已关闭。

我已经将超时时间配置为10秒,但不幸的是,在一秒钟后我收到了连接被拒绝的异常。

try {   
    final RestTemplate restTemplate = new RestTemplate();

    ((org.springframework.http.client.SimpleClientHttpRequestFactory)
        restTemplate.getRequestFactory()).setReadTimeout(1000*10);

    ((org.springframework.http.client.SimpleClientHttpRequestFactory)
        restTemplate.getRequestFactory()).setConnectTimeout(1000*10);

    HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);

    HttpEntity<String> entity = new HttpEntity<String>(reqJSON, headers);

    ResponseEntity<String> response = restTemplate.exchange(wsURI, HttpMethod.POST, entity, String.class);

    String premiumRespJSONStr = response.getBody();
}
请纠正我的理解,如果有的话。

刚在我的环境中测试了你的代码,运行良好。你的服务应该进行测试。为了实现这一点,我的建议是将连接超时值最小化,改为10毫秒。 - Vaibs
1
@ Vaibs,当外部服务正常时,setReadTimeout可以工作。当外部服务不可用时,setConnectTimeout无法工作。也就是说,在一秒后就会出现异常,而不是等待我配置的10秒钟。 - Easy2DownVoteHard2Ans
1
如果您的服务已经停止,为什么REST模板还要轮询10秒钟呢?它会直接抛出一些异常/错误。 - Vaibs
2
@Easy2DownVoteHard2Ans 有两种情况:1)远程服务器已经启动,但是在 connectTimeout 时间内无法建立连接;2)服务器已经关闭,因此无法访问。对于前者,connectTimeout 应该可以解决问题,对于后者,这样做没有意义,因为您的网络客户端已经知道它是不可达的,等待 connectTimeout 告诉您这一点是没有意义的。 - Edwin Dalorzo
@EdwinDalorzo。感谢您的评论。我已经测试了10.255.255.1,并且连接超时按预期工作。 - Easy2DownVoteHard2Ans
显示剩余4条评论
1个回答

46
以下是与connectTimeout设置相关的内容。
情况 - 未知主机
如果您有一个无法访问的主机(例如:http://blablablabla/v1/timeout),则会尽快收到UnknownHostException。主机使用InetAddress.getByName(<host_name>)解析,AbstractPlainSocketImpl::connect()::!addr.isUnresolved()::throw UnknownHostException没有任何超时。
情况 - 未知端口
如果您有一个可达的主机但无法进行连接,则尽快收到ConnectException - Connection refused: connect。似乎这发生在本地方法DualStackPlainSocketImpl::static native void waitForConnect(int fd, int timeout) throws IOException中,该方法由DualStackPlainSocketImpl::socketConnect()调用。超时未得到尊重。
代理?如果使用代理,情况可能会发生变化。如果有可达代理,则可能会出现超时。

相关问题 检查 this answer 是否与您遇到的情况有关。

DNS 轮询 如果将同一域名映射到多个 IP 地址,则客户端将连接到每个 IP,直到找到一个可用的。因此,connectTimeout() 将为列表中不起作用的每个 IP 添加其自己的惩罚。阅读 this article 以了解更多信息。

结论 如果您想获得 connectTimeout,则可能需要实现自己的重试逻辑或使用代理。

测试connectTimeout,您可以参考各种方法的答案,以获得防止套接字连接完成从而获取超时的端点。选择解决方案后,您可以在spring-boot中创建一个集成测试来验证您的实现。这可以类似于用于测试readTimeout的下一个测试,只是对于这种情况,URL可以更改为防止套接字连接的URL。

测试readTimeout

为了测试readTimeout,首先需要建立连接,因此服务需要运行。然后可以提供一个端点,对于每个请求,返回一个具有较长延迟的响应。

以下内容可在spring-boot中执行,以创建一个集成测试:

1. 创建测试

@RunWith(SpringRunner.class)
@SpringBootTest(
        webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
        classes = { RestTemplateTimeoutConfig.class, RestTemplateTimeoutApplication.class }
)
public class RestTemplateTimeoutTests {

    @Autowired
    private RestOperations restTemplate;

    @LocalServerPort
    private int port;

    @Test
    public void resttemplate_when_path_exists_and_the_request_takes_too_long_throws_exception() {
        System.out.format("%s - %s\n", Thread.currentThread().getName(), Thread.currentThread().getId());

        Throwable throwable = catchThrowable(() ->
                restTemplate.getForEntity(String.format("http://localhost:%d/v1/timeout", port), String.class));

        assertThat(throwable).isInstanceOf(ResourceAccessException.class);
        assertThat(throwable).hasCauseInstanceOf(SocketTimeoutException.class);
    }
}

2. 配置 RestTemplate

@Configuration
public class RestTemplateTimeoutConfig {

    private final int TIMEOUT = (int) TimeUnit.SECONDS.toMillis(10);

    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate(getRequestFactory());
    }

    private ClientHttpRequestFactory getRequestFactory() {
        HttpComponentsClientHttpRequestFactory factory =
                new HttpComponentsClientHttpRequestFactory();

        factory.setReadTimeout(TIMEOUT);
        factory.setConnectTimeout(TIMEOUT);
        factory.setConnectionRequestTimeout(TIMEOUT);
        return factory;
    }
}

3. 在测试开始时运行的Spring Boot应用程序

@SpringBootApplication
@Controller
@RequestMapping("/v1/timeout")
public class RestTemplateTimeoutApplication {

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

    @GetMapping()
    public @ResponseStatus(HttpStatus.NO_CONTENT) void getDelayedResponse() throws InterruptedException {
        System.out.format("Controller thread = %s - %s\n", Thread.currentThread().getName(), Thread.currentThread().getId());
        Thread.sleep(20000);
    }
}

配置RestTemplate的替代方法

配置现有的RestTemplate

@Configuration
public class RestTemplateTimeoutConfig {

    private final int TIMEOUT = (int) TimeUnit.SECONDS.toMillis(10);

    // consider that this is the existing RestTemplate
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

    // this will change the RestTemplate settings and create another bean
    @Bean
    @Primary
    public RestTemplate newRestTemplate(RestTemplate restTemplate) {
        SimpleClientHttpRequestFactory factory =
                (SimpleClientHttpRequestFactory) restTemplate.getRequestFactory();

        factory.setReadTimeout(TIMEOUT);
        factory.setConnectTimeout(TIMEOUT);

        return restTemplate;
    }
}

使用RequestConfig配置新的RestTemplate

@Configuration
public class RestTemplateTimeoutConfig {

    private final int TIMEOUT = (int) TimeUnit.SECONDS.toMillis(10);

    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate(getRequestFactoryAdvanced());
    }

    private ClientHttpRequestFactory getRequestFactoryAdvanced() {
        RequestConfig config = RequestConfig.custom()
                .setSocketTimeout(TIMEOUT)
                .setConnectTimeout(TIMEOUT)
                .setConnectionRequestTimeout(TIMEOUT)
                .build();

        CloseableHttpClient client = HttpClientBuilder
                .create()
                .setDefaultRequestConfig(config)
                .build();

        return new HttpComponentsClientHttpRequestFactory(client);
    }
}

使用MockRestServiceServer替代请求工厂来进行模拟测试,为什么不采用该方法呢?因此,任何RestTemplate的设置都将被替换。因此,在超时测试中使用真实应用程序可能是唯一的选择。
注意:还要检查this article有关RestTemplate配置的文章,其中包括超时配置。

1
非常感谢您提供如此全面详细的答案。这是一个非常有用的指南,可以帮助我们处理和测试Spring连接超时问题。 - Danny Bullis
1
很好的回答。在最新的RequestConfig版本中,setSocketTimeout是否等同于第一版的setReadTimeout? - Chris

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