缓存请求响应体的目的
把一个HTTP的请求,响应信息完整的纪录到日志。是一种常见有效的问题排查,BUG重现的手段。
但是流这种东西,有一个特点就是只能读取/写入一次,不能重复。下一次读写,就是一个空的流,为了实现流的重用,就很有必要,把读取和写入的数据缓存起来, 可以在某个地方,再一次的读取。
实现的思路
- HttpServletRequestWrapper
- HttpServletResponseWrapper
上面2个类,熟悉Servlet
的都知道,这俩就是Request
和Response
的装饰模式实现。
通过装饰者设计模式,我们可以在Request读取请求body的时候,把读取到的数据复制一份缓存起来,记录日志时使用。同理,也可以把Response响应的数据,先缓存起来,用于记录日志,然后再响应给客户端。
Spring提供的实现
ContentCachingRequestWrapper
// 这里忽略了 HttpServletRequest 的相关方法
public class ContentCachingRequestWrapper extends HttpServletRequestWrapper {
// 包装Servlet,不限制请求体的大小
public ContentCachingRequestWrapper(HttpServletRequest request)
// 包装Servlet,限制请求体的大小
public ContentCachingRequestWrapper(HttpServletRequest request, int contentCacheLimit)
// 获取到缓存的请求体
public byte[] getContentAsByteArray()
// 请求体超过限制时会调用这个方法,默认空实现
protected void handleContentOverflow(int contentCacheLimit)
}
比较好理解的一个类,建议通过contentCacheLimit
限制请求体大小。因为它默认把请求体缓存到内存中,如果客户端发起恶意请求,构造大体积的请求体可能会消耗干净服务器的内存
ContentCachingResponseWrapper
// 这里忽略了 HttpServletResponse 的相关方法
public class ContentCachingResponseWrapper {
// 把缓存中的响应数据,刷出到客户端
void copyBodyToResponse()
// 获取缓存数据
byte[] getContentAsByteArray()
// 获取缓存数据
InputStream getContentInputStream()
// 获取缓存数据的大小
int getContentSize()
}
很简单,通过ContentCachingResponseWrapper
的包装,任何往客户端的响应数据,都会被它缓存起来,重复的读取使用,最终响应给客户端
请求日志的实现
Controller
及其简单,把请求体,添加时间戳后回写给客户端。
import java.util.HashMap;
import java.util.Map;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/demo"