日志架構系列講解文章 日志架構 - 基于spring-boot - 使用入門 日志架構 - 基于spring-boot - 設計 日志架構 - 基于spring-boot - 實作1 - 配置檔案 日志架構 - 基于spring-boot - 實作2 - 消息定義及消息日志列印 日志架構 - 基于spring-boot - 實作3 - 關鍵字與三種消息解析器 日志架構 - 基于spring-boot - 實作4 - HTTP請求攔截 日志架構 - 基于spring-boot - 實作5 - 線程切換 日志架構 - 基于spring-boot - 實作6 - 自動裝配
上一篇我們講了架構實作的第三部分:如何自動解析消息
本篇主要講架構實作的第四部分:實作HTTP請求的攔截
在 設計一文中我們提到
在請求進入業務層之前進行攔截,獲得消息(Message)
鑒于HTTP請求的普遍性與代表性,本篇主要聚焦于HTTP請求的攔截與處理。
攔截HTTP請求,擷取消息
Spring中HTTP請求的攔截其實很簡單,隻需要實作Spring提供的攔截器(Interceptor)接口就可以了。其主要實作的功能是将消息中的關鍵内容填入到MDC中,代碼如下。
/**
* Http請求攔截器,其主要功能是:
* <p>
* 1. 識别請求封包
* <p>
* 2. 解析封包關鍵字
* <p>
* 3. 将值填入到MDC中
*/
public class MDCSpringMvcHandlerInterceptor extends HandlerInterceptorAdapter {
private Pattern skipPattern = Pattern.compile(Constant.SKIP_PATTERN);
private UrlPathHelper urlPathHelper = new UrlPathHelper();
@Autowired
private DefaultKeywords defaultKeywords;
@Autowired
private MDCSpringMvcHandlerInterceptor self;
@Autowired
ApplicationContext context;
@Override
public boolean preHandle(
HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
MessageResolverChain messageResolverChain =
context.getBean(MessageResolverChain.class);
if (messageResolverChain == null) {
return true;
}
String uri = this.urlPathHelper.getPathWithinApplication(request);
boolean skip = this.skipPattern.matcher(uri).matches();
if (skip) {
return true;
}
Message message = tidyMessageFromRequest(request);
((MDCSpringMvcHandlerInterceptor) AopContext.currentProxy())
.doLogMessage(message);
MDC.setContextMap(defaultKeywords.getDefaultKeyValues());
Map<String, String> keyValues =
messageResolverChain.dispose(message);
if (!CollectionUtils.isEmpty(keyValues)) {
keyValues.forEach((k, v) -> MDC.put(k, v));
}
return true;
}
@MessageToLog
public Object doLogMessage(Message message) {
return message.getContent();
}
private Message tidyMessageFromRequest(HttpServletRequest request)
throws IOException {
Message message = new Message();
if (HttpMethod.GET.matches(request.getMethod())) {
String queryString = request.getQueryString();
if (StringUtils.isEmpty(queryString)) {
message.setType(MessageType.NONE);
} else {
message.setType(MessageType.KEY_VALUE);
message.setContent(queryString);
}
} else {
String mediaType = request.getContentType();
if (mediaType.startsWith(MediaType.APPLICATION_JSON_VALUE) ||
mediaType.startsWith("json")) {
message.setType(MessageType.JSON);
message.setContent(getBodyFromRequest(request));
} else if (mediaType.startsWith(MediaType.APPLICATION_XML_VALUE) ||
mediaType.startsWith(MediaType.TEXT_XML_VALUE) ||
mediaType.startsWith(MediaType.TEXT_HTML_VALUE)) {
message.setType(MessageType.XML);
message.setContent(getBodyFromRequest(request));
} else if (mediaType.equals(MediaType
.APPLICATION_FORM_URLENCODED_VALUE) ||
mediaType.startsWith(
MediaType.MULTIPART_FORM_DATA_VALUE)) {
message.setType(MessageType.KEY_VALUE);
Map<String, String[]> parameterMap = request.getParameterMap();
Map<String, String> contentMap = new HashMap<>();
parameterMap.forEach((s, strings) -> {
contentMap.put(s, strings[0]);
});
message.setContent(contentMap);
} else if (mediaType.equals(MediaType.ALL_VALUE) ||
mediaType.startsWith("text")) {
message.setType(MessageType.TEXT);
message.setContent(getBodyFromRequest(request));
} else {
message.setType(MessageType.NONE);
}
}
return message;
}
private String getBodyFromRequest(HttpServletRequest request) throws
IOException {
if (request instanceof InputStreamReplacementHttpRequestWrapper) {
return ((InputStreamReplacementHttpRequestWrapper) request)
.getRequestBody();
} else {
return StreamUtils.copyToString(request.getInputStream(),
Constant.DEFAULT_CHARSET);
}
}
@Override
public void afterCompletion(
HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) throws Exception {
MDC.clear();
}
}
可以見到,在HTTP請求進入業務處理之前(preHandle函數)做了這些事情:
- 根據請求的URI判斷是否需要忽略請求的攔截,主要忽略的對象是Spring各元件内置的URI和靜态資源等;
- 從消息中解析出關鍵字的值,并将其存放到MDC中;
- 這裡還示範了@MessageToLog注解的用法,提供了預設的消息日志列印功能,關于@MessageToLog的設計,請參考 這篇文章 。
最後,當HTTP請求完成處理後(afterCompletion函數),将MDC中緩存的資訊銷毀。
HTTP請求輸入流的重複讀取
熟悉HTTP協定實作的夥伴們可能會意識到,上面代碼中的getBodyFromRequest函數為了擷取 HTTP Body,讀取了 HTTP 請求的輸入流(InputStream)。但來自于網絡的 HTTP 請求的輸入流隻能被讀取一次。這段代碼會導緻業務邏輯中擷取不到 HTTP Body 内容。是以,我們還需要實作一個可以重複讀取 Body 的 HTTP 請求擴充卡。
網上有很多針對 HTTP InputStream 可重複讀取的實作,比如
這個但實作普遍有一個重大缺陷,通過閱讀Tomcat的代碼可知,就是對于當 request 對象的 getParameterMap 函數被調用時,也會去讀取 InputStream 。是以,要重寫擷取parameterMap相關的所有接口,以下是改進了的代碼。
/**
* Constructs a request object wrapping the given request.
*/
public class InputStreamReplacementHttpRequestWrapper
extends HttpServletRequestWrapper {
private String requestBody;
private Map<String, String[]> parameterMap;
public InputStreamReplacementHttpRequestWrapper(HttpServletRequest request)
throws IOException {
super(request);
parameterMap = request.getParameterMap();
requestBody = StreamUtils.copyToString(request.getInputStream(),
Constant.DEFAULT_CHARSET);
}
public String getRequestBody() {
return requestBody;
}
@Override
public ServletInputStream getInputStream() throws IOException {
ByteArrayInputStream is = new ByteArrayInputStream(
requestBody.getBytes(Constant.DEFAULT_CHARSET_NAME));
return new ServletInputStream() {
@Override
public int read() throws IOException {
return is.read();
}
@Override
public boolean isFinished() {
return is.available() <= 0;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener listener) {
}
};
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
@Override
public String getParameter(String name) {
String[] values = parameterMap.get(name);
if (values != null) {
if(values.length == 0) {
return "";
}
return values[0];
} else {
return null;
}
}
@Override
public Map<String, String[]> getParameterMap() {
return parameterMap;
}
@Override
public Enumeration<String> getParameterNames() {
return Collections.enumeration(parameterMap.keySet());
}
@Override
public String[] getParameterValues(String name) {
return parameterMap.get(name);
}
}
然後,将此請求的擴充卡用Servlet Filter裝配到系統中。代碼如下。
/**
* 将http請求進行替換,為了能重複讀取http body中的内容
*/
public class RequestReplaceServletFilter extends GenericFilter {
private Pattern skipPattern = Pattern.compile(Constant.SKIP_PATTERN);
private UrlPathHelper urlPathHelper = new UrlPathHelper();
@Override
public void doFilter(
ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
if ((request instanceof HttpServletRequest)) {
HttpServletRequest httpReq = (HttpServletRequest) request;
String uri = urlPathHelper.getPathWithinApplication(httpReq);
boolean skip = this.skipPattern.matcher(uri).matches();
String method = httpReq.getMethod().toUpperCase();
if (!skip && !HttpMethod.GET.matches(method)) {
httpReq = new InputStreamReplacementHttpRequestWrapper(httpReq);
}
chain.doFilter(httpReq, response);
} else {
chain.doFilter(request, response);
}
return;
}
@Override
public void destroy() {
}
}
至此,完成了HTTP請求攔截處理的所有功能。