首页 > java string 占位符_驳《阿里「Java开发手册」中的1个bug》?

java string 占位符_驳《阿里「Java开发手册」中的1个bug》?

a2b14de866776cd15f9aa7e04ca976a9.png

前两天写了一篇关于《阿里Java开发手册中的 1 个bug》的文章,评论区有点炸锅了,基本分为两派,支持老王的和质疑老王的。

首先来说,无论是那一方,我都真诚的感谢你们。特别是「二师兄」,本来是打算周五晚上好好休息一下的(周五晚上发布的文章),结果因为和我讨论这个问题,一直搞到晚上 12 点左右,可以看出,他对技术的那份痴迷。这一点我们是一样的,和阅读本文的你一样,我们属于一类人,一类对技术无限痴迷的人

a369e55b5af50095625b7a75d1eb5e19.png

对与错的意义

其实在准备发这篇文章时,已经预料到这种局面了,当你提出质疑时,无论对错,一定有相反的声音,因为别人也有质疑的权利,而此刻你要做的,就是尽量保持冷静,用客观的态度去理解和验证这些问题。而这些问题(不同的声音)终将成为一笔宝贵的财富,因为你在这个验证的过程中一定会有所收获。

同时我也希望我的理解是错的,因为和大家一样,也是阿里《Java开发手册》的忠实“信徒”,只是意外的窥见了“不同”,然后顺手把自己的思路和成果分享给了大家。

但我也相信,任何“权威”都有犯错的可能,老祖宗曾告诉过我们“人非圣贤孰能无过”。我倒不是非要纠结谁对谁错,相反我认为一味的追求谁对谁错是件非常幼稚的事情,只有小孩子才这样做,我们要做的是通过辩论这件事的“对与错”,学习到更多的知识,帮助我们更好的成长,这才是我今天这篇文章诞生的意义。

乔布斯曾说过:我最喜欢和聪明人一起工作,因为完全不用顾忌他们的尊严。我倒不是聪明人,但我知道任何一件“错事”的背后,一定有它的价值。因此我不怕被“打脸”,如果想要快速成长的话,我劝你也要这样。

好了,就聊这么多,接下来咱们进入今天正题。

反对的声音

持不同看法的朋友的主要观点有以下这些:

aaa56dfb113ea6e2089844e609621ca8.png

cdbe0afd72c4b747e2a9d0ecebe13e63.png

我把这些意见整理了一下,其实说的是一件事,我们先来看原文的内容。

在《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 级别,然后我们执行以上程序,执行结果如下:

1d52e44c3e5c9bac8d68846b4793f8e5.png

从上图中我们可以看出,日志只输出了 infoerror 级别,也就是说我们设置的日志输出级别生效了,为了保证万无一失,我们再把日志的输出级别降为 debug 级别,测试的结果如下图所示:

630bf68572f6050818ca3e4e4d4f038f.png

从上面的结果可以看出,我们设置的日志级别没有任何问题,也就是说,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 的日志数据级别,那么此时我们再将配置文件中的日志级别设置为大于 infoerror 级别,然后执行以上代码,测试结果如下:

9a08b4087f7a51a5641efa43cfb60b15.png

哇,测试结果真令人满意。从上面的结果可以看出使用占位符的方式的性能,真的比 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 循环内。

那么此时我们再来执行测试的代码,执行结果如下图所示:

b15ce087b968599d1f83fdf72de40735.png

从上述结果可以看出,使用先判断再拼接字符串的方式还是要比使用占位符的方式性能要高。

那么,我们依然没有办法证明阿里《Java开发手册》中的占位符性能高的结论

所以我依旧保持我的看法,使用占位符而非字符串拼接,主要可以保证代码的优雅性,可以在代码中少些一些逻辑判断,但这样写和性能无关

扩展知识:格式化日志

在上面的评测过程中,我们发现日志的输出格式非常“乱”,那有没有办法可以格式化日志呢?

答案是:有的,默认日志的输出效果如下:

929d96a650ded7b0c5724dbe566a28dc.png

格式化日志可以通过配置 Spring Boot 中的 logging.pattern.console 选项实现的,配置示例如下:

logging.pattern.console=%d | %msg %n

日志的输出结果如下:

dcac940f85536499e443584d31e0b50f.png

可以看出,格式化日志之后,内容简洁多了,但千万不能因为简洁,而遗漏输出关键性的调试信息。

总结

本文我们测试了读者提出质疑的字符串拼接和占位符的性能评测,发现占位符方式性能高的观点依然无从考证,所以我们的基本看法还是,使用占位符的方式更加优雅,可以通过更少的代码实现更多的功能;至于性能方面,只能说还不错,但不能说很优秀。在文章的最后我们讲了 Spring Boot 日志格式化的知识,希望本文可以切实的帮助到你,也欢迎你在评论区留言和我互动。

最后的话

原创不易,都看到这了,点个「」再走呗,这是对我最大的支持与鼓励,谢谢你!

