原本打算寫一篇《阿里巴巴爲何不容許日誌輸出時,使用字符串拼接?》的文章,主要是想從性能方面來講此問題,可在文章寫到一半進行性能測試時,卻發現了一個異常問題,實際測試的結果和手冊上描述的結果是截然相反的!java
天擼了,怎麼會發生這種事情?此時個人心裏是拒絕的,由於文章已經寫了一半了啊,這讓我瞬間陷入了尷尬的境地。web
阿里巴巴的《Java開發手冊》泰山版(最新版)是這樣描述的,它在第二章第三小節的第 4 條規範中指出:spring
【強制】在日誌輸出時,字符串變量之間的拼接使用佔位符的方式。api
說明:由於 String 字符串的拼接會使用 StringBuilder 的 append() 方式,有必定的性能損耗。使用佔位符僅 是替換動做,能夠有效提高性能。app
正例:logger.debug("Processing trade with id: {} and symbol: {}", id, symbol);框架
從上述描述中能夠看出,阿里強制要求在日誌輸出時必須使用佔位符的方式進行字符串拼接,由於這樣能夠有效的提升程序的性能。ide
然而當咱們使用 Oracle 官方提供的 JMH(Java Microbenchmark Harness,JAVA 微基準測試套件)框架來測試時,卻發現結果和手冊上描述的徹底不同。工具
PS:對 JMH 不熟悉的朋友,能夠看我發佈的另外一篇文章《Oracle官方推薦的性能測試工具!簡單、精準又直觀!》源碼分析
本文咱們藉助 Spring Boot 2.2.6 來完成測試,首先咱們先在 Spring Boot 的 pom.xml 中添加 JMH 框架的依賴:性能
<!-- https://mvnrepository.com/artifact/org.openjdk.jmh/jmh-core -->
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.23</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.openjdk.jmh/jmh-generator-annprocess -->
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.23</version>
<scope>provided</scope>
</dependency>
複製代碼
這裏須要注意一下,通常的項目咱們只須要添加 jmh-core
的依賴包就能夠了,但若是是 Spring Boot 項目的話,咱們還必須添加 jmh-generator-annprocess
包依賴,而且要把 scope
設置爲 provided
類型,若是使用它的默認值 test
就會致使程序報錯 Unable to find the resource: /META-INF/BenchmarkList
。
scope 值說明:
緊接着,咱們編寫了完整的測試代碼:
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.TimeUnit;
@BenchmarkMode(Mode.AverageTime) // 測試完成時間
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS) // 預熱 2 輪,每次 1s
@Measurement(iterations = 5, time = 3, timeUnit = TimeUnit.SECONDS) // 測試 5 輪,每次 3s
@Fork(1) // fork 1 個線程
@State(Scope.Thread) // 每一個測試線程一個實例
@RestController
@RequestMapping("/log")
public class LogPrint {
private final Logger log = LoggerFactory.getLogger(LogPrint.class);
private final static int MAX_FOR_COUNT = 100; // for 循環次數
public static void main(String[] args) throws RunnerException {
// 啓動基準測試
Options opt = new OptionsBuilder()
.include(LogPrint.class.getName() + ".*") // 要導入的測試類
.build();
new Runner(opt).run(); // 執行測試
}
@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(".");
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");
}
}
}
複製代碼
測試結果以下:
從上述結果能夠看出直接使用 StringBuilder
拼接的方式顯然要比使用佔位符的方式性能要高,難道是我搞錯了?
備註:測試環境爲 Spring Boot 2.2.6 RELEASE、JDK 8(JDK 1.8.0_10)、MacOS(MacMini 2018)
抱着懷疑的態度,咱們打開了 slf4j 的源碼,看看佔位符的底層方法究竟是如何實現的,因而我就順着 log.info
方法找到了佔位符最終的實現源碼:
final public static FormattingTuple arrayFormat(final String messagePattern, final Object[] argArray, Throwable throwable) {
if (messagePattern == null) {
return new FormattingTuple(null, argArray, throwable);
}
if (argArray == null) {
return new FormattingTuple(messagePattern);
}
int i = 0;
int j;
// use string builder for better multicore performance
StringBuilder sbuf = new StringBuilder(messagePattern.length() + 50);
int L;
// for 循環替換佔位符
for (L = 0; L < argArray.length; L++) {
j = messagePattern.indexOf(DELIM_STR, i);
if (j == -1) {
// no more variables
if (i == 0) { // this is a simple string
return new FormattingTuple(messagePattern, argArray, throwable);
} else { // add the tail string which contains no variables and return
// the result.
sbuf.append(messagePattern, i, messagePattern.length());
return new FormattingTuple(sbuf.toString(), argArray, throwable);
}
} else {
if (isEscapedDelimeter(messagePattern, j)) {
if (!isDoubleEscaped(messagePattern, j)) {
L--; // DELIM_START was escaped, thus should not be incremented
sbuf.append(messagePattern, i, j - 1);
sbuf.append(DELIM_START);
i = j + 1;
} else {
// The escape character preceding the delimiter start is
// itself escaped: "abc x:\\{}"
// we have to consume one backward slash
sbuf.append(messagePattern, i, j - 1);
deeplyAppendParameter(sbuf, argArray[L], new HashMap<Object[], Object>());
i = j + 2;
}
} else {
// normal case
sbuf.append(messagePattern, i, j);
deeplyAppendParameter(sbuf, argArray[L], new HashMap<Object[], Object>());
i = j + 2;
}
}
}
// append the characters following the last {} pair.
sbuf.append(messagePattern, i, messagePattern.length());
return new FormattingTuple(sbuf.toString(), argArray, throwable);
}
複製代碼
從上述源碼能夠看出,所謂的佔位符其實底層也是使用 StringBuilder
來實現的,怪不得性能不如直接使用 StringBuilder
。由於在進行佔位符替換的時候,還通過了一些列的驗證才進行替換的,而直接使用 StringBuilder
則能夠省去這部分效驗的工做。
爲了保證我沒有搞錯,因而我使用 Idea 開啓了調試模式,調試的結果以下圖所示:
從上圖能夠看出,此方法就是佔位符的實際執行方法,那也就是說,手冊上寫的性能問題確實是錯的。
因而我就隨手發了一個朋友圈:
卻發如今寫紙質書的編輯也剛好是個人好友:
這樣就能夠避免這個問題,會直接出如今將來的紙質書中,也算是功勞一件了。
咱們在 Spring Boot 中使用日誌一般會這樣寫:
private final Logger log = LoggerFactory.getLogger(LogPrint.class);
複製代碼
而且每一個類中都要寫這樣一行代碼,未免有些麻煩。
此時咱們可使用 @Slf4j
註解來替代上面的那行 Logger
對象建立的代碼,完整使用示例以下:
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
@Slf4j
class LogTest {
@Test
void show() {
log.debug("Hello, {}.", "Debug");
log.info("Hello, {}.", "Info");
log.error("Hello, {}.", "Error");
}
}
複製代碼
程序的執行結果:
14:33:18.377 [main] DEBUG com.example.demo.LogTestTest - Hello, Debug.
14:33:18.382 [main] INFO com.example.demo.LogTestTest - Hello, Info.
14:33:18.382 [main] ERROR com.example.demo.LogTestTest - Hello, Error.
從上述結果能夠看出,日誌已經正常輸出到控制檯了。
注意:@Slf4j 註解屬於 lombok,所以要想在項目中使用 @Slf4j 註解,須要保證項目中已經添加了 lombok 的依賴。
在進行日誌輸出時,字符串變量之間的拼接要使用佔位符的方式,由於這樣寫會比較優雅。咱們查了 slf4j 的源碼發現,佔位符的底層也是經過 StringBuilder
拼接來實現的。
舊文章:
爲了響應國家「低碳生活,綠色出行」的號召,老王決定之後要騎自行車上下班,不但省錢並且還不堵車,最主要的是能爲國家出一份力。爲了達成這個目標,老王 happy 的掏出了他的諾基亞老年機,一頓操做在某寶買了一輛 8 成新的二八自行車,幻想着今後能過上了幸福生活。
但車子買回來以後卻發現,村東頭已經放滿了 2 元錢就能騎一個月的共享自行車,你說氣人不?不但如此,老王還發現本身買回來的車子不只很差騎,還不支持七天無理由退貨,這給老王氣的,回手就是一個五星差評。
在看賣家這邊,反而一點都不着急,只是冷冷的給回了一句:小子,我還怕你?這月幹完我就全職擺地攤了,網店反正也不開了,差評就差評唄。
這個故事深入告訴咱們一個道理:若是別人已經給咱們提供好了「自行車」,那咱們就不用本身花錢再賣一個了。
看到這裏有人可能會問了:小夥子,你這是正經的技術文章不?你說的這些和技術有毛關係啊?
這個故事和今天的主題還真有關係,下面咱們正式開始...
關注公衆號「Java中文社羣」回覆「乾貨」,獲取 50 篇原創乾貨 Top 榜。