1. 程式人生 > >使用RestTemplate,顯示請求資訊,響應資訊

使用RestTemplate,顯示請求資訊,響應資訊

# 使用RestTemplate,顯示請求資訊,響應資訊 這裡不講怎麼用RestTemplate具體細節用法,就是一個學習中的過程記錄 一個簡單的例子 ```java 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); } } ``` 執行結果: ![image-20201130123152314](https://img2020.cnblogs.com/blog/2186622/202012/2186622-20201201222630654-405560632.png) ❓:現在我想看看他的請求頭,請求引數,響應頭,響應體的詳細資訊是怎麼樣子的,這樣也方便以後檢查請求引數是否完整,響應正確與否。 經過蒐集資料發現`ClientHttpRequestInterceptor`滿足需求,於是就有了下面的程式碼 ## 列印請求頭/響應頭 ```java 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 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")); } } ``` 執行結果: ![](https://img2020.cnblogs.com/blog/2186622/202012/2186622-20201201222658288-2066109236.png) 執行過程中會報錯,具體錯誤資訊是 ```java 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()`, ```java try (...) { } // 這個try塊結束後就把流給關了 ``` 註釋掉程式碼中流操作相關程式碼,再次執行沒有錯誤資訊。因該是在攔截器後,RestTemplate也需要操作了`response.getBody()`的流(廢話)。 > Response body 不能讀第二次這個很要命呀 問題找到了,初步的想到了幾種解決 1. 改寫程式碼,不`close`流,讀取完之後再`reset`流 2. 代理一下`ClientHttpResponse`每次呼叫`getBody`都返回一個新的輸入流 ## 解決不能重複讀Response body ### 方法一:讀取完後不關閉流 ```java // 略... 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(); // 略... ``` 很遺憾,執行後依舊有錯誤 ```java 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`實現的介面數量不多,實現的程式碼如下。 ```java @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](https://img2020.cnblogs.com/blog/2186622/202012/2186622-20201201222658679-1935149575.png) > 程式碼執行沒問題,但是總感覺程式碼寫出來笨笨的,要重寫這麼多用不著的方法,看著不舒服,換個寫法。 2. 動態代理 ```java 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](https://img2020.cnblogs.com/blog/2186622/202012/2186622-20201201222658919-552733011.png) # 總結 - 使用RestTemplate想要顯示詳細請求資訊,和響應資訊 - 新增攔截器 - 攔截器中操作InputSteam導致流關閉,不能重複讀Response body - 嘗試不關閉流,重置流的方案失敗 - 使用代理解決 Response body 不能讀第二次讀的問題 - 靜態代理(可以重複讀Response body了) - 動態代理(可以重複讀Response body了) - 靜態代理的程式碼有點囉嗦,動態代理又有點不夠味,看來『茴』字不