SpringBoot中Servlet提供的Filter与Gin的Middleware洋葱模型是很相似的,本质上可以认为是责任链设计模式。通过Filter,我们可以获取到请求对象ServletRequest
、响应对象ServletResponse
。
利用Filter的特性,我们可以实现一个Filter,获取Request和Response,并整合写入到日志中去。
本文基于 Spring Boot 2.6.x 实现。
1、新建一个filter类
由于ServletRequest
中的InputStream
是一次性用品,所以我们需要使用Spring提供的ContentCachingRequestWrapper
将request对象包装起来。ServletResponse
也是类似的原理,需要用ContentCachingResponseWrapper
包装起来。这样我们就可以在请求处理完毕后得到其中的数据。
package com.gentleltd.todo965.backend.infra.filter;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Slf4j
public class LogFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
// 包装 request 和 response
ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper((HttpServletRequest) servletRequest);
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper((HttpServletResponse) servletResponse);
filterChain.doFilter(requestWrapper, responseWrapper);
// 记录日志 (方案一,request和response整合到同一条日志中,截断超长文本)
ObjectMapper mapper = new ObjectMapper();
ObjectNode root = mapper.createObjectNode();
root.putIfAbsent("request", mapper.readTree(requestWrapper.getContentAsByteArray()));
root.putIfAbsent("response", mapper.readTree(responseWrapper.getContentAsByteArray()));
log.info(Strings.left(mapper.writeValueAsString(root), 10240)); // 利用Jackson将请求响应数据整合到同一条json结构中,截断超长文本
// 记录日志(方案二,request和response分别作为单独的span整合到日志中,截断超长文本)
var span = GlobalTracer.get().activeSpan();
if (span != null) {
GlobalTracer.get().buildSpan("request").addReference("", span.context())
.start().log(Strings.left(requestWrapper.getContentAsByteArray()), 10240).finish();
GlobalTracer.get().buildSpan("response").addReference("", span.context())
.start().log(Strings.left(responseWrapper.getContentAsByteArray()), 10240).finish();
}
// 注:以上两个方案任选其一即可
// 回写响应数据
responseWrapper.copyBodyToResponse();
}
}
添加一个配置类,将我们的Filter加载起来
Spring Boot 加载 Filter 有两种方法。一种是通过注解实现,一种是通过配置类实现。个人偏好使用后者,方便统一管理。注解实现可参考网上其他资料,这里不再赘述。
package com.gentleltd.todo965.backend.config;
import com.gentleltd.todo965.backend.infra.filter.LogFilter;
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<LogFilter> filterRegistration() {
return new FilterRegistrationBean<>(new LogFilter()) {{
addUrlPatterns("/*"); // 对所有URL生效
setEnabled(true);
setName("LogFilter");
setOrder(1); // 有多个Filter时,数字越小执行越早
}};
}
}
实现效果
简单两步,我们就完成了日志打印。在控制台我们看到了包含请求数据和响应数据的日志。
扩展
- 配合Jaeger,可以更方便地查看日志,Jaeger非常贴心地格式化了JSON。
- 方案一将request和response整合到同一个json里,如果服务涉及HTML等业务,会出现JSON出错的情况。所以可以考虑方案二将request、response的String分别打log。
- 由于使用Thrift协议将数据发送给Jaeger,存在长度限制,所以在Response或Request非常长的时候会发生错误导致日志无法被记录,因此需要对超长文本进行截断。
- 整合接入Jaeger可查看上一篇文章 《SpringBoot 利用Filter将请求数据、响应数据写入日志》
- 如果需要将OperationName修改为请求的URI,可以查看下一篇文《Spring Boot 对接 Jaeger 时利用Filter修改 Span 的 OperationName》
转载请注明出处
《Spring Boot 利用Filter将请求数据、响应数据写入日志》https://www.ywlib.com/archives/194.html (from 一闻自习室)
本文固定链接
https://www.ywlib.com/archives/194.html
标签
Spring Boot, Filter, 日志, 请求数据, 响应数据