日志框架系列讲解文章
日志框架 - 基于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请求拦截处理的所有功能。