前兩天寫了一篇關於《阿里Java開發手冊中的 1 個bug》的文章,評論區有點炸鍋了,基本分爲兩派,支持老王的和質疑老王的。java
首先來講,不管是那一方,我都真誠的感謝大家。特別是「二師兄」,原本是打算週五晚上好好休息一下的(週五晚上發佈的文章),結果由於和我討論這個問題,一直搞到晚上 12 點左右,能夠看出,他對技術的那份癡迷。這一點咱們是同樣的,和閱讀本文的你同樣,咱們屬於一類人,一類對技術無限癡迷的人。spring
其實在準備發這篇文章時,已經預料到這種局面了,當你提出質疑時,不管對錯,必定有相反的聲音,由於別人也有質疑的權利,而此刻你要作的,就是儘可能保持冷靜,用客觀的態度去理解和驗證這些問題。而這些問題(不一樣的聲音)終將成爲一筆寶貴的財富,由於你在這個驗證的過程當中必定會有所收穫。編程
同時我也但願個人理解是錯的,由於和你們同樣,也是阿里《Java開發手冊》的忠實「信徒」,只是意外的窺見了「不一樣」,而後順手把本身的思路和成果分享給了你們。app
但我也相信,任何「權威」都有犯錯的可能,老祖宗曾告訴過咱們「人非聖賢孰能無過」。我倒不是非要糾結誰對誰錯,相反我認爲一味的追求誰對誰錯是件很是幼稚的事情,只有小孩子才這樣作,咱們要作的是經過辯論這件事的「對與錯」,學習到更多的知識,幫助咱們更好的成長,這纔是我今天這篇文章誕生的意義。框架
喬布斯曾說過:我最喜歡和聰明人一塊兒工做,由於徹底不用顧忌他們的尊嚴。我倒不是聰明人,但我知道任何一件「錯事」的背後,必定有它的價值。所以我不怕被「打臉」,若是想要快速成長的話,我勸你也要這樣。工具
好了,就聊這麼多,接下來我們進入今天正題。性能
持不一樣見解的朋友的主要觀點有如下這些:學習
我把這些意見整理了一下,其實說的是一件事,咱們先來看原文的內容。測試
在《Java開發手冊》泰山版(最新版)的第二章第三小節的第 4 條規範中指出:ui
【強制】在日誌輸出時,字符串變量之間的拼接使用佔位符的方式。說明:由於 String 字符串的拼接會使用 StringBuilder 的 append() 方式,有必定的性能損耗。使用佔位符僅 是替換動做,能夠有效提高性能。
正例:logger.debug("Processing trade with id: {} and symbol: {}", id, symbol);
反對者(注意這個「反對者」不是貶義詞,而是爲了更好的區分角色)的意思是這樣的:
使用佔位符會先判斷日誌的輸出級別再決定是否要進行拼接輸出,而直接使用 StringBuilder 的方式會先進行拼接再進行判斷,這樣的話,當日志級別設置的比較高時,由於 StringBuilder 是先拼接再判斷的,所以形成系統資源的浪費,因此使用佔位符的方式比 StringBuilder 的方式性能要高。
咱先放下反對者說的這個含義在阿里《Java開發手冊》中是否有體現,由於我確實沒有看出來,我們先順着這個思路來證明一下這個結論是否正確。
仍是老規矩,我們用數據和代碼說話,爲了測試 JMH(測試工具)能和 Spring Boot 很好的結合,首先咱們要作的就是先測試一下日誌輸出級別設置,是否能在 JMH 的測試代碼中生效。
那麼接下來咱們先來編寫「日誌級別設置」的測試代碼:
import lombok.extern.slf4j.Slf4j; import org.openjdk.jmh.annotations.*; import org.openjdk.jmh.runner.Runner; import org.openjdk.jmh.runner.RunnerException; import org.openjdk.jmh.runner.options.Options; import org.openjdk.jmh.runner.options.OptionsBuilder; import org.springframework.boot.SpringApplication; import java.util.concurrent.TimeUnit; @BenchmarkMode(Mode.AverageTime) // 測試完成時間 @OutputTimeUnit(TimeUnit.NANOSECONDS) @Warmup(iterations = 2, time = 2, timeUnit = TimeUnit.SECONDS) // 預熱 2 輪,每次 2s @Measurement(iterations = 5, time = 3, timeUnit = TimeUnit.SECONDS) // 測試 5 輪,每次 3s @Fork(1) // fork 1 個線程 @State(Scope.Thread) // 每一個測試線程一個實例 @Slf4j public class LogPrintAmend { public static void main(String[] args) throws RunnerException { // 啓動基準測試 Options opt = new OptionsBuilder() .include(LogPrintAmend.class.getName() + ".*") // 要導入的測試類 .build(); new Runner(opt).run(); // 執行測試 } @Setup public void init() { // 啓動 spring boot SpringApplication.run(DemoApplication.class); } @Benchmark public void logPrint() { log.debug("show debug"); log.info("show info"); log.error("show error"); } }
在測試代碼中,咱們使用了 3 個級別的日誌輸出指令:debug
級別、 info
級別和 error
級別。
而後咱們再在配置文件(application.properties)中的設置日誌的輸出級別,配置以下:
logging.level.root=info
能夠看出咱們把全部的日誌輸出級別設置成了 info
級別,而後咱們執行以上程序,執行結果以下:
從上圖中咱們能夠看出,日誌只輸出了 info
和 error
級別,也就是說咱們設置的日誌輸出級別生效了,爲了保證萬無一失,咱們再把日誌的輸出級別降爲 debug
級別,測試的結果以下圖所示:
從上面的結果能夠看出,咱們設置的日誌級別沒有任何問題,也就是說,JMH 框架能夠很好的搭配 SpringBoot 來使用。
小貼士,日誌的等級權重爲:TRACE < DEBUG < INFO < WARN < ERROR < FATAL
有了上面日誌等級的設置基礎以後,咱們來測試一下,若是先拼接字符串再判斷輸出的性能和佔位符的性能評測結果,完整的測試代碼以下:
import lombok.extern.slf4j.Slf4j; import org.openjdk.jmh.annotations.*; import org.openjdk.jmh.runner.Runner; import org.openjdk.jmh.runner.RunnerException; import org.openjdk.jmh.runner.options.Options; import org.openjdk.jmh.runner.options.OptionsBuilder; import org.springframework.boot.SpringApplication; import java.util.concurrent.TimeUnit; @BenchmarkMode(Mode.AverageTime) // 測試完成時間 @OutputTimeUnit(TimeUnit.NANOSECONDS) @Warmup(iterations = 2, time = 2, timeUnit = TimeUnit.SECONDS) // 預熱 2 輪,每次 2s @Measurement(iterations = 5, time = 3, timeUnit = TimeUnit.SECONDS) // 測試 5 輪,每次 3s @Fork(1) // fork 1 個線程 @State(Scope.Thread) // 每一個測試線程一個實例 @Slf4j public class LogPrintAmend { private final static int MAX_FOR_COUNT = 100; // for 循環次數 public static void main(String[] args) throws RunnerException { // 啓動基準測試 Options opt = new OptionsBuilder() .include(LogPrintAmend.class.getName() + ".*") // 要導入的測試類 .build(); new Runner(opt).run(); // 執行測試 } @Setup public void init() { SpringApplication.run(DemoApplication.class); } @Benchmark public void appendLogPrint() { for (int i = 0; i < MAX_FOR_COUNT; i++) { // 循環的意圖是爲了放大性能測試效果 // 先拼接 StringBuilder sb = new StringBuilder(); sb.append("Hello, "); sb.append("Java"); sb.append("."); sb.append("Hello, "); sb.append("Redis"); sb.append("."); sb.append("Hello, "); sb.append("MySQL"); sb.append("."); // 再判斷 if (log.isInfoEnabled()) { log.info(sb.toString()); } } } @Benchmark public void logPrint() { for (int i = 0; i < MAX_FOR_COUNT; i++) { // 循環的意圖是爲了放大性能測試效果 log.info("Hello, {}.Hello, {}.Hello, {}.", "Java", "Redis", "MySQL"); } } }
能夠看出代碼中使用了 info
的日誌數據級別,那麼此時咱們再將配置文件中的日誌級別設置爲大於 info
的 error
級別,而後執行以上代碼,測試結果以下:
哇,測試結果然使人滿意。從上面的結果能夠看出使用佔位符的方式的性能,真的比 StringBuilder
的方式高不少,這就說明阿里的《Java開發手冊》說的沒問題嘍。
但事情並無那麼簡單,就好比你正在路上走着,迎面而來了一個自行車,眼看就要撞到你了,此時你會怎麼作?毫無疑問你會下意識的躲開。
那麼對於上面的那個評測也是同樣,爲何要在字符串拼接以後再進行判斷呢?
若是編程已是你的一份正式職業,那麼先判斷再拼接字符串是最基礎的職業技能要求,這和你會下意識的躲開迎面相撞的自行車的道理是同樣的,在你徹底有能力規避問題的時候,必定是先規避問題,再進行其餘操做的,不然在團隊 review 代碼的時候或者月底裁人的時候時,你必定是首選的「受害」對象了。由於像這麼簡單的(錯誤)問題,只有剛入門的新手纔可能會出現。
那麼按照一個程序最基本的要求,咱們應該這樣寫代碼:
import lombok.extern.slf4j.Slf4j; import org.openjdk.jmh.annotations.*; import org.openjdk.jmh.runner.Runner; import org.openjdk.jmh.runner.RunnerException; import org.openjdk.jmh.runner.options.Options; import org.openjdk.jmh.runner.options.OptionsBuilder; import org.springframework.boot.SpringApplication; import java.util.concurrent.TimeUnit; @BenchmarkMode(Mode.AverageTime) // 測試完成時間 @OutputTimeUnit(TimeUnit.NANOSECONDS) @Warmup(iterations = 2, time = 2, timeUnit = TimeUnit.SECONDS) // 預熱 2 輪,每次 2s @Measurement(iterations = 5, time = 3, timeUnit = TimeUnit.SECONDS) // 測試 5 輪,每次 3s @Fork(1) // fork 1 個線程 @State(Scope.Thread) // 每一個測試線程一個實例 @Slf4j public class LogPrintAmend { private final static int MAX_FOR_COUNT = 100; // for 循環次數 public static void main(String[] args) throws RunnerException { // 啓動基準測試 Options opt = new OptionsBuilder() .include(LogPrintAmend.class.getName() + ".*") // 要導入的測試類 .build(); new Runner(opt).run(); // 執行測試 } @Setup public void init() { SpringApplication.run(DemoApplication.class); } @Benchmark public void appendLogPrint() { for (int i = 0; i < MAX_FOR_COUNT; i++) { // 循環的意圖是爲了放大性能測試效果 // 再判斷 if (log.isInfoEnabled()) { StringBuilder sb = new StringBuilder(); sb.append("Hello, "); sb.append("Java"); sb.append("."); sb.append("Hello, "); sb.append("Redis"); sb.append("."); sb.append("Hello, "); sb.append("MySQL"); sb.append("."); log.info(sb.toString()); } } } @Benchmark public void logPrint() { for (int i = 0; i < MAX_FOR_COUNT; i++) { // 循環的意圖是爲了放大性能測試效果 log.info("Hello, {}.Hello, {}.Hello, {}.", "Java", "Redis", "MySQL"); } } }
甚至是把 if
判斷提到 for
循環外,但本文的 for
不表明具體的業務,而是爲了更好的放大測試效果而寫的代碼,所以咱們會把判斷寫到 for
循環內。
那麼此時咱們再來執行測試的代碼,執行結果以下圖所示:
從上述結果能夠看出,使用先判斷再拼接字符串的方式仍是要比使用佔位符的方式性能要高。
那麼,咱們依然沒有辦法證實阿里《Java開發手冊》中的佔位符性能高的結論。
因此我依舊保持個人見解,使用佔位符而非字符串拼接,主要能夠保證代碼的優雅性,能夠在代碼中少些一些邏輯判斷,但這樣寫和性能無關。
在上面的評測過程當中,咱們發現日誌的輸出格式很是「亂」,那有沒有辦法能夠格式化日誌呢?
答案是:有的,默認日誌的輸出效果以下:
格式化日誌能夠經過配置 Spring Boot 中的 logging.pattern.console
選項實現的,配置示例以下:
logging.pattern.console=%d | %msg %n
日誌的輸出結果以下:
能夠看出,格式化日誌以後,內容簡潔多了,但千萬不能由於簡潔,而遺漏輸出關鍵性的調試信息。
本文咱們測試了讀者提出質疑的字符串拼接和佔位符的性能評測,發現佔位符方式性能高的觀點依然無從考證,因此咱們的基本見解仍是,使用佔位符的方式更加優雅,能夠經過更少的代碼實現更多的功能;至於性能方面,只能說還不錯,但不能說很優秀。在文章的最後咱們講了 Spring Boot 日誌格式化的知識,但願本文能夠切實的幫助到你,也歡迎你在評論區留言和我互動。
原創不易,都看到這了,點個「贊」再走唄,這是對我最大的支持與鼓勵,謝謝你!