驳《阿里「Java开发手册」中的1个bug》?

这是我的第 211 期分享

作者 | 王磊

来源 | Java中文社群(ID:javacn666)

转载请联系授权(微信ID:GG_Stone)

前两天写了一篇关于《阿里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开发手册》中的 1 个bug!

阿里新版《Java 开发手册(泰山版)》内容解读(附下载地址)

关注下方二维码,每一天都有干货!

点亮“在看”,助我写出更多好文!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/546082.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Java String indexOf(String substr,int fromIndex)方法,带示例

字符串indexOf(String substr&#xff0c;int fromIndex)方法 (String indexOf(String substr, int fromIndex) Method) indexOf(String substr, int fromIndex) is a String method in Java and it is used to get the index of a specified substring in the string from giv…

zoj 1078 palindrom numbers

题目见zoj 1078 主要是判断一个整数在基数为2-16之间的某个数字时是否为回文&#xff0c;我是直接该整数转换成对应基数的表示的逆序列&#xff0c;并计算出该表示下的值&#xff0c;判断是否等于这个整数值&#xff0c;如果相等&#xff0c;那么就是回文&#xff0c;如果不相…

iredmail邮件服务器之修改默认的web服务端口号

安装iredmail之后&#xff0c;由于需要在路由器上做端口映射以便在外网访问webmail&#xff0c;因此端口不能和WEB服务的端口好冲突&#xff0c;所以需要修改邮件服务器的httpd服务的端口。 一、apache/httpd的http服务和https服务端口号都要修改。 基本服务端口好办&#xff0…

轻松学算法的秘密!可视化算法网站汇总!(附动图)

对于「算法」的第一印象&#xff0c;我相信大部分人都是一样的&#xff0c;就是一个“难”字了得。而我比较特殊&#xff0c;我的第一印象、第二印象以至第 N 印象都觉得很难&#xff0c;所以为了更好的学习和理解算法&#xff0c;我千金一掷一下买了一堆的算法书&#xff0c;有…

从100套真题中提炼而出的100个经典句子

1. Typical of the grassland dwellers of the continent is the American antelope, or pronghorn. 1.美洲羚羊&#xff0c;或称叉角羚&#xff0c;是该大陆典型的草原动物。 2. Of the millions who saw Haley’s comet in 1986, how many people will live long enough to s…

字符串大写转小写库函数_PHP程序无需使用库函数即可将字符串转换为大写

字符串大写转小写库函数Given a string and we have to convert it into uppercase string without using any library function. 给定一个字符串&#xff0c;我们必须在不使用任何库函数的情况下将其转换为大写字符串。 PHP code: PHP代码&#xff1a; <?php//function …

zoj 1091 Knight Moves

题目见zoj 1091 使用宽度搜索优先来求解&#xff0c;这个算法已经忘记的差不多了&#xff0c;所以写出来的代码很罗嗦&#xff0c;看起来很不清晰。 好像还可以直接用公式或者神经网络算法求解,详见Knights Tour /* zoj 1091 Knight Moves */ #include <stdio.h> #incl…

1.基本类型的指针

例子一&#xff1a; #include<stdio.h>int main() {int i10;int * p&i; //p是个变量名字&#xff0c;int * 表示该p变量只能存储int类型变量的地址。//int *p&i;等价于 int *p; p&i;//p存放了i的地址,p指向i,*p就是i变量本身printf("%d\n",p);// …

VB中KeyCode常数用法 VB 按键

VB中KeyCode常数用法可在代码中的任何地方用下列常数代替实际值&#xff1a;常数 值 描述vbKeyLButton 0x1 鼠标左键vbKeyRButton 0x2 鼠标右键vbKeyCancel 0x3 CANCEL 键vbKeyMButton 0x4 鼠标中键vbKeyBack 0x8 BACKSPACE 键vbKeyTab 0x9 TAB 键vbKeyClear 0xC CLEAR 键vbKey…

zoj 1088 System Overload

约瑟夫环 (josephus problem &#xff09;问题&#xff0c;有公式 可以直接套用 我使用暴力破解方法求解&#xff08;用时3秒多&#xff09;。 代码如下&#xff1a; /* zoj 1088 System Overload */ #include <stdio.h> #include <string.h>#define MAXBUILDING …

链表竟然比数组慢了1000多倍?(动图+性能评测)

这是我的第 215 期分享作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;数组和链表是程序中常用的两种数据结构&#xff0c;也是面试中常考的面试题之一。然而对于很多人来说…

c语言用宏定义常量_使用宏定义常量以在C的数组声明中使用

c语言用宏定义常量As we know that, while declaring an array we need to pass maximum number of elements, for example, if you want to declare an array for 10 elements. You need to pass 10 while declaring. Example: int arr[10]; 众所周知&#xff0c;在声明数组时…

HTML字体颜色设置

html字体颜色设置behavior移动效果>插入文字,alternate(交替滚动)、slide(幻灯片效果,指的是滚动一次,然后停止滚动) scrollAmount"移动速度" direction"方向"字体颜色设置字体< f o n t f a c e 宋 体 c o l o r # 9 9 3 2 c c s i z e 5 > 字…

zoj 1025 Wooden Sticks

题目见&#xff1a;zoj 1025 先对木棒按照长度进行排序&#xff0c;然后再计算对应重量构成的数组中非递减子序列的个数。 相关代码&#xff08;悲催的是该代码不能在poj1065 和hdoj1051 下通过&#xff0c;懒得看具体是什么原因了&#xff09; /* zoj 1025 Wooden sticks *…

win7安装python

2019独角兽企业重金招聘Python工程师标准>>> 官网下载 python 安装包&#xff0c;一个 .msi 文件&#xff0c;双击安装 右键[计算机]-属性-高级系统设置-环境变量&#xff0c;在[系统变量]中找到 Path 变量&#xff0c;双击&#xff0c;在值后面添加 python 的安装路…

漫画:Java如何实现热更新?

Arthas&#xff08;阿尔萨斯&#xff09;是 Alibaba 开源的一款 Java 诊断工具&#xff0c;使用它我们可以监控和排查 Java 程序&#xff0c;然而它还提供了非常实用的 Java 热更新功能。所谓的 Java 热更新是指在不重启项目的情况下实现代码的更新与替换。使用它可以实现不停机…

十进制数转换为十六进制数_十进制数制到十六进制数制的转换

十进制数转换为十六进制数Conversion of decimal number system into hexadecimal number system can be done by successively dividing an integral part by 16 till the quotient is 0 and then reading the remainder of all in the bottom to the top manner, where the b…

编译器开发相关资源

开发编译器相关的一些网络资源&#xff1a; how difficult is it to write a compiler? wikipedia compiler compiler 101 code competetion pl/0 (couses &#xff0c;source code ,another source code )and pl/I language the gentle compiler construction sys…

计算机二级考试C++考试大纲

基本要求: 1. 掌握C语言的基本语法规则。 2. 熟练掌握有关类与对象的相关知识。 3. 能够阅读和分析C程序。 4. 能够采用面向对象的编程思路和方法编写应用程序。 5. 能熟练使用Visual C6.0集成开发环境编写和调度程序。 考试内容&#xff1a; 一、 C语言概述 1. 了解C语言的基本…