更多相关:

  • Intellij IDEA解析jacoco结果文件的方法JaCoCo经常用来分析代码覆盖率,最方便的当然是在Jenkins中集成,能很定时分析代码覆盖率并查看变化情况。如果需要单独查看某一模块中的覆盖率,则可以借助IntelliJ IDEA。一、设置修改用Idea打开对应的项目代码,右上角编辑设置。将代码覆盖的runner修改为jac...

  • 现在的以太网测试仪多数应用于以太网专线及NGN网络,多业务城域网络等各种测试环境。以太网测试仪表提供了强大的分析、统计和处理功能、支持符合IEE802.3标准的10/100/1000Mbps RJ45电口和波长可选的1000Mbps等等,可以用于线缆测试、以太网误码(一、二、三层 )测试、环回时延测试、RFC2544性能测试,支持以监...

  • 在2008年刚刚进入这个行业时,我对于要学什么东西,是迷茫的。记得我在08年的时候,入职的公司给我们这批新人做了培训。培训中介绍了很多概念,告诉我们质量很重要,软件测试前景很好,还教了我们很多软件测试理论知识,在最后还给我们介绍了所谓高大上的自动化测试和性能测试。然而,在之后的三年里,这些当时培训时给我灌输的错误知识,不断地给我职业生...

  • 说明 quic-go是使用Go来重写chromium中的QUIC协议,将来计划过渡到IETF版本的QUIC协议。 目前该协议还处于不断更新和活跃之中,目前IETF版本的QUIC协议草案版本号已经到draft 9 下面是我对REAME的实操记录,以作备忘。 项目主页 https://github.com/lucas-clement...

  • 英语的重要性,毋庸置疑!尤其对广大职场人士,掌握英语意味着就多了一项竞争的技能。那,对于我们成人来说,时间是最宝贵的。如何短时间内在英语方面有所突破,这是我们最关心的事情。英语学习,到底有没有捷径可以走,是否可以速成?周老师在这里明确告诉大家,英语学习,没有绝对的捷径走,但是可以少走弯路。十多年的教学经验告诉我们,成功的学习方法可以借...

  • 展开全部 其实IDLE提供了一个显32313133353236313431303231363533e78988e69d8331333365663438示所有行和所有字符的功能。 我们打开IDLE shell或者IDLE编辑器,可以看到左下角有个Ln和Col,事实上,Ln是当前光标所在行,Col是当前光标所在列。 我们如果想得到文件代码...

  • 前言[1]从 Main 方法说起[2]走进 Tomcat 内部[3]总结[4]《Java 2019 超神之路》《Dubbo 实现原理与源码解析 —— 精品合集》《Spring 实现原理与源码解析 —— 精品合集》《MyBatis 实现原理与源码解析 —— 精品合集》《Spring MVC 实现原理与源码解析 —— 精品合集》《Spri...

  • 【本文摘要】【注】本文所述内容为学习Yjango《学习观》相关视频之后的总结,观点归Yjango所有,本文仅作为学习之用。阅读本节,会让你对英语这类运动类知识的学习豁然开朗,你会知道英语学习方面,我们的症结所在。学习英语这类运动类知识,需要把握四个原则第一,不要用主动意识。第二,关注于端对端第三,输入输出符合实际情况第四,通过多个例子...

  • 点云PCL免费知识星球,点云论文速读。文章:RGB-D SLAM with Structural Regularities作者:Yanyan Li , Raza Yunus , Nikolas Brasch , Nassir Navab and Federico Tombari编译:点云PCL代码:https://github.co...

  • ELK:(ELK 由 ElasticSearch 、 Logstash 和 Kiabana 三个开源工具组成),Elasticsearch用于存储日志信息,Logstash用于收集日志,Kibana用于图形化展示。上一节我们介绍了如何使用docker-compose搭建ELK日志分析系统。传送门:传说中的ELK日志分析系统,手把手教你...

  • 欢迎关注头条号:老顾聊技术精品原创技术分享,知识的组装工目录前言常用日志组件什么是日志门面和日志实现常见的日志框架日志使用@slf4j注解日志的配置logback-spring配置总结前言日志是我们系统必备的功能之一,可以帮助我们开发人员定位系统的异常、错误以及运行流程的重要的工具。今天老顾就来介绍一下Spring boot的默认的l...

  • glog简介 glog是著名的google开源C++日志库glog(https://github.com/google/glog)的golang版本,glog是一个轻量级的日志库,上手简单不需要配置文件并且稳定高效,可以自定义控制的内容比较少。  glog主要有以下几个特点:  1. glog有四种日志等级INFO < WARING...

  • 应用场景 该配置文件用于ATS用作反向代理模式,访问日志被server域名分隔。比如,我想将sohu和ifeng的域名的日志分别记在两个不同的日志文件中,其它的日志统一记在默认的日志文件中。 配置 为了将不同源站的HTTP transaction记录在不同的日志文件中,你必须在log_hosts.config中列举出每...

  • 缘起 近来因为公司项目需要,阅读了一部分ATS logging system的源码实现,越发觉得logs_xml.config文件的配置非常重要,而我目前只是实践了一点它的皮毛。为此,根据自己的理解,翻译了官网的这篇文档,以作备忘。 logs_xml.config文件定义了自定义日志文件格式,过滤器和处理选项。文件格式采用x...