RestTemplate之java.io.IOException:stream closed 异常的原因及处理

springboot集成resttemplate时想打印相关请求日志,设置统一的拦截器

拦截器相关代码:


import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

public class RestTemplateLogRecordInterceptor implements ClientHttpRequestInterceptor {

    private final static Logger LOGGER = LoggerFactory.getLogger(RestTemplateLogRecordInterceptor.class);

    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
        traceRequest(request, body);
        ClientHttpResponse response = execution.execute(request, body);
        traceResponse(response);
        return response;
    }

    private void traceRequest(HttpRequest request, byte[] body) throws IOException {
        LOGGER.debug("===========================request begin================================================");
        LOGGER.debug("URI         : {}", request.getURI());
        LOGGER.debug("Method      : {}", request.getMethod());
        LOGGER.debug("Headers     : {}", request.getHeaders());
        LOGGER.debug("Request body: {}", new String(body, "UTF-8"));
        LOGGER.debug("==========================request end================================================");
    }

    private void traceResponse(ClientHttpResponse response) throws IOException {
        StringBuilder inputStringBuilder = new StringBuilder();
        InputStream body = response.getBody();
        try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(body, "UTF-8"))) {
            String line = bufferedReader.readLine();
            while (line != null) {
                inputStringBuilder.append(line);
                inputStringBuilder.append('\n');
                line = bufferedReader.readLine();
            }
        }
        LOGGER.debug("============================response begin==========================================");
        LOGGER.debug("Status code  : {}", response.getStatusCode());
        LOGGER.debug("Status text  : {}", response.getStatusText());
        LOGGER.debug("Headers      : {}", response.getHeaders());
        LOGGER.debug("Response body: {}", inputStringBuilder.toString());
        LOGGER.debug("=======================response end=================================================");
    }

}

配置resttemplate自定义bean


@Configuration
public class RestTemplateConfiguration {

    @Bean
    public RestTemplate getRestTemplate() {
        SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
        //motor服务有的服务响应时间较长,暂定半个小时
        factory.setReadTimeout(1800000);
        factory.setConnectTimeout(1800000);
        RestTemplate restTemplate = new RestTemplate(factory);
        restTemplate.getInterceptors().add(new RestTemplateLogRecordInterceptor());
        restTemplate.getMessageConverters().set(1, new StringHttpMessageConverter(StandardCharsets.UTF_8));
        return restTemplate;
    }

}

经过上述运行后,会出现一个问题,问题如下:

试过很多方法依旧找不到原因,不知道为啥会被close掉
解决方案:怀疑是我加了拦截器的原因,我把拦截器注释掉,则不会报该错误,思考:为啥加了拦截器就会报错呢?
只能一步一步跟踪源码,发现只要加了拦截器,inputStream中的close属性就为true,所以这也是导致为啥调取接口的时候会is close
接下来就是找到为啥inputStream中的close为啥为true?
后来某大神看了这段代码说response和request流只能读取一次,只要读取完就结束了


private void traceResponse(ClientHttpResponse response) throws IOException {
        StringBuilder inputStringBuilder = new StringBuilder();
        InputStream body = response.getBody();
        try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(body, "UTF-8"))) {
            String line = bufferedReader.readLine();
            while (line != null) {
                inputStringBuilder.append(line);
                inputStringBuilder.append('\n');
                line = bufferedReader.readLine();
            }
        }
        LOGGER.debug("============================response begin==========================================");
        LOGGER.debug("Status code  : {}", response.getStatusCode());
        LOGGER.debug("Status text  : {}", response.getStatusText());
        LOGGER.debug("Headers      : {}", response.getHeaders());
        LOGGER.debug("Response body: {}", inputStringBuilder.toString());
        LOGGER.debug("=======================response end=================================================");
    }

我尝试了一次,把该代码注释掉,果然不出所料,是这里的原因,那么接下来就是如何解决流只能读取一次的问题,上网搜索了一番,网上好多人解决都是说用包装类可以解决该问题,将该对象缓存下来,就不会有问题,这也就是Servlet中Fileter的实现,Filter调用链如果不包装,也可能会出现该问题

好了,现在找到解决方案了,那么如何将该返回的ClientHttpResponse 包装一下呢,又不想自己自定义,后来就在org.springframework.http.client下面找了找,发现有类似的类,但是可用的包装类都是protected的,那么就想一下既然他提供了类似的包装类,那么看他内部咋调用类似的包装类,只能一点一点看源码,最后发现一个类BufferingClientHttpRequestWrapper,但是这个也是内部调用类,外部是无法新建的,只要找调用该类的地方,发现BufferingClientHttpRequestFactory类中有相关的实现,而且这个刚好是我需要的,刚好该类是public修饰,有点意思哈,不得不惊叹作者设计模式(工厂模式)玩的很溜啊
既然找到解决方案,代码修改如下:
在RestTemplate定义的时候,将BufferingClientHttpRequestFactory传入,而非传入SimpleClientHttpRequestFactory,最终解决问题


@Bean
    public RestTemplate getRestTemplate() {
        SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
        BufferingClientHttpRequestFactory bufferingClientHttpRequestFactory = new BufferingClientHttpRequestFactory(factory);
        //motor服务有的服务响应时间较长,暂定半个小时
        factory.setReadTimeout(1800000);
        factory.setConnectTimeout(1800000);
        RestTemplate restTemplate = new RestTemplate(bufferingClientHttpRequestFactory);
        restTemplate.getInterceptors().add(new RestTemplateLogRecordInterceptor());
        restTemplate.getMessageConverters().set(1, new StringHttpMessageConverter(StandardCharsets.UTF_8));
        return restTemplate;
    }
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论