项目中记录请求日志是重要的非业务功能,记录请求出入参是常见的方式。一方面,项目本身提供的Http接口需要记录外部的访问记录。另一方面,项目调用第三方服务的Http接口也需要记录请求日志。
方案一:请求日志可以使用AOP实现,给Controller层定义一个切面,统一记录出入参即可。这种方式在我的上一篇博文已经介绍。
方案二:Http请求在项目中本身可以通过过滤器或拦截器处理,而AOP切面自定义成本较高。一个针对Controller层方法的切面并不能保障请求日志的完整性,还要有针对ControllerAdvice层方法的切面。如果项目中调用了多方服务的Http接口,那么需要定义更多的AOP切面。所以利用Tomcat官方或Spring官方提供的过滤器和拦截器处理请求日志也是不错的方案。
问题:过滤器中记录请求出入参日志,绕不开获取请求流和响应流。众所周知,请求流和响应流只能获取一次。请求处理前,在过滤器中获取请求流,则Spring MVC无法获取RequestBody。请求处理后,在过滤器中获取响应流,则前端无法再次获取ResponseBody。
解决方案:利用Spring提供的ContentCachingRequestWrapper(请求内容缓存类)、ContentCachingResponseWrapper(响应内容缓存类)解决请求流和响应流只能获取一次的问题。这两个类分别是HttpServletRequestWrapper、HttpServletResponseWrapper的子类,他们分别实现了HttpServletRequest、HttpServletResponse接口。
使用:ContentCachingRequestWrapper和ContentCachingResponseWrapper提供了getContentAsByteArray()方法,这个方法可以获取请求流或响应流。但需要注意的是,这个方法也只能调用一次,相当于只缓存了一份,用了就没了。
原理:
流对象 | 去向 |
---|---|
原请求流 | Spring MVC获取 |
缓存请求流 | 过滤器获取 |
原响应流 | 前端获取 |
缓存响应流 | 过滤器获取 |
import cn.hutool.core.date.StopWatch;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.Charset;
@Slf4j
public class LogRequestFilter extends HttpFilter {
@Override
protected void doFilter(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws IOException, ServletException {
// 转换为请求缓存对象和响应缓存对象
ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(request);
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);
// 过滤器放行并计时
StopWatch stopWatch = new StopWatch();
stopWatch.start();
super.doFilter(requestWrapper, responseWrapper, filterChain);
stopWatch.stop();
// 获取需要记录的信息
String ip = requestWrapper.getRemoteHost();
String path = requestWrapper.getRequestURI();
byte[] body = requestWrapper.getContentAsByteArray();
String bodyStr = new String(body, Charset.forName("UTF-8"));
byte[] resp = responseWrapper.getContentAsByteArray();
String respStr = new String(resp, Charset.forName("UTF-8"));
//注意,获取缓存响应流之后默认会同时清楚原响应流,前端将获取不到ResponseBody,要将内容复制回响应体
responseWrapper.copyBodyToResponse();
// 记录请求日志
log.info("request->ip:{},path:{},body:{},exec:{}ms,resp:{}.",
ip, path, bodyStr, stopWatch.getLastTaskTimeMillis(), respStr);
}
}
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean<LogMDCFilter> logFilterRegistration() {
FilterRegistrationBean<LogMDCFilter> registration = new FilterRegistrationBean<>();
// 注入过滤器
registration.setFilter(new LogMDCFilter());
// 拦截规则
registration.addUrlPatterns("/*");
// 过滤器名称
registration.setName("logMDCFilter");
// 过滤器顺序
registration.setOrder(0);
return registration;
}
@Bean
public FilterRegistrationBean<LogRequestFilter> cachingFilterRegistration() {
FilterRegistrationBean<LogRequestFilter> registration = new FilterRegistrationBean<>();
// 注入过滤器
registration.setFilter(new LogRequestFilter());
// 拦截规则
registration.addUrlPatterns("/*");
// 过滤器名称
registration.setName("logRequestFilter");
// 过滤器顺序
registration.setOrder(1);
return registration;
}
}
注:本文依赖Apche httpclient的RestTemplate实现远程第三方服务的Http接口。
import cn.hutool.core.date.StopWatch;
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 java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
@Slf4j
public class LogHttpClientInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
// 手动执行HttpClient请求
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ClientHttpResponse response = execution.execute(request, body);
stopWatch.stop();
// 获取HttpClient请求的请求体和响应体
Charset charset = StandardCharsets.UTF_8;
String bodyStr = new String(body, charset);
String respStr = IOUtils.toString(response.getBody(), charset);
// 记录HttpClient日志
log.info("client->path:{},body:{},exec:{}ms,resp:{}.",
request.getURI(), bodyStr, stopWatch.getLastTaskTimeMillis(), respStr);
return response;
}
}
import com.google.common.collect.Lists;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.BufferingClientHttpRequestFactory;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import java.nio.charset.Charset;
import java.util.List;
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate ChoiceRestTemplate() {
// boot中可使用RestTemplateBuilder.build创建
RestTemplate restTemplate = new RestTemplate();
// StringHttpMessageConverter 默认使用ISO-8859-1编码,此处修改为UTF-8
List<HttpMessageConverter<?>> messageConverters = restTemplate.getMessageConverters();
for (HttpMessageConverter<?> converter : messageConverters) {
if (converter instanceof StringHttpMessageConverter) {
((StringHttpMessageConverter) converter).setDefaultCharset(Charset.forName("UTF-8"));
}
}
// Interceptors 添加写的 Interceptors
restTemplate.setInterceptors(Lists.newArrayList(new LogHttpClientInterceptor()));
// 配置请求工厂 此处替换为BufferingClientHttpRequestFactory
restTemplate.setRequestFactory(new BufferingClientHttpRequestFactory(new HttpComponentsClientHttpRequestFactory()));
return restTemplate;
}
}
参考:
【RestTemplate】统一记录RestTemplate的调用日志
https://blog.csdn.net/hkk666123/article/details/116282274
Spring MVC系列(16)-使用HttpServletRequestWrapper 解决流只能读取一次的问题
https://yunyanchengyu.blog.csdn.net/article/details/122102362
SpringMVC requestBody和responseBody重复获取
https://blog.csdn.net/huangliuyu00/article/details/120517645