阿里《Java开发手册》中的 1 个bug!

这是我的第 210 期分享

作者 | 王磊

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

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

本来打算写一篇《阿里巴巴为什么不允许日志输出时,使用字符串拼接?》的文章,主要是想从性能方面来说此问题,可在文章写到一半进行性能测试时,却发现了一个异常问题,实际测试的结果和手册上描述的结果是截然相反的!

天撸了,怎么会发生这种事情?此时我的内心是拒绝的,因为文章已经写了一半了啊,这让我瞬间陷入了尴尬的境地。

阿里巴巴的《Java开发手册》泰山版(最新版)是这样描述的,它在第二章第三小节的第 4 条规范中指出:

【强制】在日志输出时,字符串变量之间的拼接使用占位符的方式。

说明:因为 String 字符串的拼接会使用 StringBuilder 的 append() 方式,有一定的性能损耗。使用占位符仅 是替换动作,可以有效提升性能

正例:logger.debug("Processing trade with id: {} and symbol: {}", id, symbol);

从上述描述中可以看出,阿里强制要求在日志输出时必须使用占位符的方式进行字符串拼接,因为这样可以有效的提高程序的性能。

然而当我们使用 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 值说明

  • compile:默认值,它表示被依赖项目需要参与当前项目的编译、测试和运行等阶段,在打包时通常也需要添加进去;

  • test:表示依赖项目仅仅参与测试相关的工作,在编译和运行环境下都不会被使用,更别说打包了;

  • provided:适用于编译和测试的阶段,他不会被打包到 lib 目录下;

  • runntime:仅仅适用于运行环境,在编译和测试环境下都不会被使用。

紧接着,我们编写了完整的测试代码:

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(); // 执行测试}@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(".");log.info(sb.toString());}}@Benchmarkpublic 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 performanceStringBuilder 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 variablesif (i == 0) { // this is a simple stringreturn 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 incrementedsbuf.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 slashsbuf.append(messagePattern, i, j - 1);deeplyAppendParameter(sbuf, argArray[L], new HashMap<Object[], Object>());i = j + 2;}} else {// normal casesbuf.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 {@Testvoid 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 开发手册(泰山版)》内容解读(附下载地址)

阿里巴巴为什么让初始化集合时必须指定大小?

关注下方二维码,订阅更多精彩内容

觉得有用,请点击“在看”!

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

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

相关文章

zoj 1006 do the untwist

题目见zoj 1006 或poj 1317 简单的解密算法&#xff0c;直接套用题目中公式即可。 /* zoj 1006 Do the Untwist */ #include <stdio.h> #include <string.h>#define MAXLEN 80 #define MAGICNUM 28char num2Char(int n); int char2Num(char c); int main(void)…

安装完SqlServer2008,wamp服务器无法启动的问题

"开始"->"程序"->Microsoft SQL Server 2008->配置工具->SQL Server配置管理器->SQL Server服务: 只保留SQL Server(MSSQLSERVER)(正在运行)&#xff0c;其他的全部设为停止。 重启wamp服务器成功!

猫版超级玛丽 附下载

不再多言 玩者自知しょぼんのアクション猫版超级玛丽 下载

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

这是我的第 211 期分享作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;前两天写了一篇关于《阿里Java开发手册中的 1 个bug》的文章&#xff0c;评论区有点炸锅了&#xff0…

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 的安装路…