十分鐘通過一個實際問題,真正教會大家如何解決Bug

血夜之末發表於2020-04-28

前言

這篇文章從實際問題 -> 問題解決步驟 -> 問題解決思路,幫助大家能夠明白如何在程式中發現問題,定位問題,解決問題。並真正理解那些問題解決思路。

首先說說這個實際問題是什麼,又是怎麼遇到的。

我這邊做了一個操作日誌模組,需要提供獨立查詢頁面。正好集團內部有一個xxx前端產品,可以簡單配置就生成一個報表頁面。

但是由於該產品請求http介面時,會自動加入一個“sorts=[]”的引數,作為報表排序的依據。但是悲劇發生了。請求後,後端伺服器返回一個400-bad request。

而xxx前端產品的mock資料就沒問題。說明是後端差異造成的。所以需要後端優先解決。

定位問題

斷點定位

通過遠端debug(因為是部署到遠端預發環境的),進行斷點檢視。
首先將斷點打在對應Controller上,發現沒用。
緊接著將斷點打在了DispatcherServlet的doService方法上,發現還是沒用。

那麼再往前就不是SpringBoot這樣的應用服務範圍了,而是屬於Tomcat這樣的Web服務範圍了。
於此同時,我們發現400請求的頁面與tomcat的錯誤頁面很相似。由於很久沒有看到tomcat錯誤頁面,所以並沒有在第一時間察覺。

資料查詢

