使用RestTemplate,顯示請求信息,響應信息

使用RestTemplate,顯示請求信息,響應信息

這裏不講怎麼用RestTemplate具體細節用法,就是一個學習中的過程記錄java

一個簡單的例子web

public class App {
    public static void main(String[] args) {
        String url = "https://api.uixsj.cn/hitokoto/get";
        RestTemplate restTemplate = new RestTemplate();
        String body = restTemplate.getForObject(url, String.class);
        System.out.println(body);
    }
}

運行結果:spring

image-20201130123152314

❓:如今我想看看他的請求頭,請求參數,響應頭,響應體的詳細信息是怎麼樣子的,這樣也方便之後檢查請求參數是否完整,響應正確與否。api

通過蒐集資料發現ClientHttpRequestInterceptor知足需求,因而就有了下面的代碼緩存

打印請求頭/響應頭

public class App {
    public static void main(String[] args) {
        String url = "https://api.uixsj.cn/hitokoto/get";
        RestTemplate restTemplate = new RestTemplate();
        // 加上攔截器打印將請求請求,響應信息打印出來
        restTemplate.setInterceptors(Collections.singletonList(new LoggingInterceptor()));
        String body = restTemplate.getForObject(url, String.class);
        System.out.println(body);
    }
}

@Slf4j
class LoggingInterceptor implements ClientHttpRequestInterceptor {

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

    /**
     * 顯示請求相關信息
     * @param request
     * @param body
     */
    private void displayRequest(HttpRequest request, byte[] body) {
        log.debug("====request info====");
        log.debug("URI         : {}", request.getURI());
        log.debug("Method      : {}", request.getMethod());
        log.debug("Req Headers : {}", this.headersToString(request.getHeaders()));
        log.debug("Request body: {}", body == null ? "" : new String(body, StandardCharsets.UTF_8));
    }

    /**
     * 顯示響應相關信息
     * @param response
     * @throws IOException
     */
    private void displayResponse(ClientHttpResponse response) throws IOException {
        StringBuilder inputStringBuilder = new StringBuilder();
        try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(response.getBody(), StandardCharsets.UTF_8))) {
            String line = bufferedReader.readLine();
            while (line != null) {
                inputStringBuilder.append(line);
                inputStringBuilder.append('\n');
                line = bufferedReader.readLine();
            }
        }
        log.debug("====response info====");
        log.debug("Status code  : {}", response.getStatusCode());
        log.debug("Status text  : {}", response.getStatusText());
        log.debug("Resp Headers : {}", headersToString(response.getHeaders()));
        log.debug("Response body: {}", inputStringBuilder.toString());
    }

    /**
     * 將Http頭信息格式化處理
     * @param httpHeaders
     * @return
     */
    private String headersToString(HttpHeaders httpHeaders) {
        if (Objects.isNull(httpHeaders)) {
            return "[]";
        }
        return httpHeaders.entrySet().stream()
                .map(entry -> {
                    List<String> values = entry.getValue();
                    return "\t" + entry.getKey() + ":" + (values.size() == 1 ?
                            "\"" + values.get(0) + "\"" :
                            values.stream().map(s -> "\"" + s + "\"").collect(Collectors.joining(", ")));
                })
                .collect(Collectors.joining(", \n", "\n[\n", "\n]\n"));
    }
}

運行結果:app

執行過程當中會報錯,具體錯誤信息是ide

Exception in thread "main" org.springframework.web.client.ResourceAccessException: I/O error on GET request for "https://api.uixsj.cn/hitokoto/get": stream is closed; nested exception is java.io.IOException: stream is closed

這裏報錯信息是流已關閉,報錯是在添加LoggingInterceptor後出現的,那就是新加代碼引發的。在看看LoggingInterceptor的實現,什麼地方操做了流,而且關閉了流。學習

LoggingInterceptor.displayResponse這個方法裏面,爲了讀取響應體操做了流response.getBody()ui

try (...) {
}
// 這個try塊結束後就把流給關了

註釋掉代碼中流操做相關代碼,再次運行沒有錯誤信息。因該是在攔截器後,RestTemplate也須要操做了response.getBody()的流(廢話)。this

Response body 不能讀第二次這個很要命呀

問題找到了,初步的想到了幾種解決

  1. 改寫代碼,不close流,讀取完以後再reset
  2. 代理一下ClientHttpResponse每次調用getBody都返回一個新的輸入流

解決不能重複讀Response body

方法一:讀取完後不關閉流

// 略...
InputStream responseBody = response.getBody();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(responseBody, StandardCharsets.UTF_8));
String line = bufferedReader.readLine();
while (line != null) {
    inputStringBuilder.append(line);
    inputStringBuilder.append('\n');
    line = bufferedReader.readLine();
}
responseBody.reset();
// 略...

很遺憾,執行後依舊有錯誤

Exception in thread "main" org.springframework.web.client.ResourceAccessException: I/O error on GET request for "https://api.uixsj.cn/hitokoto/get": mark/reset not supported; nested exception is java.io.IOException: mark/reset not supported

說的很清楚,不支持mark/reset方法。很明顯了它不容許隨意修改讀取定位。沒辦法只轉爲第二種方法了。

