前言
這篇文章從實際問題 -> 問題解決步驟 -> 問題解決思路,幫助大家能夠明白如何在程序中發現問題,定位問題,解決問題。並真正理解那些問題解決思路。
首先說說這個實際問題是什么,又是怎么遇到的。
我這邊做了一個操作日志模塊,需要提供獨立查詢頁面。正好集團內部有一個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
- 通過遠程debug的斷點,將問題范圍確定在應用服務之前,進而判斷大概率在tomcat(當然也可能是在應用服務前的nginx等。不過在當前場景優先排查tomcat)。
- 通過400錯誤頁面的樣式,聯想到tomcat其他錯誤頁面,猜測問題發生在tomcat。
- 通過資料查詢,網站搜索,找到初步解決方案(這里需要重視搜索關鍵詞,關鍵詞的准確性決定了查詢效率)
SpringBoot版本沖突問題
- 通過日志中的錯誤信息,配合搜索引擎,猜測錯誤是由於版本沖突造成。
- 通過代碼的增量,判斷問題發生在SpringBoot2.x的依賴上。
- [可選] 這里可以通過demo,確定問題是否版本沖突造成(這里的demo必須精准,否則就多准備幾個)。
- 通過maven查看項目依賴樹,確定是由於SpringBoot2.x與SpringBoot1.x的版本沖突造成。
- 查詢資料,沒有明確解決方案直接指向。
- 通過提示的tomcat配置信息,找到ServerProperties類
- 在ServerProperties類中,尋找Tomcat相關的類。
- 最終找到TomcatEmbeddedServletContainerFactory,兩者父類與接口吻合,並且可以直接替換原有的TomcatServletWebServerFactory
事后思考了一下,發現上述5-8可以更加簡單。那就是直接查詢SpringBoot1.x中類似TomcatServletWebServerFactory的類。
具體方法就是明確TomcatServletWebServerFactory的父類AbstractEmbeddedServletContainerFactory(實際功能)與接口ResourceLoaderAware(資源注入)。
在SpringBoot1.x中直接查找AbstractEmbeddedServletContainerFactory子類即可(發現有三個,分別對應Tomcat,Netty與Undertow,都實現了ResourceLoaderAware接口)。不過需要確認是否可以直接使用,還是需要再進行轉化等。
小結
再將上述步驟濃縮一下,就是大家常見的:
- 查詢資料
- debug,打斷點
- 代碼跳轉
- 聯想
- 單一變量進行篩選
- ...
相信上面這類總結,大家見得多了。但是真正落實后,效率確實差別很大的。
這篇文章,從實際問題 -> 問題解決步驟 -> 問題解決思路,幫助大家能夠真正明白如何在程序中發現問題,定位問題,解決問題,理解問題解決思路。如果可以的話,希望大家更進一步,學到如何進行這樣的總結。
願與諸君共進步。
