日志框架系列講解文章
日志框架 - 基于spring-boot - 使用入門(mén)
日志框架 - 基于spring-boot - 設(shè)計(jì)
日志框架 - 基于spring-boot - 實(shí)現(xiàn)1 - 配置文件
日志框架 - 基于spring-boot - 實(shí)現(xiàn)2 - 消息定義及消息日志打印
日志框架 - 基于spring-boot - 實(shí)現(xiàn)3 - 關(guān)鍵字與三種消息解析器
日志框架 - 基于spring-boot - 實(shí)現(xiàn)4 - HTTP請(qǐng)求攔截
日志框架 - 基于spring-boot - 實(shí)現(xiàn)5 - 線程切換
日志框架 - 基于spring-boot - 實(shí)現(xiàn)6 - 自動(dòng)裝配
上一篇我們講了框架實(shí)現(xiàn)的第三部分:如何自動(dòng)解析消息
本篇主要講框架實(shí)現(xiàn)的第四部分:實(shí)現(xiàn)HTTP請(qǐng)求的攔截
在設(shè)計(jì)一文中我們提到
在請(qǐng)求進(jìn)入業(yè)務(wù)層之前進(jìn)行攔截,獲得消息(Message)
鑒于HTTP請(qǐng)求的普遍性與代表性,本篇主要聚焦于HTTP請(qǐng)求的攔截與處理。
攔截HTTP請(qǐng)求,獲取消息
Spring中HTTP請(qǐng)求的攔截其實(shí)很簡(jiǎn)單,只需要實(shí)現(xiàn)Spring提供的攔截器(Interceptor)接口就可以了。其主要實(shí)現(xiàn)的功能是將消息中的關(guān)鍵內(nèi)容填入到MDC中,代碼如下。
/**
* Http請(qǐng)求攔截器,其主要功能是:
* <p>
* 1. 識(shí)別請(qǐng)求報(bào)文
* <p>
* 2. 解析報(bào)文關(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();
}
}
可以見(jiàn)到,在HTTP請(qǐng)求進(jìn)入業(yè)務(wù)處理之前(preHandle函數(shù))做了這些事情:
- 根據(jù)請(qǐng)求的URI判斷是否需要忽略請(qǐng)求的攔截,主要忽略的對(duì)象是Spring各組件內(nèi)置的URI和靜態(tài)資源等;
- 從消息中解析出關(guān)鍵字的值,并將其存放到MDC中;
- 這里還演示了@MessageToLog注解的用法,提供了默認(rèn)的消息日志打印功能,關(guān)于@MessageToLog的設(shè)計(jì),請(qǐng)參考這篇文章。
最后,當(dāng)HTTP請(qǐng)求完成處理后(afterCompletion函數(shù)),將MDC中緩存的信息銷毀。
HTTP請(qǐng)求輸入流的重復(fù)讀取
熟悉HTTP協(xié)議實(shí)現(xiàn)的伙伴們可能會(huì)意識(shí)到,上面代碼中的getBodyFromRequest函數(shù)為了獲取 HTTP Body,讀取了 HTTP 請(qǐng)求的輸入流(InputStream)。但來(lái)自于網(wǎng)絡(luò)的 HTTP 請(qǐng)求的輸入流只能被讀取一次。這段代碼會(huì)導(dǎo)致業(yè)務(wù)邏輯中獲取不到 HTTP Body 內(nèi)容。因此,我們還需要實(shí)現(xiàn)一個(gè)可以重復(fù)讀取 Body 的 HTTP 請(qǐng)求適配器。
網(wǎng)上有很多針對(duì) HTTP InputStream 可重復(fù)讀取的實(shí)現(xiàn),比如這個(gè)。
但實(shí)現(xiàn)普遍有一個(gè)重大缺陷,通過(guò)閱讀Tomcat的代碼可知,就是對(duì)于當(dāng) request 對(duì)象的 getParameterMap 函數(shù)被調(diào)用時(shí),也會(huì)去讀取 InputStream 。因此,要重寫(xiě)獲取parameterMap相關(guān)的所有接口,以下是改進(jì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);
}
}
然后,將此請(qǐng)求的適配器用Servlet Filter裝配到系統(tǒng)中。代碼如下。
/**
* 將http請(qǐng)求進(jìn)行替換,為了能重復(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請(qǐng)求攔截處理的所有功能。