springboot 打印slf4_SpringBoot打印请求体与响应体
一、前言
在工作中,出现了需要打印每次请求中调用方传过来的requestBody的需求
出现这个需求的原因是我在和某平台做联调工作,出现了一个比较恶心的情况。
有一些事件通知需要由他们调用我们的http接口来实现事件通知,但是这个http接口的数据格式是由他们定义的(照搬其他地方的),而他们给的相关文档很烂,示例中缺乏某些字段,而字段表里的字段又没有分级,因此很难弄清楚他们请求的字段有哪些。
自己写的类不一定能正确反序列化它的所有字段,如果反序列化有误,不清楚它传来的xml长什么样子,也无法解决问题
总结一下问题原因:
我们写的接口,要由他们定义字段类型,但文档写的烂,字段定义的不清楚,不能提供维护以及答疑支持
配合程度有限,不能提供请求的xml
这两点带来的问题是当反序列化出现问题,不自己打印它们请求过来的xml,就没法快速找到问题原因,因此,需要我们通过某种手段打印出requestBody的内容
二、传统请求参数的打印
通常,最简单的HTTP GET请求可以通过写一个继承HandlerInterceptorAdapter的拦截器来实现,形如:
package com.chasel.interceptor;
import com.alibaba.fastjson.JSON;
import com.cmic.origin.internal.gateway.core.util.IpUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Enumeration;
import java.util.Map;
/**
* @author XieLongzhen
* @date 2018/12/26 18:46
*/
@Slf4j
@Component
public class HttpInterceptor extends HandlerInterceptorAdapter {
private ThreadLocal startTime = new ThreadLocal<>();
/**
* 预处理回调方法,实现处理器的预处理(如检查登陆),第三个参数为响应的处理器,自定义Controller
*
* 返回值:
* true表示继续流程(如调用下一个拦截器或处理器)
* false表示流程中断(如登录检查失败),不会继续调用其他的拦截器或处理器
* 此时我们需要通过response来产生响应;
*
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
startTime.set(System.currentTimeMillis());
String uri = request.getRequestURI();
Map paramMap = request.getParameterMap();
log.info("用户访问地址:{}, 来路地址: {}, 请求参数: {}", uri, IpUtil.getRemoteIp(request), JSON.toJSON(paramMap));
log.info("----------------请求头.start.....");
Enumeration enums = request.getHeaderNames();
while (enums.hasMoreElements()) {
String name = enums.nextElement();
log.info(name + ": {}", request.getHeader(name));
}
log.info("----------------请求头.end!");
return super.preHandle(request, response, handler);
}
/**
* 在任何情况下都会对返回的请求做处理
*
* 即在视图渲染完毕时回调,如性能监控中我们可以在此记录结束时间并输出消耗时间
* 还可以进行一些资源清理,类似于try-catch-finally中的finally,但仅调用处理器执行链中
*
* @param request
* @param response
* @param handler
* @param ex
* @throws Exception
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
log.info("请求处理结束. 处理耗时: {}", System.currentTimeMillis() - startTime.get());
startTime.remove();
super.afterCompletion(request, response, handler, ex);
}
}
三、为什么打印requestBody是一个问题?
请求参数可以通过 request.getParameterMap() 来获得,但要获取requestBody,只能通过request.getInputStream() 来获取输入流,但是由于request 的inputStream和response 的outputStream默认情况下是只能读一次,若在拦截器中读取打印了,后面业务就读取不到了(别想着读完还能写回去,死了这条心叭)
3.1 解决办法
在头痛烦闷的尝试了各种办法后偶然看了这篇文章受到了启发
Spring为了解决这个问题,为Request与Response分别封装了 ContentCachingRequestWrapper 与 ContentCachingResponseWrapper 包裹类得这两个流信息可重复读(缓存机制,在读取输入流以后缓存下来)
3.1.1 初步解决方案
通过 ContentCachingRequestWrapper 这个类可以简单的实现requestBody的打印
package com.chasel.filter;
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;
/**
* @author XieLongzhen
* @date 2019/10/9 14:38
*/
@Slf4j
public class LogFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void destroy() {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper((HttpServletRequest) request);
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper((HttpServletResponse) response);
try {
chain.doFilter(requestWrapper, responseWrapper);
} finally {
String requestBody = new String(requestWrapper.getContentAsByteArray());
log.info("请求body: {}", requestBody);
}
}
}
然后就可以打印出请求body的内容了
3.1.2 解决方案优化
后来我又发现Spring提供了一个过滤器抽象类AbstractRequestLoggingFilter,它为请求日志的打印提供了更丰富的功能,但使用的时候也要注意一些小细节(小坑)
要使用这个过滤器,只要按照你的需要实现它的两个抽象类就可以
protected abstract void beforeRequest(HttpServletRequest request, String message);
protected abstract void afterRequest(HttpServletRequest request, String message);
核心代码如下
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
boolean isFirstRequest = !isAsyncDispatch(request);
HttpServletRequest requestToUse = request;
if (isIncludePayload() && isFirstRequest && !(request instanceof ContentCachingRequestWrapper)) {
requestToUse = new ContentCachingRequestWrapper(request, getMaxPayloadLength());
}
boolean shouldLog = shouldLog(requestToUse);
if (shouldLog && isFirstRequest) {
beforeRequest(requestToUse, getBeforeMessage(requestToUse));
}
try {
filterChain.doFilter(requestToUse, response);
}
finally {
if (shouldLog && !isAsyncStarted(requestToUse)) {
afterRequest(requestToUse, getAfterMessage(requestToUse));
}
}
}
同样,你可以直接使用Spring提供的 AbstractRequestLoggingFilter 的实现类 ServletContextRequestLoggingFilter
public class ServletContextRequestLoggingFilter extends AbstractRequestLoggingFilter {
/**
* Writes a log message before the request is processed.
*/
@Override
protected void beforeRequest(HttpServletRequest request, String message) {
getServletContext().log(message);
}
/**
* Writes a log message after the request is processed.
*/
@Override
protected void afterRequest(HttpServletRequest request, String message) {
getServletContext().log(message);
}
}
使用Spring提供的过滤器的好处是,除了requestBody以外,还可以很方便的根据需要打印更详细请求信息,以下是 createMessage() 的完整代码
protected String createMessage(HttpServletRequest request, String prefix, String suffix) {
StringBuilder msg = new StringBuilder();
msg.append(prefix);
msg.append("uri=").append(request.getRequestURI());
if (isIncludeQueryString()) {
String queryString = request.getQueryString();
if (queryString != null) {
msg.append('?').append(queryString);
}
}
if (isIncludeClientInfo()) {
String client = request.getRemoteAddr();
if (StringUtils.hasLength(client)) {
msg.append(";client=").append(client);
}
HttpSession session = request.getSession(false);
if (session != null) {
msg.append(";session=").append(session.getId());
}
String user = request.getRemoteUser();
if (user != null) {
msg.append(";user=").append(user);
}
}
if (isIncludeHeaders()) {
msg.append(";headers=").append(new ServletServerHttpRequest(request).getHeaders());
}
if (isIncludePayload()) {
String payload = getMessagePayload(request);
if (payload != null) {
msg.append(";payload=").append(payload);
}
}
msg.append(suffix);
return msg.toString();
}
可以看到它能帮你生产的信息包含了uri、请求参数、客户端信息、会话信息、远程用户信息、headers以及payload,并且这些都是根据你的需要配置的
生成效果如下:
3.1.3 注册Filter
只需要在继承WebMvcConfigurationSupport的配置类中注册这个Filter即可
@Bean
public FilterRegistrationBean loggingFilterRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean<>();
ServletContextRequestLoggingFilter filter = new ServletContextRequestLoggingFilter();
filter.setIncludePayload(true);
filter.setMaxPayloadLength(9999);
registration.setFilter(filter);
registration.setUrlPatterns(Collections.singleton("/notifications/*"));
return registration;
}
3.1.4 遇到的坑
其中 setIncludePayload() 以及 setMaxPayloadLength() 就是我在使用中遇到的坑。因为AbstractRequestLoggingFilter 的includePayload属性的默认值是false,不会打印payload信息,同时maxPayloadLength默认值是50,会导致打印的requestBody不完整
贴一下它们的相关代码
protected String createMessage(HttpServletRequest request, String prefix, String suffix) {
StringBuilder msg = new StringBuilder();
msg.append(prefix);
msg.append("uri=").append(request.getRequestURI());
...
// 只有 includePayload 为true时才打印payload信息
if (isIncludePayload()) {
String payload = getMessagePayload(request);
if (payload != null) {
msg.append(";payload=").append(payload);
}
}
msg.append(suffix);
return msg.toString();
}
protected String getMessagePayload(HttpServletRequest request) {
ContentCachingRequestWrapper wrapper =
WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class);
if (wrapper != null) {
byte[] buf = wrapper.getContentAsByteArray();
if (buf.length > 0) {
// 取的是buf.length与maxPayloadLength的最小值
int length = Math.min(buf.length, getMaxPayloadLength());
try {
return new String(buf, 0, length, wrapper.getCharacterEncoding());
}
catch (UnsupportedEncodingException ex) {
return "[unknown]";
}
}
}
return null;
}
四、弊端
但是使用这两个包裹类会有一些潜在的问题,ContentCachingRequestWrapper类缓存请求是通过消耗输入流来进行缓存的,因此这是一个不小的代价,它使得过滤器链中的其他过滤器无法再读取输入流。
springboot 打印slf4_SpringBoot打印请求体与响应体相关推荐
- Spring/SpringBoot 过滤器修改、获取http 请求request中的参数 和 response返回值,比如修改请求体和响应体的字符编码
通过自定义filter,RequestWrapper,ResponseWrapper 处理请求和响应数据,比如修改请求体和响应体的字符编码 1.request 和 response 中的数据都是 存在 ...
- java过滤器修改响应,在过滤器中实现修改http请求体和响应体
在一些业务场景中,需要对http的请求体和响应体做加解密的操作,如果在controller中来调用加解密函数,会增加代码的耦合度,同时也会增加调试的难度. 参考spring中http请求的链路,选择过 ...
- http请求,get请求和post请求体以及响应体
一.http请求 1.Http请求格式 Http请求即客户端发送给服务器的请求.该请求的内容格式如下所示: 请求首行 请求头信息 空行 请求正文,也称请求体 2.使用HttpWatch抓包工具 请求信 ...
- baseresponse响应类_Java response响应体和文件下载实现原理
通过response 设置响应体: 响应体设置文本: PrintWriter getWriter() 获得字符流,通过字符流的write(String s)方法可以将字符串设置到response 缓冲 ...
- HTTP请求报文和响应报文
目录 HTTP报文 请求报文 响应报文 HTTP状态码 HTTP状态码分类 使用Chrome网络控制台查看通信报文 HTTP报文 HTTP报文是HTTP协议交互时所规定请求和响应的规则.请求端(客户端 ...
- Android中使用logger打印完整的okhttp网络请求和响应的所有相关信息(请求行、请求头、请求体、响应行、响应行、响应头、响应体)
如果你的项目中的网络请求库是Retrofit的话,他的底层封装的是OkHttp,通常调试网络接口时都会将网络请求和响应相关数据通过日志的形式打印出来.OkHttp也提供了一个网络拦截器okhttp-l ...
- SpringBoot切面AOP打印请求和响应日志
1.说明 Spring Boot微服务对外开放的Restful接口, 为了方便定位问题, 一般需要记录请求日志和响应日志, 而在每个接口中开发日志代码是非常繁琐的, 本文介绍使用Spring的切面AO ...
- java http打印请求日志_spring打印http接口请求和响应
在程序日志中打印出接口请求和响应的内容是一个基本的技术需求.如果在每个接口中实现请求响应的日志打印,程序编写会很繁琐,我们可以利用spring提供的机制,集中处理接口请求响应的日志打印. 具体的代码参 ...
- Springboot校园在线打印预约系统小程序【纯干货分享,附源码91740】
摘 要 本文设计了一种基于微信支付的校园在线打印预约系统小程序,系统为人们提供了方便快捷的线上打印服务,包括打印预约.注册登录.打印平台.校园资讯等,用户不仅能够方便快捷在线搜索打印方式.还能进行打印 ...
- SpringBoot利用Aop打印入参出参日志
SpringBoot利用Aop打印入参出参日志 前言 以前写代码不会用Aop的时候,记录入参出参的日志打印都是在Controller中完成的,每个Controller的方法开始之前先打印个日志,然后方 ...
最新文章
- git 修改标签名称_Git常用命令汇总,希望能帮到你
- Spring Cloud Greenwich 最后一个计划版本发布!
- No module named 'dlframework.common.utils.local'
- [小算法] 找出单链表中的中间元素
- 从内涵段子到皮皮虾,娱乐App为何不能一鱼两吃?
- python算法预测风险等级_一般算法水平到底什么样子才能秒杀Bat的笔试编程题?...
- boost::describe模块实现计算基础修饰符的测试程序
- python 波形发生_事件与信号
- 2019 年,智能问答(Question Answering)的主要研究方向有哪些?
- golang关键字和程序语句
- (转)基于MVC4+EasyUI的Web开发框架经验总结(10)--在Web界面上实现数据的导入和导出...
- python记录_day33 线程
- cs1.6 linux,Ubuntu 8.04下用Wine 0.9.59安装cs1.6 (Esai_Cs1.6_2834)
- 浏览器无法打开搜索引擎页面
- ARM的启动过程详解(CHINAITLAB)
- 视频gif如何制作?试试这个视频制作gif神器
- 遍历Lua全局环境变量
- Typora收费了, 还有哪些好用的markdown工具
- HashMap结构图及特点
- 开车的26条教训!开车的人一定看看!