java string 占位符_驳《阿里「Java开发手册」中的1个bug》?
前兩天寫了一篇關于《阿里Java開發手冊中的 1 個bug》的文章,評論區有點炸鍋了,基本分為兩派,支持老王的和質疑老王的。
首先來說,無論是那一方,我都真誠的感謝你們。特別是「二師兄」,本來是打算周五晚上好好休息一下的(周五晚上發布的文章),結果因為和我討論這個問題,一直搞到晚上 12 點左右,可以看出,他對技術的那份癡迷。這一點我們是一樣的,和閱讀本文的你一樣,我們屬于一類人,一類對技術無限癡迷的人。
對與錯的意義
其實在準備發這篇文章時,已經預料到這種局面了,當你提出質疑時,無論對錯,一定有相反的聲音,因為別人也有質疑的權利,而此刻你要做的,就是盡量保持冷靜,用客觀的態度去理解和驗證這些問題。而這些問題(不同的聲音)終將成為一筆寶貴的財富,因為你在這個驗證的過程中一定會有所收獲。
同時我也希望我的理解是錯的,因為和大家一樣,也是阿里《Java開發手冊》的忠實“信徒”,只是意外的窺見了“不同”,然后順手把自己的思路和成果分享給了大家。
但我也相信,任何“權威”都有犯錯的可能,老祖宗曾告訴過我們“人非圣賢孰能無過”。我倒不是非要糾結誰對誰錯,相反我認為一味的追求誰對誰錯是件非常幼稚的事情,只有小孩子才這樣做,我們要做的是通過辯論這件事的“對與錯”,學習到更多的知識,幫助我們更好的成長,這才是我今天這篇文章誕生的意義。
喬布斯曾說過:我最喜歡和聰明人一起工作,因為完全不用顧忌他們的尊嚴。我倒不是聰明人,但我知道任何一件“錯事”的背后,一定有它的價值。因此我不怕被“打臉”,如果想要快速成長的話,我勸你也要這樣。
好了,就聊這么多,接下來咱們進入今天正題。
反對的聲音
持不同看法的朋友的主要觀點有以下這些:
我把這些意見整理了一下,其實說的是一件事,我們先來看原文的內容。
在《Java開發手冊》泰山版(最新版)的第二章第三小節的第 4 條規范中指出:
【強制】在日志輸出時,字符串變量之間的拼接使用占位符的方式。
說明:因為 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(); // 執行測試}@Setuppublic void init() {// 啟動 spring bootSpringApplication.run(DemoApplication.class);}@Benchmarkpublic 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(); // 執行測試}@Setuppublic void init() {SpringApplication.run(DemoApplication.class);}@Benchmarkpublic 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());}}}@Benchmarkpublic 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(); // 執行測試}@Setuppublic void init() {SpringApplication.run(DemoApplication.class);}@Benchmarkpublic 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());}}}@Benchmarkpublic 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 日志格式化的知識,希望本文可以切實的幫助到你,也歡迎你在評論區留言和我互動。
最后的話
原創不易,都看到這了,點個「贊」再走唄,這是對我最大的支持與鼓勵,謝謝你!
總結
以上是生活随笔為你收集整理的java string 占位符_驳《阿里「Java开发手册」中的1个bug》?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python删除csv某一行_Pytho
- 下一篇: python获取数据类型_python数