使用RestTemplate,顯示請求資訊,響應資訊
這裡不講怎麼用RestTemplate具體細節用法,就是一個學習中的過程記錄
一個簡單的例子
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);
}
}
執行結果:
❓:現在我想看看他的請求頭,請求引數,響應頭,響應體的詳細資訊是怎麼樣子的,這樣也方便以後檢查請求引數是否完整,響應正確與否。
經過蒐集資料發現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"));
}
}
執行結果:
執行過程中會報錯,具體錯誤資訊是
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()
,
try (...) {
}
// 這個try塊結束後就把流給關了
註釋掉程式碼中流操作相關程式碼,再次執行沒有錯誤資訊。因該是在攔截器後,RestTemplate也需要操作了response.getBody()
的流(廢話)。
Response body 不能讀第二次這個很要命呀
問題找到了,初步的想到了幾種解決
- 改寫程式碼,不
close
流,讀取完之後再reset
流 - 代理一下
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
方法。很明顯了它不允許隨意修改讀取定位。沒辦法只轉為第二種方法了。
方法二:代理,每次都返回一個新的流
- 靜態代理實現
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();
}
}
}
執行效果:
程式碼執行沒問題,但是總感覺程式碼寫出來笨笨的,要重寫這麼多用不著的方法,看著不舒服,換個寫法。
- 動態代理
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);
}
}
}
執行結果:
總結
- 使用RestTemplate想要顯示詳細請求資訊,和響應資訊
- 新增攔截器
- 攔截器中操作InputSteam導致流關閉,不能重複讀Response body
- 嘗試不關閉流,重置流的方案失敗
- 使用代理解決 Response body 不能讀第二次讀的問題
- 靜態代理(可以重複讀Response body了)
- 動態代理(可以重複讀Response body了)
- 靜態代理的程式碼有點囉嗦,動態代理又有點不夠味,看來『茴』字不好寫呀