驳《阿里「Java开发手册」中的1个bug》?
這是我的第?211?期分享
作者 | 王磊
來源 | Java中文社群(ID:javacn666)
轉(zhuǎn)載請(qǐng)聯(lián)系授權(quán)(微信ID:GG_Stone)
前兩天寫了一篇關(guān)于《阿里Java開發(fā)手冊(cè)中的 1 個(gè)bug》的文章,評(píng)論區(qū)有點(diǎn)炸鍋了,基本分為兩派,支持老王的和質(zhì)疑老王的。
首先來說,無論是那一方,我都真誠的感謝你們。特別是「二師兄」,本來是打算周五晚上好好休息一下的(周五晚上發(fā)布的文章),結(jié)果因?yàn)楹臀矣懻撨@個(gè)問題,一直搞到晚上 12 點(diǎn)左右,可以看出,他對(duì)技術(shù)的那份癡迷。這一點(diǎn)我們是一樣的,和閱讀本文的你一樣,我們屬于一類人,一類對(duì)技術(shù)無限癡迷的人。
對(duì)與錯(cuò)的意義
其實(shí)在準(zhǔn)備發(fā)這篇文章時(shí),已經(jīng)預(yù)料到這種局面了,當(dāng)你提出質(zhì)疑時(shí),無論對(duì)錯(cuò),一定有相反的聲音,因?yàn)閯e人也有質(zhì)疑的權(quán)利,而此刻你要做的,就是盡量保持冷靜,用客觀的態(tài)度去理解和驗(yàn)證這些問題。而這些問題(不同的聲音)終將成為一筆寶貴的財(cái)富,因?yàn)槟阍谶@個(gè)驗(yàn)證的過程中一定會(huì)有所收獲。
同時(shí)我也希望我的理解是錯(cuò)的,因?yàn)楹痛蠹乙粯?#xff0c;也是阿里《Java開發(fā)手冊(cè)》的忠實(shí)“信徒”,只是意外的窺見了“不同”,然后順手把自己的思路和成果分享給了大家。
但我也相信,任何“權(quán)威”都有犯錯(cuò)的可能,老祖宗曾告訴過我們“人非圣賢孰能無過”。我倒不是非要糾結(jié)誰對(duì)誰錯(cuò),相反我認(rèn)為一味的追求誰對(duì)誰錯(cuò)是件非常幼稚的事情,只有小孩子才這樣做,我們要做的是通過辯論這件事的“對(duì)與錯(cuò)”,學(xué)習(xí)到更多的知識(shí),幫助我們更好的成長,這才是我今天這篇文章誕生的意義。
喬布斯曾說過:我最喜歡和聰明人一起工作,因?yàn)橥耆挥妙櫦伤麄兊淖饑?yán)。我倒不是聰明人,但我知道任何一件“錯(cuò)事”的背后,一定有它的價(jià)值。因此我不怕被“打臉”,如果想要快速成長的話,我勸你也要這樣。
好了,就聊這么多,接下來咱們進(jìn)入今天正題。
反對(duì)的聲音
持不同看法的朋友的主要觀點(diǎn)有以下這些:
我把這些意見整理了一下,其實(shí)說的是一件事,我們先來看原文的內(nèi)容。
在《Java開發(fā)手冊(cè)》泰山版(最新版)的第二章第三小節(jié)的第 4 條規(guī)范中指出:
【強(qiáng)制】在日志輸出時(shí),字符串變量之間的拼接使用占位符的方式。
說明:因?yàn)?String 字符串的拼接會(huì)使用 StringBuilder 的 append() 方式,有一定的性能損耗。使用占位符僅 是替換動(dòng)作,可以有效提升性能。
正例:logger.debug("Processing trade with id: {} and symbol: {}", id, symbol);
反對(duì)者(注意這個(gè)“反對(duì)者”不是貶義詞,而是為了更好的區(qū)分角色)的意思是這樣的:
使用占位符會(huì)先判斷日志的輸出級(jí)別再?zèng)Q定是否要進(jìn)行拼接輸出,而直接使用 StringBuilder 的方式會(huì)先進(jìn)行拼接再進(jìn)行判斷,這樣的話,當(dāng)日志級(jí)別設(shè)置的比較高時(shí),因?yàn)?StringBuilder 是先拼接再判斷的,因此造成系統(tǒng)資源的浪費(fèi),所以使用占位符的方式比 StringBuilder 的方式性能要高。
咱先放下反對(duì)者說的這個(gè)含義在阿里《Java開發(fā)手冊(cè)》中是否有體現(xiàn),因?yàn)槲掖_實(shí)沒有看出來,咱們先順著這個(gè)思路來證實(shí)一下這個(gè)結(jié)論是否正確。
性能評(píng)測
還是老規(guī)矩,咱們用數(shù)據(jù)和代碼說話,為了測試 JMH(測試工具)能和 Spring Boot 很好的結(jié)合,首先我們要做的就是先測試一下日志輸出級(jí)別設(shè)置,是否能在 JMH 的測試代碼中生效。
那么接下來我們先來編寫「日志級(jí)別設(shè)置」的測試代碼:
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)?//?測試完成時(shí)間 @OutputTimeUnit(TimeUnit.NANOSECONDS) @Warmup(iterations?=?2,?time?=?2,?timeUnit?=?TimeUnit.SECONDS)?//?預(yù)熱?2?輪,每次?2s @Measurement(iterations?=?5,?time?=?3,?timeUnit?=?TimeUnit.SECONDS)?//?測試?5?輪,每次?3s @Fork(1)?//?fork?1?個(gè)線程 @State(Scope.Thread)?//?每個(gè)測試線程一個(gè)實(shí)例 @Slf4j public?class?LogPrintAmend?{public?static?void?main(String[]?args)?throws?RunnerException?{//?啟動(dòng)基準(zhǔn)測試Options?opt?=?new?OptionsBuilder().include(LogPrintAmend.class.getName()?+?".*")?//?要導(dǎo)入的測試類.build();new?Runner(opt).run();?//?執(zhí)行測試}@Setuppublic?void?init()?{//?啟動(dòng)?spring?bootSpringApplication.run(DemoApplication.class);}@Benchmarkpublic?void?logPrint()?{log.debug("show?debug");log.info("show?info");log.error("show?error");} }在測試代碼中,我們使用了 3 個(gè)級(jí)別的日志輸出指令:debug?級(jí)別、 info?級(jí)別和 error?級(jí)別。
然后我們?cè)僭谂渲梦募?#xff08;application.properties)中的設(shè)置日志的輸出級(jí)別,配置如下:
logging.level.root=info可以看出我們把所有的日志輸出級(jí)別設(shè)置成了 info 級(jí)別,然后我們執(zhí)行以上程序,執(zhí)行結(jié)果如下:
從上圖中我們可以看出,日志只輸出了 info?和 error?級(jí)別,也就是說我們?cè)O(shè)置的日志輸出級(jí)別生效了,為了保證萬無一失,我們?cè)侔讶罩镜妮敵黾?jí)別降為 debug?級(jí)別,測試的結(jié)果如下圖所示:
從上面的結(jié)果可以看出,我們?cè)O(shè)置的日志級(jí)別沒有任何問題,也就是說,JMH 框架可以很好的搭配 SpringBoot 來使用。
小貼士,日志的等級(jí)權(quán)重為:TRACE < DEBUG < INFO < WARN < ERROR < FATAL
有了上面日志等級(jí)的設(shè)置基礎(chǔ)之后,我們來測試一下,如果先拼接字符串再判斷輸出的性能和占位符的性能評(píng)測結(jié)果,完整的測試代碼如下:
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)?//?測試完成時(shí)間 @OutputTimeUnit(TimeUnit.NANOSECONDS) @Warmup(iterations?=?2,?time?=?2,?timeUnit?=?TimeUnit.SECONDS)?//?預(yù)熱?2?輪,每次?2s @Measurement(iterations?=?5,?time?=?3,?timeUnit?=?TimeUnit.SECONDS)?//?測試?5?輪,每次?3s @Fork(1)?//?fork?1?個(gè)線程 @State(Scope.Thread)?//?每個(gè)測試線程一個(gè)實(shí)例 @Slf4j public?class?LogPrintAmend?{private?final?static?int?MAX_FOR_COUNT?=?100;?//?for?循環(huán)次數(shù)public?static?void?main(String[]?args)?throws?RunnerException?{//?啟動(dòng)基準(zhǔn)測試Options?opt?=?new?OptionsBuilder().include(LogPrintAmend.class.getName()?+?".*")?//?要導(dǎo)入的測試類.build();new?Runner(opt).run();?//?執(zhí)行測試}@Setuppublic?void?init()?{SpringApplication.run(DemoApplication.class);}@Benchmarkpublic?void?appendLogPrint()?{for?(int?i?=?0;?i?<?MAX_FOR_COUNT;?i++)?{?//?循環(huán)的意圖是為了放大性能測試效果//?先拼接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++)?{?//?循環(huán)的意圖是為了放大性能測試效果log.info("Hello,?{}.Hello,?{}.Hello,?{}.",?"Java",?"Redis",?"MySQL");}} }可以看出代碼中使用了 info?的日志數(shù)據(jù)級(jí)別,那么此時(shí)我們?cè)賹⑴渲梦募械娜罩炯?jí)別設(shè)置為大于 info?的 error 級(jí)別,然后執(zhí)行以上代碼,測試結(jié)果如下:
哇,測試結(jié)果真令人滿意。從上面的結(jié)果可以看出使用占位符的方式的性能,真的比 StringBuilder 的方式高很多,這就說明阿里的《Java開發(fā)手冊(cè)》說的沒問題嘍。
反轉(zhuǎn)
但事情并沒有那么簡單,就比如你正在路上走著,迎面而來了一個(gè)自行車,眼看就要撞到你了,此時(shí)你會(huì)怎么做?毫無疑問你會(huì)下意識(shí)的躲開。
那么對(duì)于上面的那個(gè)評(píng)測也是一樣,為什么要在字符串拼接之后再進(jìn)行判斷呢?
如果編程已經(jīng)是你的一份正式職業(yè),那么先判斷再拼接字符串是最基礎(chǔ)的職業(yè)技能要求,這和你會(huì)下意識(shí)的躲開迎面相撞的自行車的道理是一樣的,在你完全有能力規(guī)避問題的時(shí)候,一定是先規(guī)避問題,再進(jìn)行其他操作的,否則在團(tuán)隊(duì) review 代碼的時(shí)候或者月底裁員的時(shí)候時(shí),你一定是首選的“受害”對(duì)象了。因?yàn)橄襁@么簡單的(錯(cuò)誤)問題,只有剛?cè)腴T的新手才可能會(huì)出現(xiàn)。
那么按照一個(gè)程序最基本的要求,我們應(yīng)該這樣寫代碼:
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)?//?測試完成時(shí)間 @OutputTimeUnit(TimeUnit.NANOSECONDS) @Warmup(iterations?=?2,?time?=?2,?timeUnit?=?TimeUnit.SECONDS)?//?預(yù)熱?2?輪,每次?2s @Measurement(iterations?=?5,?time?=?3,?timeUnit?=?TimeUnit.SECONDS)?//?測試?5?輪,每次?3s @Fork(1)?//?fork?1?個(gè)線程 @State(Scope.Thread)?//?每個(gè)測試線程一個(gè)實(shí)例 @Slf4j public?class?LogPrintAmend?{private?final?static?int?MAX_FOR_COUNT?=?100;?//?for?循環(huán)次數(shù)public?static?void?main(String[]?args)?throws?RunnerException?{//?啟動(dòng)基準(zhǔn)測試Options?opt?=?new?OptionsBuilder().include(LogPrintAmend.class.getName()?+?".*")?//?要導(dǎo)入的測試類.build();new?Runner(opt).run();?//?執(zhí)行測試}@Setuppublic?void?init()?{SpringApplication.run(DemoApplication.class);}@Benchmarkpublic?void?appendLogPrint()?{for?(int?i?=?0;?i?<?MAX_FOR_COUNT;?i++)?{?//?循環(huán)的意圖是為了放大性能測試效果//?再判斷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++)?{?//?循環(huán)的意圖是為了放大性能測試效果log.info("Hello,?{}.Hello,?{}.Hello,?{}.",?"Java",?"Redis",?"MySQL");}} }甚至是把 if?判斷提到 for?循環(huán)外,但本文的 for?不代表具體的業(yè)務(wù),而是為了更好的放大測試效果而寫的代碼,因此我們會(huì)把判斷寫到 for?循環(huán)內(nèi)。
那么此時(shí)我們?cè)賮韴?zhí)行測試的代碼,執(zhí)行結(jié)果如下圖所示:
從上述結(jié)果可以看出,使用先判斷再拼接字符串的方式還是要比使用占位符的方式性能要高。
那么,我們依然沒有辦法證明阿里《Java開發(fā)手冊(cè)》中的占位符性能高的結(jié)論。
所以我依舊保持我的看法,使用占位符而非字符串拼接,主要可以保證代碼的優(yōu)雅性,可以在代碼中少些一些邏輯判斷,但這樣寫和性能無關(guān)。
擴(kuò)展知識(shí):格式化日志
在上面的評(píng)測過程中,我們發(fā)現(xiàn)日志的輸出格式非常“亂”,那有沒有辦法可以格式化日志呢?
答案是:有的,默認(rèn)日志的輸出效果如下:
格式化日志可以通過配置 Spring Boot 中的 logging.pattern.console?選項(xiàng)實(shí)現(xiàn)的,配置示例如下:
logging.pattern.console=%d?|?%msg?%n日志的輸出結(jié)果如下:
可以看出,格式化日志之后,內(nèi)容簡潔多了,但千萬不能因?yàn)楹啙?#xff0c;而遺漏輸出關(guān)鍵性的調(diào)試信息。
總結(jié)
本文我們測試了讀者提出質(zhì)疑的字符串拼接和占位符的性能評(píng)測,發(fā)現(xiàn)占位符方式性能高的觀點(diǎn)依然無從考證,所以我們的基本看法還是,使用占位符的方式更加優(yōu)雅,可以通過更少的代碼實(shí)現(xiàn)更多的功能;至于性能方面,只能說還不錯(cuò),但不能說很優(yōu)秀。在文章的最后我們講了 Spring Boot 日志格式化的知識(shí),希望本文可以切實(shí)的幫助到你,也歡迎你在評(píng)論區(qū)留言和我互動(dòng)。
最后的話原創(chuàng)不易,都看到這了,點(diǎn)個(gè)「在看」再走唄,這是對(duì)我最大的支持與鼓勵(lì),謝謝你!往期推薦阿里《Java開發(fā)手冊(cè)》中的 1 個(gè)bug!
阿里新版《Java 開發(fā)手冊(cè)(泰山版)》內(nèi)容解讀(附下載地址)
關(guān)注下方二維碼,每一天都有干貨!
點(diǎn)亮“在看”,助我寫出更多好文!
總結(jié)
以上是生活随笔為你收集整理的驳《阿里「Java开发手册」中的1个bug》?的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 报告老板:这次的缓存事故是这样的...
- 下一篇: 不要再用main方法测试代码性能了,用这