记录RestTemplate日志
前言
最近在做的项目经常需要与微信服务器交互,选用了restTemplate作为请求工具。但由于对微信API的不熟悉,导致请求时经常会出错,由此产生了记录restTemplate请求与响应日志的想法。
过程
接口实现
实际上RestTemplate已经为我们提供了拦截器的接口,我们只需要实现该接口,并实现其中的指定方法,就可以获取到RestTemplate的每一次请求。
@FunctionalInterface
public interface ClientHttpRequestInterceptor {
ClientHttpResponse intercept(HttpRequest var1, byte[] var2, ClientHttpRequestExecution var3) throws IOException;
}
日志记录
拦截器记录日志具体代码
当发起请求时,RestTemplate会将本次请求的请求对象、请求参数及请求执行者,传递给拦截器。
我们只需在请求调用执行前,将请求对象,及请求参数进行记录,然后在使用执行者发送请求,再将本次请求返回的响应进行记录即可。
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import sun.net.www.protocol.http.HttpURLConnection;
import java.io.*;
@Slf4j
public class LoggingClientHttpRequestInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
traceRequest(request, body);
ClientHttpResponse response = execution.execute(request, body);
traceResponse(response);
return response;
}
//记录请求体及请求参数
private void traceRequest(HttpRequest request, byte[] body) throws IOException {
log.info("===========================request begin================================================");
log.info("URI : {}", request.getURI());
log.info("Method : {}", request.getMethod());
log.info("Headers : {}", request.getHeaders());
log.info("Request body: {}", new String(body, "UTF-8"));
log.info("==========================request end================================================");
}
//记录响应信息及响应数据
private void traceResponse(ClientHttpResponse response) throws IOException {
StringBuilder inputStringBuilder = new StringBuilder();
// 获取响应流
InputStream body = response.getBody();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(body));
String line = bufferedReader.readLine();
while (line != null) {
inputStringBuilder.append(line);
inputStringBuilder.append('\n');
line = bufferedReader.readLine();
}
log.info("============================response begin==========================================");
log.info("Status code : {}", response.getStatusCode());
log.info("Status text : {}", response.getStatusText());
log.info("Headers : {}", response.getHeaders());
log.info("Response body: {}", inputStringBuilder.toString());//WARNING: comment out in production to improve performance
log.info("=======================response end=================================================");
}
}
将拦截器添加进RestTemplate中
@Configuration
public class OtherConfig {
@Bean
public RestTemplate restTemplate(ClientHttpRequestFactory factory, @Value("${spring.http.encoding.charset}")String charset){
RestTemplate restTemplate = new RestTemplate(factory);
// 获取转换器集合
List<HttpMessageConverter<?>> messageConverters = restTemplate.getMessageConverters();
if (messageConverters==null){
messageConverters=new ArrayList<>();
}
//新建一个json转换器,用来处理请求与响应的json数据
GsonHttpMessageConverter converter = new GsonHttpMessageConverter();
// 设置该转换器支持类型为所有
converter.setSupportedMediaTypes(Collections.singletonList(MediaType.ALL));
// 设置该转换器字符集
converter.setDefaultCharset(Charset.forName(charset));
messageConverters.add(converter);
restTemplate.setMessageConverters(messageConverters);
// 新建拦截器,并添加进restTemplate实例中
restTemplate.getInterceptors().add(new LoggingClientHttpRequestInterceptor());
return restTemplate;
}
@Bean
public ClientHttpRequestFactory simpleClientHttpRequestFactory(){
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(15000);
factory.setReadTimeout(5000);
return factory;
}
}
至此,记录RestTemplate请求日志的需求就已完成
重读响应流
流的重读
java中的部分流是可以进行重读的,只需调用该流实例的mark()方法,传入int:readlimit值,即可在流此时的位置添加书签。
mark()中的readlimit,表示在mark()之后,只要该流被继续读取的长度在此int值范围内,此mark就有效,超出则失效。
标记书签后,只需调用该流的reset()方法,就可以将流重回最后一次mark的位置上,实现流的重复使用。
我们可以调用流的markSupported(),查看此流实例是否支持mark()重读。
RestTemplate所提供的InputStream实现类:HttpURLConnection$HttpInputStream直接继承于FilterInputStream,FilterInputStream又继承于InputStream,该类的markSupported()返回false,表不支持,所以无法对响应流进行重读。
但我们在RestTemplate的日志记录时,已经将该流读取,之后业务再用到响应流数据时,再读取流时会报空指针异常,所以此时只能考虑放弃记录响应数据,或是重写RestTemplate底层,将RestTemplate的响应流替换为可重读的InputStream子类。
底层替换
底层的实现的替换,可以使用java的反射机制,强制将可重读流写入RestTemplate响应流的位置上即可。
由于最近有在学习javassist,对在运行中class字节码的重写,所以我在这里用了重构字节码对象的方式,进行流的替换。
实现
依赖导入
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.25.0-GA</version>
</dependency>
然后只需要在拦截器的构造器中,对指定class的方法进行重写即可
public LoggingClientHttpRequestInterceptor() {
try {
// 获取class字节池
final ClassPool classPool = ClassPool.getDefault();
//获取需要修改的class对象,RestTemplate底层使用的是SimpleClientHttpResponse
CtClass ctClass = classPool.get("org.springframework.http.client.SimpleClientHttpResponse");
// 获取 获取流的方法
CtMethod getBody = ctClass.getDeclaredMethod("getBody");
// 重写该方法的具体实现
String methodBody ="{if(responseStream==null){java.io.InputStream errorStream = this.connection.getErrorStream();\n" +
"this.responseStream = new java.io.BufferedInputStream(errorStream != null ? errorStream : this.connection.getInputStream());" +
"}return this.responseStream;}";
// 覆盖原方法方法
getBody.setBody(methodBody);
//将编辑后的对象转换成真正的class对象
ctClass.toClass();
} catch (NotFoundException e) {
e.printStackTrace();
} catch (CannotCompileException e) {
e.printStackTrace();
}
}
替换流后,我们还需修改拦截器中,记录响应数据的方法。
实际上只需要在获取流后,进行mark即可
StringBuilder inputStringBuilder = new StringBuilder();
BufferedInputStream body = (BufferedInputStream) response.getBody();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(body));
body.mark(1024);
String line = bufferedReader.readLine();
while (line != null) {
inputStringBuilder.append(line);
inputStringBuilder.append('\n');
line = bufferedReader.readLine();
}
body.reset();
到这里,就完成了对RestTemplate的日志记录。