日志框架系列講解文章
日志框架 - 基于spring-boot - 使用入門
日志框架 - 基于spring-boot - 設(shè)計
日志框架 - 基于spring-boot - 實現(xiàn)1 - 配置文件
日志框架 - 基于spring-boot - 實現(xiàn)2 - 消息定義及消息日志打印
日志框架 - 基于spring-boot - 實現(xiàn)3 - 關(guān)鍵字與三種消息解析器
日志框架 - 基于spring-boot - 實現(xiàn)4 - HTTP請求攔截
日志框架 - 基于spring-boot - 實現(xiàn)5 - 線程切換
日志框架 - 基于spring-boot - 實現(xiàn)6 - 自動裝配
上一篇我們講了框架實現(xiàn)的第三部分:如何自動解析消息
本篇主要講框架實現(xiàn)的第四部分:實現(xiàn)HTTP請求的攔截
在設(shè)計一文中我們提到
在請求進入業(yè)務(wù)層之前進行攔截,獲得消息(Message)
鑒于HTTP請求的普遍性與代表性,本篇主要聚焦于HTTP請求的攔截與處理镊屎。
攔截HTTP請求函荣,獲取消息
Spring中HTTP請求的攔截其實很簡單宾舅,只需要實現(xiàn)Spring提供的攔截器(Interceptor)接口就可以了祷蝌。其主要實現(xiàn)的功能是將消息中的關(guān)鍵內(nèi)容填入到MDC中,代碼如下沐批。
/**
* Http請求攔截器从铲,其主要功能是:
* <p>
* 1. 識別請求報文
* <p>
* 2. 解析報文關(guān)鍵字
* <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請求進入業(yè)務(wù)處理之前(preHandle函數(shù))做了這些事情:
- 根據(jù)請求的URI判斷是否需要忽略請求的攔截,主要忽略的對象是Spring各組件內(nèi)置的URI和靜態(tài)資源等名段;
- 從消息中解析出關(guān)鍵字的值阱扬,并將其存放到MDC中;
- 這里還演示了@MessageToLog注解的用法伸辟,提供了默認的消息日志打印功能麻惶,關(guān)于@MessageToLog的設(shè)計,請參考這篇文章信夫。
最后窃蹋,當(dāng)HTTP請求完成處理后(afterCompletion函數(shù)),將MDC中緩存的信息銷毀静稻。
HTTP請求輸入流的重復(fù)讀取
熟悉HTTP協(xié)議實現(xiàn)的伙伴們可能會意識到警没,上面代碼中的getBodyFromRequest函數(shù)為了獲取 HTTP Body,讀取了 HTTP 請求的輸入流(InputStream)姊扔。但來自于網(wǎng)絡(luò)的 HTTP 請求的輸入流只能被讀取一次惠奸。這段代碼會導(dǎo)致業(yè)務(wù)邏輯中獲取不到 HTTP Body 內(nèi)容梅誓。因此恰梢,我們還需要實現(xiàn)一個可以重復(fù)讀取 Body 的 HTTP 請求適配器。
網(wǎng)上有很多針對 HTTP InputStream 可重復(fù)讀取的實現(xiàn)梗掰,比如這個嵌言。
但實現(xiàn)普遍有一個重大缺陷,通過閱讀Tomcat的代碼可知及穗,就是對于當(dāng) request 對象的 getParameterMap 函數(shù)被調(diào)用時摧茴,也會去讀取 InputStream 。因此埂陆,要重寫獲取parameterMap相關(guān)的所有接口苛白,以下是改進了的代碼娃豹。
/**
* 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裝配到系統(tǒng)中购裙。代碼如下懂版。
/**
* 將http請求進行替換,為了能重復(fù)讀取http body中的內(nèi)容
*/
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請求攔截處理的所有功能躯畴。