方法二:代理,每次都返回一個新的流

  1. 靜態代理實現ClientHttpResponse接口,好在ClientHttpResponse實現的接口數量很少,實現的代碼以下。
@Slf4j
class LoggingInterceptor implements ClientHttpRequestInterceptor {

    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
        displayRequest(request, body);
        ClientHttpResponse response = execution.execute(request, body);
        // 包裝代理一下
        response = new ClientHttpResponseWrapper(response);
        displayResponse(response);
        return response;
    }

    /**
     * 顯示請求相關信息
     * @param request
     * @param body
     */
    private void displayRequest(HttpRequest request, byte[] body) {
        // 略...
    }

    /**
     * 顯示響應相關信息
     * @param response
     * @throws IOException
     */
    private void displayResponse(ClientHttpResponse response) throws IOException {
        StringBuilder inputStringBuilder = new StringBuilder();
        try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(response.getBody(), StandardCharsets.UTF_8))) {
            String line = bufferedReader.readLine();
            while (line != null) {
                inputStringBuilder.append(line);
                inputStringBuilder.append('\n');
                line = bufferedReader.readLine();
            }
        }
        // 略...
    }

    /**
     * 將Http頭信息格式化處理
     * @param httpHeaders
     * @return
     */
    private String headersToString(HttpHeaders httpHeaders) {
        // 略...
    }

    private class ClientHttpResponseWrapper implements ClientHttpResponse {
        private ClientHttpResponse clientHttpResponse;
        private byte[] body;

        public ClientHttpResponseWrapper(ClientHttpResponse clientHttpResponse) {
            this.clientHttpResponse = clientHttpResponse;
        }

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

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

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

        @Override
        public void close() {
            this.clientHttpResponse.close();
        }

        /**
         * 緩存body每次返回一個新的輸入流
         * @return
         * @throws IOException
         */
        @Override
        public InputStream getBody() throws IOException {
            if (Objects.isNull(this.body)) {
                this.body = StreamUtils.copyToByteArray(this.clientHttpResponse.getBody());
            }
            return new ByteArrayInputStream(this.body == null ? new byte[0] : this.body);
        }

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

運行效果:

image-20201130132734043

代碼運行沒問題,可是總感受代碼寫出來笨笨的,要重寫這麼多用不着的方法,看着不舒服,換個寫法。

  1. 動態代理
public class App {
    public static void main(String[] args) {
        String url = "https://api.uixsj.cn/hitokoto/get";
        RestTemplate restTemplate = new RestTemplate();
        restTemplate.setInterceptors(Collections.singletonList(new LoggingInterceptor()));
        String body = restTemplate.getForObject(url, String.class);
        System.out.println(body);
    }

}

@Slf4j
class LoggingInterceptor implements ClientHttpRequestInterceptor {

    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
        displayRequest(request, body);
        ClientHttpResponse response = execution.execute(request, body);
        // 包裝代理一下
        response = (ClientHttpResponse) Proxy.newProxyInstance(response.getClass().getClassLoader(), new Class[]{ClientHttpResponse.class}, new ClientHttpResponseHandler(response));
        displayResponse(response);
        return response;
    }

    /**
     * 顯示請求相關信息
     * @param request
     * @param body
     */
    private void displayRequest(HttpRequest request, byte[] body) {
        // 略......
    }

    /**
     * 顯示響應相關信息
     * @param response
     * @throws IOException
     */
    private void displayResponse(ClientHttpResponse response) throws IOException {
        StringBuilder inputStringBuilder = new StringBuilder();
        try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(response.getBody(), StandardCharsets.UTF_8))) {
            String line = bufferedReader.readLine();
            while (line != null) {
                inputStringBuilder.append(line);
                inputStringBuilder.append('\n');
                line = bufferedReader.readLine();
            }
        }
        // 略......
    }

    /**
     * 將Http頭信息格式化處理
     * @param httpHeaders
     * @return
     */
    private String headersToString(HttpHeaders httpHeaders) {
        // 略......
    }

    private static class ClientHttpResponseHandler implements InvocationHandler {
        private static final String methodName = "getBody";
        private ClientHttpResponse clientHttpResponse;
        private byte[] body;

        ClientHttpResponseHandler(ClientHttpResponse clientHttpResponse) {
            this.clientHttpResponse = clientHttpResponse;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            if (StringUtils.equals(methodName, method.getName())) {
                if (Objects.isNull(this.body)) {
                    this.body = StreamUtils.copyToByteArray(this.clientHttpResponse.getBody());
                }
                return new ByteArrayInputStream(this.body == null ? new byte[0] : this.body);
            }
            return method.invoke(this.clientHttpResponse, args);
        }
    }
}

運行結果:

image-20201130140437927

總結

  • 使用RestTemplate想要顯示詳細請求信息,和響應信息
  • 添加攔截器
  • 攔截器中操做InputSteam致使流關閉,不能重複讀Response body
  • 嘗試不關閉流,重置流的方案失敗
  • 使用代理解決 Response body 不能讀第二次讀的問題
    • 靜態代理(能夠重複讀Response body了)
    • 動態代理(能夠重複讀Response body了)
  • 靜態代理的代碼有點囉嗦,動態代理又有點不夠味,看來『茴』字很差寫呀
相關文章
相關標籤/搜索