2024年7月17日 星期三

JavaEE - 使用 ContentCachingRequestWrapper 和 ContentCachingResponseWrapper 在 filter 中取得 request 和 response 的內容

 JavaEE 中,如果 HttpServletResponse 的內容一經使用 OutputStream 等方式被讀取的話,
因為資料流只能被讀取一次,
會讓之後在輸入內容到前端時發生不預期的錯誤,
所以不能只接讀取,
如果要獲得 HttpServletRequest, HttpServletResponse 的內容的話,例如拿來做 log 紀錄等用途時會需要,
這時可以使用 Spring 的
ContentCachingRequestWrapper

ContentCachingResponseWrapper
這兩個工具來幫忙,
以下用一個 Filter 實作來做範例:

package xxx.api.filter;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.Instant;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;

@WebFilter(filterName = "apiFilter", urlPatterns = "/xxx/xxx.do")
public class ApiFilter extends OncePerRequestFilter {

	private Logger logger = LoggerFactory.getLogger("xxxLogger");
	
	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {
		
		Instant tic = Instant.now();
		
		ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(request);
        ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);
		
		filterChain.doFilter(requestWrapper, responseWrapper); //不用原來的 Request 和 Response,改把用 Wrapper 封裝的 Request 和 Response 放到 filterChain 中繼續傳遞
		
		long durationMillis = Duration.between(tic, Instant.now()).toMillis();
		
		String requestBody = new String(requestWrapper.getContentAsByteArray(), StandardCharsets.UTF_8);
		
		byte[] responseByteArr = responseWrapper.getContentAsByteArray();
		String responseStr = new String(responseByteArr, StandardCharsets.UTF_8);
		
		String loggerMsg = requestWrapper.getRemoteAddr() +
						   "|" + requestWrapper.getMethod() +
						   "|" + requestWrapper.getRequestURI() +
						   "|" + StringUtils.defaultIfBlank(requestWrapper.getQueryString(), "")  +
						   "|" + responseWrapper.getStatus() +
						   "|" + durationMillis +
						   "|" + requestBody +
						   "|" + responseStr +
					       "|" + responseByteArr.length;
		logger.info(loggerMsg);
		
		responseWrapper.copyBodyToResponse(); //要記得執行這段,不然 HttpServletResponse 會沒有資料輸出
	}

}

說明:
ContentCachingRequestWrapper 和 ContentCachingResponseWrapper 會把
HttpServletRequest 和 HttpServletResponse 包起來,內部用了 ByteArrayInputStream 和
ByteArrayOutputStream 等取得 HttpServletRequest 和 HttpServletResponse 的內容,
可以重覆取得而不用怕影響到 HttpServletRequest 和 HttpservletResponse 的輸入、輸出流。

這邊要注意的是,我們在 Filter 的最後要記得呼叫

responseWrapper.copyBodyToResponse()

不然 HttpServletResponse 會沒有資料輸出。

參考資料:

  1. 【Spring Boot】第16課-使用 Filter 擷取請求與回應

沒有留言 :

張貼留言