這時候,已經很難通過斷點方式進行查詢了。但是我們已經有一定的把握將問題定位在tomcat這一web容器了。並且通過之前的介面測試,確定問題發生在“[”這樣字元上。
所以,直接通過百度查詢“tomcat 非法字元 400“這樣的關鍵字,找到了如下的部落格:
解決springboot專案請求出現非法字元問題 java.lang.IllegalArgumentException:Invalid character found in the request target. The valid characters are defined in RFC 7230 and RFC 3986

根據部落格中給出的方法,加入TomcatConfiguration這一配置類:

/**
 * @author: zw
 * @create: 2019-06-27 11:19
 **/
@Configuration
public class TomcatConfig {

    @Bean
    public TomcatServletWebServerFactory webServerFactory() {
        TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
        factory.addConnectorCustomizers((Connector connector) -> {
                connector.setProperty("relaxedPathChars", "\"<>[\\]^`{|}");
                connector.setProperty("relaxedQueryChars", "\"<>[\\]^`{|}");
        });
        return factory;
    }
}

這裡說一下,為什麼沒有采用其他部落格所說的修改tomcat的配置檔案。因為這種偏底層的改變,在大公司裡面實現是非常麻煩的。所以優先考慮通過應用服務自身的設定完成。

但是,問題到這裡就真的結束了嘛?
當然沒有。如果只是如此,我只需要轉載上面那篇部落格就OK了。

版本衝突

問題發生

當時,通過上述方法,在本地的demo已經解決了問題。
但是,集團內部的xxx平臺無法成功將程式碼部署到日常環境。檢視錯誤日誌,並沒有看到有效的資訊。只是一些ClassNotFound等異常,但並沒有具體資訊。

問題定位

簡單搜尋一下相關異常,看到一個關鍵字眼-版本衝突。
結合之前TomcatConfiguration引入的依賴,唯一可能存在問題的就是下面這條依賴:
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;

該依賴,來自SpringBoot 2.x。之前引入該依賴時,比較擔心的是系統採用的是集團內部的xxxboot,可能不相容。但是後來發現系統有引入SpringBoot,所以就沒有擔心了。
現在看來,這裡還是存在問題。
通過maven,發現:原專案採用的是SpringBoot1.x,而不是SpringBoot2.x,所以才會在啟動時,產生版本衝突問題。

問題解決

確定問題位置後,接下來就是解決問題。
但是比較尷尬的是,SpringBoot1.x沒有TomcatConfiguration所需要的TomcatServletWebServerFactory。

所以,接下來就是尋找SpringBoot1.x版本的解決方案。

首先谷歌”SpringBoot1.x tomcat configuration“(這種偏原理的,優先考慮谷歌。尤其是有這個網路條件,並且英文看得懂),找到以下部落格:

How to Configure Spring Boot Tomcat

但是並沒有找到直接的解決方案(相信這也是大家經常遇到的情況)。

這個時候,看到tomcat存在一個application.properties配置項:
server.tomcat.accesslog.enabled=true

因為TomcatConfiguration是@Configuration修飾的配置類,所以直接在實際專案程式碼(SpringBoot1.x)的application.properties增加該屬性,通過屬性跳轉,跳到ServerProperties(位於org.springframework.boot.autoconfigure.web下)。

直接搜尋tomcat,發現了TomcatEmbeddedServletContainerFactory,而這與SpringBoot2.x所使用的TomcatServletWebServerFactory很類似。
開啟程式碼,發現兩者都繼承自AbstractEmbeddedServletContainerFactory,並實現了ResourceLoaderAware介面。

所以針對之前的解決方案,修改成下面樣子就OK了。

package tech.jarry.learning.birdlog.birdlog.config;

import org.apache.catalina.connector.Connector;
//import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * Tomcat配置類:SpringBoot1.x下,解決Tomcat對非法字元返回400問題
 * @author: jarry
 **/
@Configuration
public class TomcatConfiguration {

  @Bean
  public TomcatEmbeddedServletContainerFactory webServerFactory() {
    TomcatEmbeddedServletContainerFactory factory = new TomcatEmbeddedServletContainerFactory();
    factory.addConnectorCustomizers((Connector connector) -> {
      connector.setProperty("relaxedPathChars", "\"<>[\\]^`{|}");
      connector.setProperty("relaxedQueryChars", "\"<>[\\]^`{|}");
    });
    return factory;
  }
}

總結

至此,tomcat非法字元請求直接返回400的問題,就在SpringBoot1.x版本下解決了。

但是,如果只是到此為止,不再向前一步就太可惜了。就如同百米賽跑,跑到99米為止,放棄了。

問題的解決固然重要,但是更重要的是解決問題的思路。因為問題千千萬萬,每個問題的解決方法可能都不一樣。而解決問題的思路才是有限的,才有最為寶貴的。

所以,讓我們覆盤一下上述問題的解決思路:

tomcat對非法字元返回400

  1. 通過遠端debug的斷點,將問題範圍確定在應用服務之前,進而判斷大概率在tomcat(當然也可能是在應用服務前的nginx等。不過在當前場景優先排查tomcat)。
  2. 通過400錯誤頁面的樣式,聯想到tomcat其他錯誤頁面,猜測問題發生在tomcat。
  3. 通過資料查詢,網站搜尋,找到初步解決方案(這裡需要重視搜尋關鍵詞,關鍵詞的準確性決定了查詢效率)

SpringBoot版本衝突問題

  1. 通過日誌中的錯誤資訊,配合搜尋引擎,猜測錯誤是由於版本衝突造成。
  2. 通過程式碼的增量,判斷問題發生在SpringBoot2.x的依賴上。
  3. [可選] 這裡可以通過demo,確定問題是否版本衝突造成(這裡的demo必須精準,否則就多準備幾個)。
  4. 通過maven檢視專案依賴樹,確定是由於SpringBoot2.x與SpringBoot1.x的版本衝突造成。
  5. 查詢資料,沒有明確解決方案直接指向。
  6. 通過提示的tomcat配置資訊,找到ServerProperties類
  7. 在ServerProperties類中,尋找Tomcat相關的類。
  8. 最終找到TomcatEmbeddedServletContainerFactory,兩者父類與介面吻合,並且可以直接替換原有的TomcatServletWebServerFactory

事後思考了一下,發現上述5-8可以更加簡單。那就是直接查詢SpringBoot1.x中類似TomcatServletWebServerFactory的類。

具體方法就是明確TomcatServletWebServerFactory的父類AbstractEmbeddedServletContainerFactory(實際功能)與介面ResourceLoaderAware(資源注入)。
在SpringBoot1.x中直接查詢AbstractEmbeddedServletContainerFactory子類即可(發現有三個,分別對應Tomcat,Netty與Undertow,都實現了ResourceLoaderAware介面)。不過需要確認是否可以直接使用,還是需要再進行轉化等。

小結

再將上述步驟濃縮一下,就是大家常見的:

  • 查詢資料
  • debug,打斷點
  • 程式碼跳轉
  • 聯想
  • 單一變數進行篩選
  • ...

相信上面這類總結,大家見得多了。但是真正落實後,效率確實差別很大的。

這篇文章,從實際問題 -> 問題解決步驟 -> 問題解決思路,幫助大家能夠真正明白如何在程式中發現問題,定位問題,解決問題,理解問題解決思路。如果可以的話,希望大家更進一步,學到如何進行這樣的總結。

願與諸君共進步。

相關文章