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

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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市薇芝,隨后出現(xiàn)的幾起案子蓬抄,更是在濱河造成了極大的恐慌,老刑警劉巖夯到,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件嚷缭,死亡現(xiàn)場離奇詭異,居然都是意外死亡耍贾,警方通過查閱死者的電腦和手機峭状,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來逼争,“玉大人优床,你說我怎么就攤上這事∈慕梗” “怎么了胆敞?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長杂伟。 經(jīng)常有香客問我移层,道長,這世上最難降的妖魔是什么赫粥? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任观话,我火速辦了婚禮,結(jié)果婚禮上越平,老公的妹妹穿的比我還像新娘频蛔。我一直安慰自己,他們只是感情好秦叛,可當(dāng)我...
    茶點故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布晦溪。 她就那樣靜靜地躺著,像睡著了一般挣跋。 火紅的嫁衣襯著肌膚如雪三圆。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天,我揣著相機與錄音舟肉,去河邊找鬼修噪。 笑死,一個胖子當(dāng)著我的面吹牛路媚,可吹牛的內(nèi)容都是我干的割按。 我是一名探鬼主播,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼磷籍,長吁一口氣:“原來是場噩夢啊……” “哼适荣!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起院领,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤弛矛,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后比然,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體丈氓,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年强法,在試婚紗的時候發(fā)現(xiàn)自己被綠了万俗。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡饮怯,死狀恐怖闰歪,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情蓖墅,我是刑警寧澤库倘,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站论矾,受9級特大地震影響教翩,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜贪壳,卻給世界環(huán)境...
    茶點故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一饱亿、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧闰靴,春花似錦彪笼、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽队寇。三九已至膘掰,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背识埋。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工凡伊, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人窒舟。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓系忙,卻偏偏與公主長得像,于是被迫代替她去往敵國和親惠豺。 傳聞我的和親對象是個殘疾皇子银还,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,976評論 2 355

推薦閱讀更多精彩內(nèi)容