现如今比较火的微服务架构,SpringCloud顺势成为了主流框架,当然SpringCloud并不是一个框架,而是一个框架的集合。不管是否为微服务,难免会有程序之间的调用,当然zipkin可以帮助收集时间数据,解决在微服务架构下的延迟问题,如何详细记录请求以及返回的信息变得比较重要。
RestTemplate 对rest复杂请求封装简单的调用方法,默认JDK facilities。当然你也可以 通过setRequestFactory属性切换到不同的HTTP源,比如Apache HttpComponents、Netty和OkHttp 具体可以看ClientHttpRequestFactory
实现。当然本质上做是封装的匣子,使用者方便很多。
以上均是本章节的废话,转入正题如何优雅的记录请求以及返回信息。当然考虑一个问题得有切入点,知道@LoadBalanced
注解的当然很容易理解
Annotation to mark a RestTemplate bean to be configured to use a LoadBalancerClient.
其实他也就是个Interceptor 默认LoadBalancerInterceptor
详见LoadBalancerAutoConfiguration
类,它可以做到地址的‘偷梁换柱’,记录请求信息也就没有那么困难。新加一个Interceptor就可以解决,当然这不怎么太完美,众所周知http输入输出流只能读取一次的问题,当然spring比较完美的是做了处理。详看 BufferingClientHttpRequestFactory#createRequest
方法BufferingClientHttpRequestWrapper#executeInternal
,提供对输入/输出流的缓冲。ok前提了解工作已备齐,撸码开始。
LogClientHttpRequestInterceptor.java
/**
* restTemplate log interceptor
*
* @author liweigao
* @date 2019/7/9 下午2:06
*/
@Slf4j(topic = "outgoing")
public class LogClientHttpRequestInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ClientHttpResponse response = execution.execute(request, body);
stopWatch.stop();
StringBuilder resBody = new StringBuilder();
try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(response.getBody(),
Charset.forName("UTF-8")))) {
String line = bufferedReader.readLine();
while (line != null) {
resBody.append(line);
line = bufferedReader.readLine();
}
}
//当然图片、文件一类的就可以省了,打出日志没啥用处,此处的业务逻辑随意撸了,比如header头信息类似于 Accept 、Accept-Encoding 、Accept-Language、Connection 等等
if (request.getHeaders().getContentType() != null && request.getHeaders().getContentType().includes(MediaType.MULTIPART_FORM_DATA)) {
body = new byte[]{};
}
log.info(JSON.toJSONString(RestLog.builder().costTime(stopWatch.getLastTaskTimeMillis()).headers(request.getHeaders()).method(request.getMethodValue())
.reqUrl(request.getURI().toString()).reqBody(new String(body, Charset.forName("UTF-8"))).resBody(resBody.toString()).resStatus(response.getRawStatusCode()).build()));
return response;
}
@Data
@Builder
@SuppressWarnings("rawtypes")
private static class RestLog {
private String reqUrl;
private String method;
private HttpHeaders headers;
private String reqBody;
private String resBody;
private long costTime;
private int resStatus;
}
}
@Bean
public RestTemplate restTemplate(ClientHttpRequestFactory httpRequestFactory) {
RestTemplate restTemplate = new RestTemplate();
/**
* StringHttpMessageConverter 默认使用ISO-8859-1编码,此处修改为UTF-8
*/
List> messageConverters = restTemplate.getMessageConverters();
Iterator> iterator = messageConverters.iterator();
while (iterator.hasNext()) {
HttpMessageConverter> converter = iterator.next();
if (converter instanceof StringHttpMessageConverter) {
((StringHttpMessageConverter) converter).setDefaultCharset(Charset.forName("UTF-8"));
}
}
//Interceptors 添加写的 Interceptors
restTemplate.setInterceptors(Lists.newArrayList(
new LogClientHttpRequestInterceptor()));
//BufferingClientHttpRequestFactory 此处替换为BufferingClientHttpRequestFactory
restTemplate.setRequestFactory(new BufferingClientHttpRequestFactory(httpRequestFactory));
return restTemplate;
}
完成搞定,当然还可以扩展
扩展1 上面指定了@Slf4j(topic = "outgoing")
可以配合logback 输出到指定的文件,收集到es中做数据监控用,毕竟是符合规则的json格式,当然接口间调用媒体类型都是application/json
格式,response对象也可以通过判断contentType进行格式化输出标准的json格式,出去tostring带来的转移符。
扩展2 应用于Interceptors 我们还可以添加相应的请求头信息,方便日志追踪,附赠
ClientHttpRequestInterceptor.java
/**
* requestId interceptor
*
* @author liweigao
* @date 2019/7/9 下午2:52
*/
public class RequestIdInterceptor implements ClientHttpRequestInterceptor {
private static final String REQUEST_ID = "X-Request-Id";
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body,
ClientHttpRequestExecution execution) throws IOException {
if (CollectionUtils.isEmpty(request.getHeaders().get(REQUEST_ID))) {
request.getHeaders().set(REQUEST_ID, UUID.randomUUID().toString());
}
return execution.execute(request, body);
}
}
初始化restTemplate对象时restTemplate.setInterceptors(Lists.newArrayList(new RequestIdInterceptor(), new LogClientHttpRequestInterceptor()));
即可。
那么问题来了,很多个Interceptors 执行的顺序怎么保证,如果你能想到这个问题,那么你真是个机灵鬼。话不多说,详看InterceptingHttpAccessor.java#setInterceptors
当然spring也有自己的详看
HttpHeaderInterceptor
当然还是自己撸一把比较爽吧。
/**
* Set the request interceptors that this accessor should use.
* The interceptors will get sorted according to their order
* once the {@link ClientHttpRequestFactory} will be built.
* @see #getRequestFactory()
* @see AnnotationAwareOrderComparator
*/
public void setInterceptors(List interceptors) {
// Take getInterceptors() List as-is when passed in here
if (this.interceptors != interceptors) {
this.interceptors.clear();
this.interceptors.addAll(interceptors);
AnnotationAwareOrderComparator.sort(this.interceptors);
}
}