驳《阿里「Java开发手册」中的1个bug》?
前两天写了一篇关于《阿里Java开发手册中的 1 个bug》的文章,议论区有点炸锅了,底子分为两派,支持老王的和质疑老王的。
首要来说,无论是那一方,我都真挚的感谢你们。特别是I G 1 8 U「二师兄W P 9」,原本# 9 w ) r a k是打算周五晚上好好歇息一下的(周五晚上发布的a ` H C c文章),效果因为和我议论这个问题,一向搞到晚上 12Z : + % m K V | 点左右,可以看出,他对技能的那o S T o L & ^ * U份痴迷。这一点我们y e ]是相同的,和阅览本文的你相同,我们归n l 8 V % 于一类人,一类对技能无限痴迷的人。

对与错的含义
其实在准备发这篇文章时,已经预料到这种局势了,当你提出质疑时,无论对错,必定有相反的动静,因为他人也有质疑的权利,而此时你要做的,便f 4 ~ N ^ 8 . &是尽量坚持镇定,用客观的情绪去了解和验证@ M | 9 p ( p . }这些问题。而这些问题(不同的动静)? 6 [ ^ b –终将成为一笔宝贵的财富,因为你在这个验证的过程中必定会有@ 4 1 D O j P [所收获。
一起我也希望我的了解是错的,因为和我们相同,也是阿里《Java开发手册》的忠诚“信徒”,只是意外的窥见了“不同”,然后顺手把自己的思路和效果同享给了我们。
但我也信赖,任何“权威”都有犯错的可能,老祖宗] i 8 g曾告知过我们“人非圣贤孰能无过”。我倒不是非要纠结谁对谁错,相反我以为一味的寻求谁对谁错是件十分天真的作G 3 /业,只需小孩子才这样做,我们要做的是通过争论这R – q t件事的“对与错”,学习到更多的知识,帮忙我们更好的成长,这才是我今天这篇文章诞生的含义。
乔布斯曾说过:我最喜欢和聪明人一起作业,因为完全不用顾忌他们的庄严。我倒不是聪明人,但我知道任何一件“错事”的背面,必定有它的价值。因此我不怕被“打脸”,假设想要快速成长的话,我劝你也要这样。
好了,就聊这么多,接下来我们进入今天正题。
敌对的动静
持不同观点的朋友的首要观念有以下这些:


我p – ]把这些定见整理了一下,其实说的是一件事,我们先来看原文的内容。
在F I q《Java开发手册》泰山版(最新版)的第二章第三末节的第 4 条Y i & , w { A标准中指出:
【强制】在日志输出时,字符串变量之间的拼接运用占位符的办法。
阐明:因为 String 字符串的拼接会运用 StringBuilder 的 append()= X F 办法,有必定的功用损耗。运用占位符仅 是替换动作,可以有效提升s a 8 i (功用。
正例:logger.debug(“ProcessinH + l ! . w :g trade with id$ q m Y v ` t e J: {} and symbol: {}”, id, symbol);
敌对者(留意这个“敌对者”不是贬义词,而是为了更好的区分人物)的意思是这样的:
运用占位符会先判别日志的输出等级再抉择是否要进行拼接输出,而直接运用 StringBuilder 的办法会先进行拼接再进行判别,这样的话,当日志等级设置的比较高时,因为 StringBuilder 是先拼接再判别的,因此形成系统资源的浪费,所以运用占位符的办法比 StringBuilder 的办法功用要高。
咱先放下敌对者说的这个含d + f & { / n t义在阿里《Java[ # { { g & 9 o开发手册》中是否有体现,因为我确实没有看出来,我们先顺着这个思路来证明一下这个结论是否正确。
功用评测
仍是老规矩,我们用数据和Z ^ = v # F a I代码说话,为V w S % F f了检验 JMH(检验东西)能和 Spring Boot 很好的结合,首要我们要做的就是先检验一下日志输出等级设置,是否能在 JMH 的检验代码中收效。
那么接下来我们先来编写「日志等级设置」的检验代码:
import lombok.extern.slf4j.Slf4j;
import org.openjdk.jm$ p : 3 V z 1 ah.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.op@ O { N Yenjdk.jmh.runner.RunnerE] D ! y j 6 P nxception;
import org.openjdk.jm , t . = { tmh.runner.options.OptiA l V 2 ^ *ons;
impoK & u / hrt org.openjdk.jml d O & #h.runner.options.OptionsBuilder;
import org.springframework.boot.SpX t { 2ringApplication;
import java.util.con% v s C Lcurrent.TimeUnit;
@BenchmarkMode(Mode.AverageTime) // 检验结束时刻
@OutputTimeUC 1 Cnih G e yt(TimeUnit.NANOSECONDS)
@Warmup(iterations = 2, time = 2, timec : 6Unit = TimeUnit.SECONDS) // 预热 2 轮,每次 2s
@Measurement(iterations = 5, time = 3, timeUnit = TimeUnit# ! S.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()
.includ0 ] 0 F 1 / Te(LogPriw q :ntAmend.class.getName() + ".*") // 要导入的检验类
.build();
new Runner(opt).run(); // 实行检验
}
@Setup
pub| { , 4lic void init() {
// 发起 spring boot
SpringApplication.run(DemoApplication.class);
}
@Benchmark
public void logPrint() {
log.debug("show debug");
log.info("show info");
log.error("show error% , ; 6 ] x Y (");
}
}
在检验代码中,我们运用了 3 个等级的日志输出指令:debug
等级、 info
等级和 error
等级。
然后我们再在配备文件(application.properties)中的设置日志的输出等级,装V : P C e } 4 X备如下:
logging.level.root=info
可以看出我们把一切的日D = A志输出等级设置成: H s了 info
等级,然后我们实行以上程序,实行效果如下:

从上图中我们可以看出,日志只输出了 info
和 error
等级,也就是说我们设置的日志输出等级收效了,为了保证满有掌握,我们再把日志的输出等级降为 debug
等级,检验的效果如Y : u ; v .下图所示:

从上面的效果可以看出,我们设置的日志等级没有任何问题,也就是说,Y 2 ! ^ U t A UJMH 结构可以很好的分配 SpringBoot 来运用。
小贴士,日志的等级权重为:TO % 2 a &RACEf i b < DEBUG < INFO < WARN < EH l , 2 { ] = @RROR < FATAL
有了上面日志等级的设置基础之后,我们来检验一下,假设先拼接字符串再判别输出的功用和占位符的功用评测效果,完好的检验代码如下:
import lombok.extern.slf$ T k % . w C v }4j.Slf4j;4 @ i 9 t b f
imporO Q z 8 | - . Ft org.openjdk.jmh.annotations.*;
ip T S T dmport org.openjdk.jmh.runner.1 8 { { ^ 7 gRunner;
import org.openjdk.U | 2 fjmh.runner.RunnerException;
import org.2 W v : o l K 7opU , H B O - m /enjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
impor( D $ Q Ct org.springframewo/ 0 ; w 9rk.boo+ l Wt.SpringApplication;
import java.util.concurrent.TimeUnit;
@BenchmarkMode(Mode.AverageTime) // 检验结束时刻
@OutputTimeUnit(TimeUnit.NANOSECm & j T s I 3 #ONDS)
@Warmup(iterations = 2, time = 2, timeUnit = TimeUnit.SE& ] i (CONDS) // 预热m Z , 6 x M 2 轮,每次 2sa H d % N q
@Measurement(iteratr ^ - ~ & u Yions = 5, time = 3, timeUnit = TimeUnit.SECONDS) // 检验 5 轮,每次 3s
@Fork(1z f K F ~ 1 ` Y) // fork 1 个线程
@State(Scope.Thread) // 每个检验线程一个实例
@Slf4j
public class LogPrintAmend {
private final static int MAX_FOR_COUNT = 100;N G h a ] ^ S i s // for 循环次数
public static void main(St! % [ f n c i oring[]X 6 u B - ~ args) throws RunnerException {
// 发起基准检验
Options opt = new OptionsBuild# 2 q A % ; M (eru { F | p h 5()
.include(LogPrintAmend.class.getName() + ".*") // 要导入的测C I n } F e g ? 1验类
.build();
new Runn[ @ 9 b F R fer(opt).run(); // 实行检验
}
@Setup
public void init() {
Sp} h M /ringApplication.run(DemoApplication.class);
}
@Benchmark
public void appendLogPr/ X B I o Z Sint() {
fC g @ V ) w ?or (int i = 0; i < MAX_FOR_COUNT; i++) { // 循环的意图是为了扩展功z { } B H $ s H能检验效果
//b Y p f M + 5 9 先拼接
StringBuill ^ o A K xder sb = new StringBuilder();
sb.append("Hello, ");
sb.append("Java");
sb.append(".");
sb.append("Hello, ");
sb.appendW o } a w 5("Redis");
sb.append(".");
sb.append("Hello, ");
sb.append("MySQL");
sb.append(".");
// 再判别
if (log8 / ; 5 W - Q ~ o.isInfoEnabled()) {
log.info(sb.toString());
}
}
}
@Benchmark
public void logPrint() {
for (int i = 0; i &Q ^ B Alt; MAX_FOR_COUNT; i++) { // 循环的意图是为了扩展功用检验效果
log.il j ~ q o } _nfo("Hello, {}.Hello, {}.Hello, {}.N B c", "t S M ? 6 : +Java", "Redis", "MySQL");
}
}
}
可以看出代码中运用了 info
的日志数据等级,那么此时我们再将配备文件中的日志等级设置为大于 infoy O W q g k l L
的 error
等级,然后实行以上代码,检验效果如下:

