使用回退流PushbackInputStream,在http客户端拦截器中检测http响应是否是文本格式并打印响应内容

在实际开发中我们可能经常会使用http客户端调用外部第三方的接口。为了后期排错、解决bug,一般都需要添加统一的拦截器,在日志中打印请求和响应参数。而且一般会有这种需求,只打印文本参数、而不打印如文件、图片这类二进制流,毕竟二进制流打印出来也看不懂、没啥意义。
在这里插入图片描述

在拦截器中的输入出入流一般都是有状态的,只能读写一次,在读/写后再次尝试读写直接抛出异常。那么如果我们在拦截器中直接读取出IO流中的参数,那么实际的业务方就没法用这个流数据了、业务方会抛出I/O异常。这里我们需要一种读取了部分I/O流数据还能回退到起始点的I/O流,幸运的是还有这种流,它就是PushbackInputStream,这是我在RestTemplate的相关类MessageBodyClientHttpResponseWrapper中发现的。

另外我们要怎么检测http I/O流是文本格式还是二进制格式尼? 第一想到的是利用http header中的Content-Type字段来判断,但是它的类型太多了、没法列举完,并不是一个理想方案。 我在ok http3中发现他是通过读取流中的前16个UTF-8字符来判断的。
在这里插入图片描述

以下代码是我配置的拦截器,它可以打印响应内容、自动检测响应格式

 @Bean
    public RestTemplate restTemplate(@Autowired(required = false) @Qualifier("logRequestInterceptor") ClientHttpRequestInterceptor logInterceptor,
                                     ClientHttpRequestFactory clientHttpRequestFactory) {
        RestTemplate restTemplate = new RestTemplate(clientHttpRequestFactory);
        if (logInterceptor != null) {
            restTemplate.setInterceptors(Collections.singletonList(logInterceptor));
        }
        return restTemplate;
    }

    @Bean
    public ClientHttpRequestInterceptor logRequestInterceptor(
            @Value("${spring.rest-client.need-print-headers:false}") boolean needPrintHeaders) {
        return new LogHttpRequestInterceptor(needPrintHeaders);
    }

    static class LogHttpRequestInterceptor implements ClientHttpRequestInterceptor {

        private final Logger log = LoggerFactory.getLogger(getClass());

        private final boolean needPrintHeaders;

        LogHttpRequestInterceptor(boolean needPrintHeaders) {
            this.needPrintHeaders = needPrintHeaders;
        }

        @Override
        public ClientHttpResponse intercept(HttpRequest request, byte[] reqBody, ClientHttpRequestExecution execution) throws IOException {
            if (needPrintHeaders) {
                log.info("-->Http request START, method={}, url={}, headers={}", request.getMethod(), request.getURI(), request.getHeaders());
            }

            if(isPlainText(request.getHeaders().getContentType())){
                log.info("-->Http request START, method={}, url={}, text body==>{}", request.getMethod(), request.getURI(), new String(reqBody, StandardCharsets.UTF_8));
            } else {
                log.info("-->Http request START, method={}, url={}, binary {} bytes body omitted", request.getMethod(), request.getURI(), reqBody.length);
            }

            long start = System.currentTimeMillis();
            ClientHttpResponse response = execution.execute(request, reqBody);
            long totalTime = System.currentTimeMillis() - start;

            if (needPrintHeaders) {
                log.info("-->Http request END, method={}, url={}, headers={}", request.getMethod(), request.getURI(), response.getHeaders());
            }

            ClientHttpResponseWrapper responseWrapper = new ClientHttpResponseWrapper(response);
            if (responseWrapper.isPlainText()) {
                byte[] bodyBytes = StreamUtils.copyToByteArray(responseWrapper.getBody());
                responseWrapper.getBody().close();
                String responseText = new String(bodyBytes, StandardCharsets.UTF_8);
                //响应内容可能是unicode的转义字符
                if(responseText.contains("\\u")){
                    responseText = StringEscapeUtils.unescapeJava(responseText);
                }
                log.info("-->Http request END, method={}, url={}, http status={}, total time={}ms, response text body==>{}",
                        request.getMethod(), request.getURI(), response.getStatusCode(), totalTime, responseText);
                /*
                 * responseWrapper.getBody()是PushbackInputStream,上面的StreamUtils.copyToByteArray
                 * 已将PushbackInputStream中数据读完,这里需要返回一个新的I/O流给业务方,即这个'ByteArrayBodyResponse'
                 */
                return new ByteArrayBodyResponse(response, bodyBytes);
            } else {
                log.info("-->Http request END, method={}, url={}, http status={}, total time={}ms, response binary body",
                        request.getMethod(), request.getURI(), response.getStatusCode(), totalTime);
                return responseWrapper;
            }
        }

        private static boolean isPlainText(@Nullable MediaType reqContentType) {
            if (reqContentType == null) {
                return true;
            }
            return !MediaType.MULTIPART_FORM_DATA.getType().equals(reqContentType.getType());
        }
    }

    static class ByteArrayBodyResponse implements ClientHttpResponse {
        private final ClientHttpResponse response;

        private final ByteArrayInputStream body;

        public ByteArrayBodyResponse(ClientHttpResponse response, byte[] bodyBytes) {
            this.response = response;
            this.body = new ByteArrayInputStream(bodyBytes);
        }

        @Override
        public HttpStatus getStatusCode() throws IOException {
            return response.getStatusCode();
        }

        @Override
        public int getRawStatusCode() throws IOException {
            return response.getRawStatusCode();
        }

        @Override
        public String getStatusText() throws IOException {
            return response.getStatusText();
        }

        @Override
        public void close() {
            response.close();
        }

        @Override
        public InputStream getBody() {
            return body;
        }

        @Override
        public HttpHeaders getHeaders() {
            return response.getHeaders();
        }
    }

    static class ClientHttpResponseWrapper implements ClientHttpResponse {

        private final ClientHttpResponse response;

        private final PushbackInputStream pushbackInputStream;

        ClientHttpResponseWrapper(ClientHttpResponse response) throws IOException {
            this.response = response;
            pushbackInputStream = new PushbackInputStream(response.getBody(), 64);
        }

        //只能调用一次
        boolean isPlainText() throws IOException {
            //尝试读取64个字节,(一个utf8最多占4个字节,16个utf-8字符最多占用64个字节)
            byte[] readBytes = new byte[64];
            int readCnt = pushbackInputStream.read(readBytes);
            if (readCnt == -1) {
                return true;
            }
            //将读取的数据再归还回去,否则再次读取I/O流会出现异常
            pushbackInputStream.unread(readBytes, 0, readCnt);

            InputStreamReader reader = new InputStreamReader(new ByteArrayInputStream(readBytes, 0, readCnt),
                    StandardCharsets.UTF_8);
            int codePoint;
            int i = 0;
            //对读取的16个字符进行检测
            while ((codePoint = reader.read()) != -1 && i < 16) {
                i++;
                if (Character.isISOControl(codePoint) && !Character.isWhitespace(codePoint)) {
                    return false;
                }
            }
            return true;
        }

        @Override
        public HttpStatus getStatusCode() throws IOException {
            return response.getStatusCode();
        }

        @Override
        public int getRawStatusCode() throws IOException {
            return response.getRawStatusCode();
        }

        @Override
        public String getStatusText() throws IOException {
            return response.getStatusText();
        }

        @Override
        public void close() {
            response.close();
        }

        @Override
        public InputStream getBody() {
            return pushbackInputStream;
        }

        @Override
        public HttpHeaders getHeaders() {
            return response.getHeaders();
        }
    }
  • 5
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值