弹簧 RestTemplate 连接超时不起作用

2022-09-05 00:03:26

我正在尝试配置外部 Web 服务调用时的超时。我在我的服务中通过Spring Rest模板调用外部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();
}

如果有的话,请纠正我的理解。


答案 1

以下是与设置相关的内容。connectTimeout

案例 - 未知主机

如果您有一个无法访问的主机(例如:),那么您将尽快收到。 没有任何超时。使用 解析主机。http://blablablabla/v1/timeoutUnknownHostExceptionAbstractPlainSocketImpl :: connect() :: !addr.isUnresolved() :: throw UnknownHostExceptionInetAddress.getByName(<host_name>)

情况 - 未知端口

如果您有一个可访问的主机,但无法完成连接,那么您会收到 - 尽快。这似乎发生在从 调用 的本机方法中。不考虑超时。ConnectExceptionConnection refused: connectDualStackPlainSocketImpl :: static native void waitForConnect(int fd, int timeout) throws IOExceptionDualStackPlainSocketImpl :: socketConnect()

代理?如果使用代理,情况可能会发生变化。具有可访问的代理可能会遇到超时。

相关问题检查此答案,因为与您遇到的案例有关。

将同一域映射到多个 IP 地址的 DNS 轮循机制将导致客户端连接到每个 IP,直到找到一个 IP。因此,将为列表中不起作用的每个IP添加自己的惩罚。阅读本文了解更多信息。connectTimeout()

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

测试 connectTimeout 您可以参考此答案,了解具有阻止套接字连接完成的终结点从而获取超时的各种方法。选择解决方案时,您可以在 spring-boot 中创建集成测试,以验证您的实现。这可以类似于用于测试 的下一个测试,只是在这种情况下,可以将 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. 测试开始时将运行的弹簧启动应用程序

@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);
    }
}

为什么不用 来模拟使用 ,来代替请求工厂。因此,将替换任何设置。因此,使用真实应用进行超时测试可能是这里唯一的选择。MockRestServiceServerRestTemplateRestTemplate

注意:另请查看这篇关于配置的文章,其中还包括超时配置。RestTemplate


答案 2

推荐