哇,检验效果真令人满足。从上面的效果可以看出运用占位符的办法的功用,真的比 StringBuilder
的办法高许多,这就阐明阿里的《Jav– G C 1 H c qa开发手册》说的没问题喽。
回转
但作业并没有那么简略,就比方你正在路上走着,迎面而来了一个自行车,眼看就要撞到你了,此时你会怎么做?毫无疑问你会下知道的躲开。
那么对_ 0 k ~ h于上面的那个评测也是相同,为什么要在字符串拼接之后再v I t N 3 H c进行判S U ~ 5别呢?
假设编程已经是你的一份正式职业,那么; u M H r 9 @先判别再拼接字符串是最基础的职业技能要求,这和你会下知道的躲开迎面相撞的自行车的道理是相同的,在你完全有能力躲避问题的时分,必定是先躲避问题,再进行其他操作的,否则在团队 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.L b p 5options.Options;
import org.openjdk.jmh.runner.optioO & 0 X G @ [ns.OptionsBuilder;
import orgT l g % P = D.st I ? w b ` * !pringframework.boot.SpringApplL b Dication;
import java.util.concurrent.TimeUnit;
@4 m . % ? b xBenchmarkMode(Mode.AverageTime) // 检验结束时刻
@OutputTimeUnit(TimeUnit.NANOSECB f V G d 3ONDS)
@Warmu$ { ; Up(iterations = 2, time =L 9 H R M 4 2, timeUnit = TimeUnit.SEC[ | n p d gONDS) // 预热 2 轮,每次 2s
@Measurement(iterations = 5, time = 3, timeUnit = TimeUx n p 9 K 7 qnit.SECONDS) // 检验 5 轮,每次 3s
@Fork(1l B 5 g E) // fork 1 个线程
@State(Scope.Thread) // 每个检验线程一个实例
@Slf4j
public class LogPr( } i D h G =intAmend {
prb % ] y + N Tivate final static int MAX_FOR_COUNT = 100; // for 循环次数
public static} g J void main(String[] args) throws RunnerException {
// 发起基准检验
Opt0 w Rions opt = new OptionsBuilder()
.include(LogPrintAmend.n C H :class.getName() + ".*") // 要导入的检验类
.build();
new Runner(opt).run(); // 实行检验
}
@Setup
public void init() {
SpringApplication.ruX e A M @ I fn(DemoApplication.class);
}
@Benchmark
public void appendLogPrint() {
for (iC L $nt i& & _ 7 0 O = 0; i < MAX_FOR_CO [ = ~ YUNT; i++) { // 循环的意图是为了扩展功用检验效果
// 再判别
if (log.isInfoEnabled()) {
St# ~ 6 vringBuilder sb = new StringBuilder();
sb.appe- : 2 a * D End("Hello, ");
sb.append("Java");
sb.append(".");
sb.append("Hello, ");
sb.append("Redis");
sb.append(".");
sb.append("Hello, ");
sb.append("MySQL")R X 0 .;
sb.append(".");
log.info(sb.toString());
}
}
}
@Benchmark
public void logPrint() {
for (int i = 0; i < MAi R T u p Z y KX_FOR_COUNT; i++) { // 循环的意图是为了扩展功用检验效果
log.info("Hello, {}.Hello, {}.Hello, {}.M D m s E b f", "Java", "Redis", "MySQL# q U # o n");
}
}
}
乃至是把 if
判别说到 for
循环外,但本文的 for
不代表详细的事务,而是为了更好的扩展检验效果而写的代码,因此我们会把Y T : O N Z判别写到 for
循环内。
那么此时我们再来实行检验的代码,实行效果如下图: 1 P 9 ) k B n M所示:

从上述效果可以看出,运用先判别T c l O a ( H再拼接字符串的方1 . 2 / ] 5 ^ H法仍是要比运用占t & o e 3 +位符的办法功用要高。
那么,我们依然没有办法证明阿里《Java开发手册l e ` Y q C V》中的占位符功用高的结论。
所以我仍旧坚持我的观点,运用占位符而l h l U I S D非字符串拼接,首要可以保证代码的典雅性,可以在代码中少些一些逻辑判别,但这样写和功用无关。| 3 @ h | N y ? C
扩展知识:格式化日志
在上面的评测过程中,我们发现日志的输出格式十分“乱”,那F ` m ? $有没有办法可以格式化日志呢?
答案是:有的l 4 * r P 0,默许日志的输出效果如下:

格式化日志可以通过配备 Spring Boot 中的 logging.pattern.console
选项完结的,配备示例如下:
logging.pattern.console=%d | %msg %n
日志的输出效果如下:

可以看出,格式化日志之后,内容简洁多了,但千万不能因为简洁,而遗失输出关键性的调试信息。
总结
本文我们检验了读者提出质疑的字符串拼接和占位符的功用评测,发现占位符办法功用高的观念依然无从考证,所以我们的底子观点仍v ) v ( o f是,运用占位符的办法愈加典雅,可以通过更少的代码完结更多的功用;至于功用方面,只能说还不错,但不能说很优秀。在文章的终究我们讲了 Spring Boot 日志格式化的知识,希望本文可以实在的帮忙到你,也欢迎你在议论区留言和我互动。
终究的话
原创不易,都看到这了,点个「G Z k ! z e赞」再走呗,这是对我最大的支持与鼓励,谢谢你!
关注大众号「Java中文社群」回复“干货”,获取 50 篇原创干货 Top 榜] v M A j 3 A U。
