一酝锅、前言
在工作中壹将,出現(xiàn)了需要打印每次請求中調(diào)用方傳過來的requestBody的需求
出現(xiàn)這個需求的原因是我在和某平臺做聯(lián)調(diào)工作择同,出現(xiàn)了一個比較惡心的情況判帮。
有一些事件通知需要由他們調(diào)用我們的http接口來實現(xiàn)事件通知,但是這個http接口的數(shù)據(jù)格式是由他們定義的(照搬其他地方的)裳仆,而他們給的相關(guān)文檔很爛腕让,示例中缺乏某些字段,而字段表里的字段又沒有分級歧斟,因此很難弄清楚他們請求的字段有哪些纯丸。
自己寫的類不一定能正確反序列化它的所有字段,如果反序列化有誤静袖,不清楚它傳來的xml長什么樣子觉鼻,也無法解決問題
總結(jié)一下問題原因:
- 我們寫的接口,要由他們定義字段類型队橙,但文檔寫的爛滑凉,字段定義的不清楚,不能提供維護(hù)以及答疑支持
- 配合程度有限喘帚,不能提供請求的xml
這兩點帶來的問題是當(dāng)反序列化出現(xiàn)問題,不自己打印它們請求過來的xml咒钟,就沒法快速找到問題原因吹由,因此,需要我們通過某種手段打印出requestBody的內(nèi)容
二朱嘴、傳統(tǒng)請求參數(shù)的打印
通常倾鲫,最簡單的HTTP GET請求可以通過寫一個繼承HandlerInterceptorAdapter的攔截器來實現(xiàn),形如:
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<Long> startTime = new ThreadLocal<>();
/**
* 預(yù)處理回調(diào)方法萍嬉,實現(xiàn)處理器的預(yù)處理(如檢查登陸)乌昔,第三個參數(shù)為響應(yīng)的處理器,自定義Controller
* <p>
* 返回值:
* true表示繼續(xù)流程(如調(diào)用下一個攔截器或處理器)
* false表示流程中斷(如登錄檢查失斎雷贰)磕道,不會繼續(xù)調(diào)用其他的攔截器或處理器
* 此時我們需要通過response來產(chǎn)生響應(yīng);
*
* @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("用戶訪問地址:{}, 來路地址: {}, 請求參數(shù): {}", uri, IpUtil.getRemoteIp(request), JSON.toJSON(paramMap));
log.info("----------------請求頭.start.....");
Enumeration<String> 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);
}
/**
* 在任何情況下都會對返回的請求做處理
* <p>
* 即在視圖渲染完畢時回調(diào)行冰,如性能監(jiān)控中我們可以在此記錄結(jié)束時間并輸出消耗時間
* 還可以進(jìn)行一些資源清理溺蕉,類似于try-catch-finally中的finally,但僅調(diào)用處理器執(zhí)行鏈中
*
* @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("請求處理結(jié)束. 處理耗時: {}", System.currentTimeMillis() - startTime.get());
startTime.remove();
super.afterCompletion(request, response, handler, ex);
}
}
三悼做、為什么打印requestBody是一個問題疯特?
請求參數(shù)可以通過 request.getParameterMap() 來獲得,但要獲取requestBody肛走,只能通過request.getInputStream() 來獲取輸入流漓雅,但是由于request 的inputStream和response 的outputStream默認(rèn)情況下是只能讀一次,若在攔截器中讀取打印了,后面業(yè)務(wù)就讀取不到了(別想著讀完還能寫回去邻吞,死了這條心叭)
3.1 解決辦法
在頭痛煩悶的嘗試了各種辦法后偶然看了這篇文章受到了啟發(fā)
Spring為了解決這個問題组题,為Request與Response分別封裝了 ContentCachingRequestWrapper 與 ContentCachingResponseWrapper 包裹類得這兩個流信息可重復(fù)讀(緩存機(jī)制,在讀取輸入流以后緩存下來)
3.1.1 初步解決方案
通過 ContentCachingRequestWrapper 這個類可以簡單的實現(xiàn)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的內(nèi)容了
3.1.2 解決方案優(yōu)化
后來我又發(fā)現(xiàn)Spring提供了一個過濾器抽象類AbstractRequestLoggingFilter吃衅,它為請求日志的打印提供了更豐富的功能往踢,但使用的時候也要注意一些小細(xì)節(jié)(小坑)
要使用這個過濾器,只要按照你的需要實現(xiàn)它的兩個抽象類就可以
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 的實現(xiàn)類 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以外,還可以很方便的根據(jù)需要打印更詳細(xì)請求信息趣效,以下是 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();
}
可以看到它能幫你生產(chǎn)的信息包含了uri瘦癌、請求參數(shù)、客戶端信息跷敬、會話信息讯私、遠(yuǎn)程用戶信息、headers以及payload西傀,并且這些都是根據(jù)你的需要配置的
生成效果如下:
3.1.3 注冊Filter
只需要在繼承WebMvcConfigurationSupport的配置類中注冊這個Filter即可
@Bean
public FilterRegistrationBean loggingFilterRegistration() {
FilterRegistrationBean<ServletContextRequestLoggingFilter> 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屬性的默認(rèn)值是false,不會打印payload信息拥褂,同時maxPayloadLength默認(rèn)值是50娘锁,會導(dǎo)致打印的requestBody不完整
貼一下它們的相關(guān)代碼
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類緩存請求是通過消耗輸入流來進(jìn)行緩存的莫秆,因此這是一個不小的代價,它使得過濾器鏈中的其他過濾器無法再讀取輸入流悔详。
可見:https://github.com/spring-projects/spring-framework/issues/20577