Java奇技淫巧
欢迎你来读这篇博客,这篇博客主要是关于Java·奇技淫巧。
其中包括了关于我的简介和收集的知识分享。
序言
正文
Spring 依赖注入杂谈
先说结论,@Autowired注解是为了扩展,为了灵活设计出来的。会导致循环依赖等报错出现。推荐使用构造器注入方式。Setter 方式注入也不推荐。
至于@Resource注解,这个注解在 jdk11 及之后版本移除了,因为 jdk9 开始进行了模块化,移除了部分和 JavaEE 相关的包,这个注解在javax.annotation里面。javax 这个包,随着 JavaEE 的严谨,后续也不用了,例如 tomcat10 用的 JavaEE 新版本
jakarta。如果选择的技术选型 JDK 高于 11 或者 tomcat 高于 9,需要引入 jakarta 的包,去用这个注解。
在 Spring3.0 时代,官方还是提倡 set
方法注入的。相关文档
从 Spring4.x
开始,官方就不推荐这种注入方式了,转而推荐构造器注入。相关文档
核心:通过构造方法注入的方式,能够保证注入的组件不可变,并且能够确保需要的依赖不为空。此外,构造方法注入的依赖总是能够在返回客户端(组件)代码的时候保证完全初始化的状态。
- 依赖不可变:这个好理解,通过构造方法注入依赖,在对象创建的时候就要注入依赖,一旦对象创建成功,以后就只能使用注入的依赖而无法修改了,这就是依赖不可变(通过
set 方法注入将来还能通过 set 方法修改)。 - 依赖不为空:通过构造方法注入的时候,会自动检查注入的对象是否为空,如果为空,则注入失败;如果不为空,才会注入成功。
- 完全初始化:由于获取到了依赖对象(这个依赖对象是初始化之后的),并且调用了要初始化组件的构造方法,因此最终拿到的就是完全初始化的对象了。
在 Spring3.0 文档中,官方说如果构造方法注入的话,属性太多可能会让代码变得非常臃肿,那么在 4.0
文档中,官方对这个说法也做了一些订正:如果用构造方法注入的时候,参数过多以至于代码过于臃肿,那么此时你需要考虑这个类的设计是否合理,这个类是否参杂了太多的其他无关功能,这个类是否做到了单一职责。
| 注入方式 | 可靠性 | 可维护性 | 可测试性 | 灵活性 | 循环关系的检测 | 性能影响 |
|---|---|---|---|---|---|---|
| Field Injection | 不可靠 | 低 | 差 | 灵活 | 不检测 | 启动快 |
| Constructor Injection | 可靠 | 高 | 好 | 不灵活 | 自动检测 | 启动慢 |
| Setter Injection | 不可靠 | 低 | 好 | 灵活 | 不检测 | 启动快 |
| @Autowired | @Resource | |
|---|---|---|
| 识别方式 | 默认按类型,如果需要按名称,使用@Qualifier 注解配合 | 默认按名称,名称找不到按类型 |
| 适用对象 | 构造器、方法、方法参数、字段 | 方法、字段 |
| 注解来源 | Spring 框架提供的注解 | Java 标准 JSR-250 |
如何更好的注入
目前我个人是通过Lombok的@RequiredArgsConstructor注解和final关键字配合进行注入的。
1 | |
Stream
Java 8 引入了Stream API,提供了一种高效、简洁的方式来处理集合数据,它与 java.io 包里的 InputStream 和 OutputStream
是完全不同的概念。Stream 是一种用于处理数据流的抽象,它允许我们以声明性的方式对数据进行操作,如过滤、排序、转换等。本文将详细介绍 Java
Stream 的基本概念、核心操作、常见用法及其内部工作机制。
Java 8 中的 Stream 是对集合(Collection)对象功能的增强,它专注于对集合对象进行各种非常便利、高效的聚合操作(aggregate
operation),或者大批量数据操作 (bulk data operation)。
Stream API 借助于同样新出现的 Lambda 表达式,极大的提高编程效率和程序可读性。同时它提供串行和并行两种模式进行汇聚操作,并发模式能够充分利用多核处理器的优势,使用
fork/join 并行方式来拆分任务和加速处理过程。
传统的集合操作使用外部迭代(如 for 循环)来遍历集合,而 Stream 使用内部迭代,通过声明性的方法定义需要对数据进行的操作,由 Stream 框架负责具体的迭代过程。这种方式使代码更加简洁和易读。
ParallelStream
parallelStream 默认使用了 fork-join 框架,其默认线程数是 CPU 核心数。
- 设置 parallelStream 默认的公用线程池的全局并发数:
System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "4");
1 | |
- 通过 ForkJoinPool 定义私有线程池
采用自定义的 forkJoinPool 线程池去提交任务,主线程不会参与计算。
forkJoinPool 线程池采用 submit 异步提交任务,通过 get 方法阻塞主线程,直到任务执行完成,再调用 shutdown 方法关闭线程池。
注意,等待提交任务执行完毕不能采用 awaitTermination()方法,该方法是等待指定时间后强制关闭线程池。
效率:针对高密度的 CPU 计算任务,提高线程池的并发数,反而会降低任务的执行效率,因为 CPU 抢占和大量线程频繁切换会增加任务的耗时。
1 | |
操作分类
Stream 的操作可以分为两大类:中间操作、终结操作。
中间操作可分为:
- 无状态(Stateless)操作:指元素的处理不受之前元素的影响
- 有状态(Stateful)操作:指该操作只有拿到所有元素之后才能继续下去
终结操作可分为:
- 短路(Short-circuiting)操作:指遇到某些符合条件的元素就可以得到最终结果
- 非短路(Unshort-circuiting)操作:指必须处理完所有元素才能得到最终结果
中间操作
- filter:过滤元素
- map:映射元素
- flatMap:平铺流
- distinct:去重
- sorted:排序
- peek:查看元素
- limit:截取前 N 个元素
- skip:跳过前 N 个元素
终端操作
- forEach:遍历元素
- collect:收集结果
- reduce:归约操作
- toArray:转换为数组
- min:最小值
- max:最大值
- count:计数
- anyMatch:是否有任意匹配
- allMatch:是否全部匹配
- noneMatch:是否全部不匹配
- findFirst:找到第一个元素
- findAny:找到任意一个元素
这是因为流的生命周期有三个阶段:
- 起始生成阶段。
中间操作:会逐一获取元素并进行处理。可有可无。所有中间操作都是惰性的,因此,流在管道中流动之前,任何操作都不会产生任何影响。Stream 的惰性求值特性使得中间操作不会立即执行,只有在执行终端操作时,整个操作链才会开始计算。这种机制可以有效地减少不必要的计算,提高性能。
- 终端操作。通常分为 最终的消费 (foreach 之类的)和 归纳 (collect)两类。还有重要的一点就是终端操作启动了流在管道中的流动。
Stream API
创建操作
- 通过 java.util.Collection.stream() 方法用集合创建流
1 | |
- 使用 java.util.Arrays.stream(T[] array)方法用数组创建流
1 | |
- Stream 的静态方法:of()、iterate()、generate()
1 | |
- parallelStream
stream 和 parallelStream 的简单区分:stream 是顺序流,由主线程按顺序对流执行操作,而 parallelStream 是并行流,内部以多线程并行执行的方式对流进行操作,需要注意使用并行流的前提是流中的数据处理没有顺序要求(会乱序,即使用了 forEachOrdered)。
1 | |
无状态操作
- filter
1 | |
- map
1 | |
- peek 操作 一般用于不想改变流中元素本身的类型或者只想元素的内部状态时;
- 而 map 则用于改变流中元素本身类型,即从元素中派生出另一种类型的操作。
1 | |
无序化
1 | |
有状态操作
- distinct
1 | |
- sorted
1 | |
- limit
1 | |
- skip
1 | |
短路(Short-circuiting)操作
- anyMatch
1 | |
- allMatch
1 | |
- findFirst
1 | |
- findAny
1 | |
非短路(Unshort-circuiting)操作
- forEach
1 | |
- forEachOrdered
1 | |
- toArray
1 | |
- reduce
1 | |
- collect
1 | |
- max
1 | |
- min
1 | |
- count
1 | |
分片上传
普通上传
调用接口一次性完成一个文件的上传。
- 缺点:文件无法续传、大文件上传太慢。
解决方案:分片上传
分片上传
将源文件切分成很多分片,进行上传,待所有分片上传完毕之后,将所有分片合并,便可得到源文件。这里面的分片可以采用并行的方式上传,提示大文件上传的效率。
分片上传主要的过程(3 步)
- 创建分片上传任务(分片数量、每个分片文件大小、文件 md5 值)
- 上传所有分片
- 待所有分片上传完成后,合并文件,便可得到源文件
实现分片上传需要两张表
分片上传任务表(t_shard_upload):每个分片任务会在此表创建一条记录
1 | |
分片文件表(t_shard_upload_part):这个表和上面的表是一对多的关系,用于记录每个分片的信息,比如一个文件被切分成十个分片,那么此表会产生十条记录。
1 | |
服务端需要提供四个接口
- 创建分片上传任务(/shardUpload/init) 返回分片任务 id,后续三个接口都要用
- 上传分片文件(/shardUpload/uploadPart)
- 合并分片、完成上传(shardUpload/complete)
- 获取分片任务详细信息(/shardUpload/detail) 可以得到分片任务的状态信息,如分片任务是否上传完毕,哪些分片已上传,网络出现故障可以借助此接口恢复上传
多线程任务批处理通用工具类
todo
Java 日期
Java 中有很多表示时间的类,比较容易混在一起。其实只要先分清楚三个问题,就不难理解:
- 时刻:某个真实发生的瞬间,比如时间戳、
Instant。 - 本地日期时间:不带时区的年月日时分秒,比如
LocalDateTime。 - 时区规则:同一个时刻在不同时区下会显示成不同的本地时间,比如
ZoneId、TimeZone。
一句话总结:
| 类 | 主要含义 | 是否带时区 | 推荐程度 |
|---|---|---|---|
Date |
一个毫秒级时刻 | 不直接带,但很多方法隐式用系统默认时区 | 老代码兼容 |
Calendar |
带日历系统、时区、地区的可变时间对象 | 带 | 老代码兼容 |
Instant |
UTC 时间线上的一个绝对时刻 | 不带时区 | 推荐 |
LocalDate |
本地日期,只有年月日 | 不带 | 推荐 |
LocalTime |
本地时间,只有时分秒纳秒 | 不带 | 推荐 |
LocalDateTime |
本地日期时间 | 不带 | 推荐,但别直接表示绝对时刻 |
ZonedDateTime |
日期时间 + 时区规则 | 带 | 推荐 |
OffsetDateTime |
日期时间 + 固定偏移量 | 带偏移,不带完整地区规则 | 推荐 |
DateTimeFormatter |
新时间 API 的格式化器 | 可绑定时区 | 推荐 |
时区
时区相关的类主要有 ZoneId、ZoneOffset、TimeZone、Clock。
先明确一个概念:时刻是唯一的,但是同一个时刻在不同时区下的描述不一样。
比如同一个 Instant,在中国可能是上午 10 点,在美国可能是前一天晚上。
ZoneId 表示时区 ID,比如:
1 | |
ZoneId 大体可以分成两类:
- 地区型时区:例如
Asia/Shanghai、Europe/London,背后有完整的时区规则。 - 固定偏移量时区:例如
+08:00、Z,只表示固定偏移量。
1 | |
注意下面这几个写法很容易误会:
1 | |
它们在 Java 中的规则都等价于 UTC 0 偏移量,但是 ID 本身仍然不同:
1 | |
也就是说,在 Java 计算规则上它们等价于 0 偏移量,但是字符串 ID 不一定相同。
再看下面几个:
1 | |
它们不是简单地等同于 ZoneOffset,而是带有 GMT、UTC、UT 前缀的 offset-style ZoneId。如果你想拿到纯粹的 ZoneOffset,可以这样写:
1 | |
也可以调用 normalized() 尝试规范化:
1 | |
所以不要通过实现类去判断业务含义,应该通过 ZoneId / ZoneOffset 的语义来理解。
通过地区名获取时区:
1 | |
通过简称获取时区:
1 | |
但是不推荐大量使用简称。像 CST 这种简称本身就有歧义,可能代表 China Standard Time,也可能代表 Central Standard Time。业务系统中最好使用 Asia/Shanghai 这种完整的 IANA 时区 ID。
TimeZone 是老 API 中的时区类,和 ZoneId 可以互转:
1 | |
这里有个坑:
1 | |
TimeZone.getTimeZone(String) 如果传入非法 ID,不会抛异常,而是返回 GMT。这就很坑,像一个“静默兜底”,bug 也静悄悄地进村了。
如果希望非法时区直接报错,更推荐使用:
1 | |
非法时区会抛异常:
1 | |
Clock 也是一个和时区相关的类,它主要有两个作用:
- 获取当前时刻。
- 让时间来源可替换,方便测试。
1 | |
注意:Clock 里面的时区不是说 Instant 本身有时区,而是说当需要把 Instant 解释成日期时间时,使用哪个时区。
1 | |
这两个拿到的都是当前时刻,理论上没有“8 小时时差”。因为 Instant 表示的是绝对时刻,和时区无关。
下面两个也是同理:
1 | |
它们拿到的都是当前时刻的 epoch millis,不会因为时区不同就差 8 小时。
Clock 在测试中特别好用,比如固定一个时间:
1 | |
或者构造一个偏移后的时间:
1 | |
Date
Java 中常见的 Date 有两个:
1 | |
java.util.Date 是老 API 中的日期时间类,但它的本质不是“年月日时分秒”,而是:
从 1970-01-01 00:00:00 UTC 到当前时刻的毫秒数。
也就是说,Date 本质上表示的是一个毫秒级的绝对时刻。
1 | |
new Date() 内部大体就是基于当前系统时间构造的:
1 | |
Date 有一堆老的构造方法和 getter,但是都已经不推荐使用:
1 | |
这里有两个经典坑:
- 年份是从 1900 开始偏移的。
- 月份从 0 开始,0 表示 1 月,1 表示 2 月。
所以这些 API 非常反直觉,实际开发中不要再写新代码使用它们。
Date 和 Instant 可以互转:
1 | |
java.sql.Date 则是 JDBC 里面对应 SQL DATE 类型的类,语义上只表示年月日,不表示时分秒。现在如果使用 JDBC 4.2 之后的能力,更推荐直接和 LocalDate、LocalDateTime、Instant 这些新 API 转换。
1 | |
综上,Date 的核心问题是:
- 类名叫 Date,但实际上表示的是一个毫秒级时刻。
- 老构造方法和 getter 设计反直觉。
- 很多方法已经废弃。
- 和默认时区、默认日历系统耦合较深。
- 可读性差,不适合新代码继续扩展。
现在新代码中,通常用:
1 | |
Calendar
Calendar 也是老的 JDK 时间 API,位于 java.util 包中。
它是一个抽象类,平时一般通过下面方式创建:
1 | |
默认情况下,通常返回的是 GregorianCalendar。
Calendar 相比 Date 增加了几个能力:
- 支持时区。
- 支持地区。
- 支持日历字段计算。
- 支持时间加减。
- 可以获取一年中的第几周、一月中的第几天等字段。
例如:
1 | |
Calendar 的月份同样是从 0 开始:
1 | |
这个设计依然很坑。
用 Builder 创建:
1 | |
用毫秒数构造:
1 | |
获取毫秒数:
1 | |
刚创建出来时,它大体等价于当前时间的毫秒数:
1 | |
设置时区:
1 | |
setTimeZone 不会改变这个对象表示的本质时刻,只是改变后续计算年月日时分秒时使用的时区。
比如同一个毫秒值:
1 | |
Calendar 也支持时间加减:
1 | |
注意 add 和 roll 不一样:
1 | |
综上,Calendar 比 Date 强一些,它引入了时区、地区、日历字段和时间加减能力。但是它仍然是老 API,有几个明显问题:
- 可变对象,容易被意外修改。
- 月份从 0 开始。
- API 繁琐。
- 默认宽松解析,容易出现奇怪结果。
- 不如
java.time包清晰。
所以新代码优先使用 java.time,老代码交互时再使用 Date / Calendar。
Instant
Instant 表示 UTC 时间线上的一个绝对时刻。
它和 System.currentTimeMillis() 类似,都是表示 epoch 之后的时间。但是 Instant 的表达能力更强,内部由两部分组成:
seconds:从 epoch 开始的秒数。nanos:当前秒内的纳秒偏移。
所以它可以表达纳秒级精度:
1 | |
常见用法:
1 | |
Instant 没有时区。它的 toString() 默认按照 ISO-8601 格式输出,并使用 Z 表示 UTC:
1 | |
注意:Z 不是说 Instant 存了一个 UTC 时区,而是说输出时用 UTC 方式展示这个绝对时刻。
时间截断:
1 | |
这个表示截断到小时,比如:
1 | |
截断后变成:
1 | |
Instant 转换成带时区的时间:
1 | |
Instant 和 Date 互转:
1 | |
Instant 适合表示:
- 创建时间。
- 更新时间。
- 事件发生时间。
- 日志时间。
- 数据库时间戳。
- 跨系统传输的绝对时间。
比如数据库里存“订单创建时间”,推荐使用 Instant 或者明确使用 UTC 的时间戳,而不是裸 LocalDateTime。
LocalDateTime
LocalDateTime 表示本地日期时间,它由两部分组成:
1 | |
例如:
1 | |
LocalDateTime 的月份从 1 开始,这一点比 Date 和 Calendar 正常很多:
1 | |
获取当前本地日期时间:
1 | |
指定时区获取当前本地日期时间:
1 | |
这两个结果会不同,因为它们是在不同的时区下观察同一个“当前时刻”。
1 | |
通常上海时间会比 UTC 时间大约多 8 小时。
但是下面这两个写法含义完全不同:
1 | |
第一个含义是:
当前这个时刻,在 UTC 时区下对应的本地日期时间。
第二个含义是:
先拿系统默认时区下的本地日期时间,然后强行给它贴上 UTC 时区。
假设系统默认时区是 Asia/Shanghai,当前时间是:
1 | |
那么:
1 | |
可能得到:
1 | |
而:
1 | |
可能得到:
1 | |
这两个表示的绝对时刻不是一个东西。
所以要记住:
LocalDateTime本身不表示一个全球唯一的时刻,它只表示一个“墙上挂钟看到的时间”。
它适合表示:
- 生日。
- 会议时间。
- 表单输入的日期时间。
- 不关心时区的业务时间。
- 本地展示时间。
它不适合直接表示:
- 日志发生时间。
- 订单创建时间。
- 支付成功时间。
- 跨服务传输时间。
- 跨时区比较时间。
如果要表示绝对时刻,用 Instant。如果要表示带时区的日期时间,用 ZonedDateTime。
LocalDateTime 转 Instant 必须补充时区:
1 | |
Instant 转 LocalDateTime 也必须指定时区:
1 | |
ZonedDateTime
ZonedDateTime 表示:
1 | |
也就是:
- 本地日期时间。
- 时区 ID。
- 根据时区规则解析出来的实际偏移量。
创建当前系统时区时间:
1 | |
创建 UTC 时间:
1 | |
创建上海时间:
1 | |
ZonedDateTime 转 LocalDateTime:
1 | |
这个操作会直接拿出本地日期时间部分,时区信息会丢失。
ZonedDateTime 转 Instant:
1 | |
这个操作会把带时区的时间转换成绝对时刻。
跨时区转换时,推荐使用:
1 | |
withZoneSameInstant 的意思是:
保持同一个绝对时刻不变,只是换一个时区展示。
比如:
1 | |
转换成 UTC 后是:
1 | |
这两个表示的是同一个瞬间。
还有一个容易混淆的方法:
1 | |
它的意思是:
保持本地年月日时分秒不变,只是换一个时区。
这个会改变实际表示的绝对时刻,业务上要慎用。
比较时间时,Instant、LocalDateTime、ZonedDateTime 都有类似方法:
1 | |
但是要注意:
1 | |
不带时区,所以它比较的是本地日期时间,不一定能代表真实时刻的先后。
比如:
1 | |
单看 LocalDateTime,10 点当然比 9 点晚。
但是如果 10 点是上海时间,9 点是纽约时间,它们对应的真实时刻就完全不一样。
所以跨时区、跨系统比较时,推荐先转成:
1 | |
再比较。
格式化
老 API 使用 SimpleDateFormat:
1 | |
但是 SimpleDateFormat 是可变对象,多线程环境下不安全。
以前常见的解决方案有几种:
1 | |
Java 8 之后推荐使用 DateTimeFormatter。
1 | |
DateTimeFormatter 是不可变、线程安全的,可以定义成常量复用:
1 | |
格式化:
1 | |
解析:
1 | |
如果格式里有时区,可以使用 ZonedDateTime:
1 | |
常见格式符号:
| 符号 | 含义 | 示例 |
|---|---|---|
yyyy |
年 | 2022 |
MM |
月 | 01 |
dd |
日 | 01 |
HH |
24 小时制小时 | 23 |
mm |
分钟 | 59 |
ss |
秒 | 59 |
SSS |
毫秒/秒内小数部分 | 123 |
VV |
时区 ID | Asia/Shanghai |
XXX |
偏移量 | +08:00 |
注意:
1 | |
和:
1 | |
不是一回事。
yyyy 是普通年份,YYYY 是 week-based-year,也就是基于周的年份。跨年那几天很容易出问题。
比如一些年份的 12 月 31 日,按照周历可能已经属于下一年的第一周,这时候用 YYYY 就会出现看起来“年份提前”的情况。
所以普通日期格式化,优先使用:
1 | |
不要写成:
1 | |
还有一个 JDK 8 的老坑:
1 | |
在部分 JDK 8 版本中,解析这种没有分隔符、直接把毫秒拼在秒后面的格式时,可能会失败。这是 JDK 8 DateTimeFormatter 的一个历史解析问题。
可以改成带分隔符:
1 | |
也可以使用 DateTimeFormatterBuilder 明确指定毫秒字段:
1 | |
但是这个问题不要理解成 DateTimeFormatter 线程不安全。DateTimeFormatter 本身是线程安全的,这里是 JDK 8 特定格式解析的问题。
还有一个实际开发中更常见的坑:用时间格式生成文件名。
1 | |
这个格式只有秒级精度。如果一秒内生成多个文件名,就一定可能重复。
比如:
1 | |
这不是线程安全问题,而是精度不够。
可以加毫秒:
1 | |
但是即使加到毫秒,也不保证高并发下绝对唯一。因为同一毫秒内仍然可能生成多个文件名。
更稳的做法是拼接 UUID、雪花 ID、AtomicLong 或者直接使用临时文件 API。
1 | |
或者:
1 | |
综上,格式化部分可以记住几条规则:
- 新代码优先使用
DateTimeFormatter。 DateTimeFormatter可以定义成static final常量。- 普通年份用
yyyy,不要误用YYYY。 - 跨时区格式化时,明确使用
ZonedDateTime或者给 formatter 绑定 zone。 - 时间字符串生成文件名时,不要只靠秒级时间保证唯一性。
- JDK 8 下解析
yyyyMMddHHmmssSSS这种紧凑格式要注意历史 bug。 - 绝对时间比较、存储、传输优先使用
Instant。
字符串拼接
字符串拼接【性能对比以及优缺点】
字符串拼接性能调优:最重要的就是内存的申请
- StringBuilder 不知道长度最快 不安全,指针不在乎安全
- += 耗内存
- $
- String.Formt
- String.Concat 知道长度时做快
- 速度第一:String.Concat
- 速度第二:StringBuilder
- 速度第三:$ String.Formt 差不多并列
- 速度第四:+= 最耗内存 不建议使用
1 | |
利用 String format 方法及占位符优雅拼接字符串
1 | |
其实正如 format 这个方法名所暗示的, 它主要是关于格式化字符串的, 但它正好也给出了一种拼接字符串的较为优雅的方式, 所以,
即便你不关心字符串的格式化, 依然可以利用它的这个特性去做字符串常量与变量的拼接, 从而提高代码的可读性.
日志中优雅使用
1 | |
这种比前一种方式要优雅一点, 不过呢, 却还是要用到 String.format 方法, 并且需要注意变量的类型, 而现在基本上很多的日志框架都直接支持一种占位符的写法,
与 String.format 中的不同, 可能相对来说还更简单一点, 只是它必须依赖于日志框架相关代码, 且只能用于日志输出中,
这样的方式就是利用大括号 {} 这种占位符(也称为 格式化锚点(formatting anchor))的写法:
1 | |
这种写法用户也不需要关注变量的类型, 如果有三个或更多的变量值要输出, 就相应多写几个 {}, 以及多传几个对应变量即可:
注: 方法的第二个参数是可变长参数, 因此可以传多个.
有了这种写法的支持, 就可以一口气写完要输出的日志信息, 而要拼接的变量就用 {} 暂时代替, 避免了用 + 号拼接的繁琐.
这样的使用占位符的写法还有一个潜在的好处, 就是性能的提升. 如果配置的日志级别是不输出 info 级别的日志,
那么相应的字符串的拼接与替换就不会发生, 可变长参数数组也不会生成.
而如果使用 + 号式之类的写法, 就不能得到这个好处了, 字符串传入之前就已经被拼接了, 最后因为日志级别的原因又没有输出的话,
无疑是一种浪费.
最后, 如果是 error 日志, 还支持一种特别的写法, 可以把异常传给最后一个参数, 除了替换占位符输出你的异常信息外,
它还会把异常栈整个打印出来, 避免了你自己去调用 e.printStackTrace:
1 | |
注意以上, 错误信息只有一个占位符, 但后面跟了两个变量, 其中最后的变量是 Throwable 类型的, 只有第一个变量值才参与字符串的拼接.
当最后的变量是 Throwable 类型时, log 框架会将其当作异常栈输出, 类似于这个调用 public void error(String msg, Throwable
t);, 异常变量需置于最后.
另注: 此用法需要相应的日志版本的支持, 参见这里: http://www.slf4j.org/faq.html#paramException, 适用 1.6.0 版本及以上,
相应的日志实现也需支持.
以上例子说明了在 log 异常时, 同样可以使用占位符的写法, 同时兼容原有的打印异常栈的写法, 这些都算是方便我们输出异常日志的一些技巧,
关于使用 {} 占位符输出日志的介绍就到这里.
为什么说 log 用占位符比用字符串连接比较好
我看的是 logback 的源码
如果在日志等级符合输出条件的情况下,两个是没有什么大区别的。
但如果是在日志等级不符合输出条件的情况下:
由于字符串拼接是作为一个方法参数的,意味着它进入 logback 的内部判断的时候,就已经是拼接成功了。而在这一步的拼接成功,涉及到 String 是一个 final 变量的问题,这个拼接是耗时了,创建了 String,但是进入判断之后又完全没什么用。
这两种是有区别的:
1 | |
而如果是占位符的话,它直接在 logback 的内部判断了日志等级是否足以输出,不行就直接 return 了。
再有就是isDebugEnabled()
这种方法如果要封装,但是参数是需要逻辑处理的话,是没有什么用处的,需要显示声明才行。
1 | |
① 和 ②,是没有什么区别的,但是 ③ 和 ④ 就用区别了。
④ 不会进行 StringBuilder 的拼接,但是 ③ 会。
参考资料
- java 日期
启示录
富贵岂由人,时会高志须酬。
能成功于千载者,必以近察远。