在实际开发中我们可能经常会使用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();
}
}