阿里《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,一经查实,立即删除!

相关文章

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

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

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

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

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

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

win7安装python

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

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

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

自动化运维工具Saltstack详细介绍

Saltstack是一个新的基础设施管理工具。目前处于快速发展阶段&#xff0c;可以看做是pssh弱化的Puppet的组合。间接的反映出了saltstack的两大功能&#xff1a;远程执行和配置管理。Saltstack使用Python开发&#xff0c;是一个非常简单易用和轻量级的管理工具。由Master和Minio…

为什么建议你使用枚举?

枚举是 JDK 1.5 新增的数据类型&#xff0c;使用枚举我们可以很好的描述一些特定的业务场景&#xff0c;比如一年中的春、夏、秋、冬&#xff0c;还有每周的周一到周天&#xff0c;还有各种颜色&#xff0c;以及可以用它来描述一些状态信息&#xff0c;比如错误码等。枚举类型不…

数据结构树二叉树计算节点_查找二叉树中叶节点的数量 数据结构

数据结构树二叉树计算节点Algorithm: 算法&#xff1a; One of the popular traversal techniques to solve this kind of problems is level order tree traversal (Read: Level Order Traversal on a Binary Tree) where we use the concept of BFS. 解决此类问题的一种流行…

重磅!阿里推出国产开源JDK!

简介Alibaba Dragonwell 是一款免费的, 生产就绪型Open JDK 发行版&#xff0c;提供长期支持&#xff0c;包括性能增强和安全修复。阿里巴巴拥有最丰富的Java应用场景&#xff0c;覆盖电商&#xff0c;金融&#xff0c;物流等众多领域&#xff0c;世界上最大的Java用户之一。Al…

安装TPCC-MySQL报错

2019独角兽企业重金招聘Python工程师标准>>> 安装TPCC-MySQL做压力测试&#xff0c;由于TPCC-MySQL是bzr工具进行版本控制的&#xff0c;所以要先安装bzr [rootmha_backup /root] #rpm -Uvh http://dl.fedoraproject.org/pub/epel/5/i386/epel-release-5-4.noarch.r…

自由职业的前半年,我是如何度过的?

生活中所受的苦&#xff0c;终会以一种形式回归。我是一个后知后觉的人&#xff0c;从 2009 年毕业到现在&#xff0c;已经有 11 年的光景了&#xff0c;参加工作的前几年我从没想过要快速的提升自己的技能&#xff0c;对待工作也没有全力以赴&#xff0c;这样的迷茫和随大流的…

Eucalyptus常用查询命令

前言&#xff1a; Elastic Utility Computing Architecture for Linking Your Programs To Useful Systems &#xff08;Eucalyptus&#xff09; 是一种开源的软件基础结构&#xff0c;用来通过计算集群或工作站群实现弹性的、实用的云计算。它最初是美国加利福尼亚大学 Santa …

调整灰度图像的大小,而无需在Python中使用任何内置函数

In this program, we will be using two functions of OpenCV-python (cv2) module. Lets see their syntax and descriptions first. 在此程序中&#xff0c;我们将使用OpenCV-python(cv2)模块的两个功能。 首先让我们看看它们的语法和说明。 1) imread():It takes an absolu…

漫画:对象是如何被找到的?句柄 OR 直接指针?

小贴士&#xff1a;想要使用并定位 Java 对象&#xff0c;就要用到 Java 虚拟机栈&#xff08;Java Virtual Machine Stack&#xff09;&#xff0c;它描述的是 Java 方法执行的线程内存模型&#xff1a;每个方法被执行的时候&#xff0c;Java 虚拟机都会同步创建一个栈帧&…

IDEA 不为人知的 5 个骚技巧!真香!

工欲善其事&#xff0c;必先利其器&#xff0c;磊哥最近发现了几个特别棒的 IDEA“骚”技巧&#xff0c;已经迫不及待的想要分享给你了&#xff0c;快上车...1.快速补全行末分号使用快捷键 Shfit Ctrl Enter 轻松实现。2.自带的 HTTP 请求工具IDEA 自带了 HTTP 的测试工具&am…

漫画:Integer 竟然有 4 种比较方法?

代码测试public class IntegerTest {public static void main(String[] args) {Integer i1 127;Integer i2 127;System.out.println(i1 i2);Integer i3 128;Integer i4 128;System.out.println(i3 i4);} }以上代码的执行结果为&#xff1a;truefalse首先&#xff0c;当我…

6种快速统计代码执行时间的方法,真香!(史上最全)

我们在日常开发中经常需要测试一些代码的执行时间&#xff0c;但又不想使用向 JMH&#xff08;Java Microbenchmark Harness&#xff0c;Java 微基准测试套件&#xff09;这么重的测试框架&#xff0c;所以本文就汇总了一些 Java 中比较常用的执行时间统计方法&#xff0c;总共…

连夜整理了几个开源项目,毕设/练手/私活一条龙!

一直以来&#xff0c;总有小伙伴问说&#xff1a;诶&#xff0c;有没有什么好的项目推荐啊&#xff0c;想参考使用。一般用途无非如下几种情况&#xff1a;自学练手&#xff1a;从书本和博客的理论学习&#xff0c;过渡到实践练手吸收项目经验&#xff0c;找工作写简历时能参考…

MPI编程简单介绍

第三章 MPI编程 3.1 MPI简单介绍 多线程是一种便捷的模型&#xff0c;当中每一个线程都能够訪问其他线程的存储空间。因此&#xff0c;这样的模型仅仅能在共享存储系统之间移植。一般来讲&#xff0c;并行机不一定在各处理器之间共享存储&#xff0c;当面向非共享存储系统开发…