日志框架 - 基于spring-boot - 實(shí)現(xiàn)4 - HTTP請(qǐng)求攔截

日志框架系列講解文章
日志框架 - 基于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ù))做了這些事情:

  1. 根據(jù)請(qǐng)求的URI判斷是否需要忽略請(qǐng)求的攔截,主要忽略的對(duì)象是Spring各組件內(nèi)置的URI和靜態(tài)資源等;
  2. 從消息中解析出關(guān)鍵字的值,并將其存放到MDC中;
  3. 這里還演示了@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)求攔截處理的所有功能。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容