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 中有很多表示时间的类,我们一起看一下他们的区别和使用场景。
时区相关的类 ZoneId、TimeZone,时刻是唯一的,但是对同一时刻,不同的时区描述是不一样的,比如中国早上 10 点在美国可能是晚上 10 点。
先来看一下 ZoneId 的用法,抽象类 ZoneId 有两个子类,ZoneRegion 和 ZoneOffset
1 | |
有人会问 GMT 和 UTC 还有 UT 应该是有区别的但是为啥说一样呢?
因为这是 java 里是一样的,但是实际追究历史的话,是三种计算时间的方式,他们在现实世界有着细微的区别,但是在 java 中是一样的。
对于一些常见的时区简称和哪些地区使用可以参考 https://www.timeanddate.com/time/zones/
通过地区名获取时区的方法如下。
1 | |
Date
有两个 Date 类,分别是 java.util.Date 和 java.sql.Date。后者是 sql 中的 date,他是只有年月日没有更细粒度的时间信息的。所以一般前者用的较多。Date 类主要包含了年,月,日,时,分,秒信息。可以通过这些信息构造 Date 对象,也可以通过一个 Date 对象获取这些信息。
因为 Date 本来就是表示日期的类,所以也在内部计算出了星期信息。
原则上 Date 是不需要毫秒信息的,但是 Date 中有个 fastTime 字段记录了毫秒信息,其他信息的计算也都基于这个毫秒信息。所以可以通过 getTime 方法拿到毫秒信息。
1 | |
综上,Date 是基于系统的毫秒偏移,来表示时间,并提供了日期相关的内在计算,可以快速的获取年月日星时分秒等信息,计算时候的时区则是使用了系统默认时区 TimeZone.getDefaultRef()。
Date 在设计上存在诸多问题,例如 1900 之前的年份没法表示;0 表示 1 月;日期的含义竟然包括了时分秒等信息;格式化工具线程不安全;隐式的使用了系统默认时区;隐式的使用了默认的日历系统。
Calendar
Calendar 也是老的 jdk 中的时间表示,也位于 java.util 包中,此类没有提供构造方法,可以通过 Calendar.getInstance()
创建当前时间的日历对象,也可以使用 builder 来构建。默认返回的是 GregorianCalendar,这也是世界上绝大多数国家都在使用的日历系统,但是有些国家比如日本、东南亚一些国家像泰国等没有使用,需要指定历法来创建。
Calendar 引入了可选历法,同时也引入了地区和时区的成员变量,弥补了 Date 的这些缺陷。比起 Date,还提供了时间的加法函数 add
1 | |
综上,Calendar 也是基于毫秒数进行时间计算的一个类,他比 Date 强的地方是提供更细的日历值计算,比如位于一年的第几周,还有时间加法运算,但是时间的格式化类只能作用于 date 类。
Instant
Instant 与时区无关的绝对的时间,和 System.currentTimeMillis 类似,但是 Instant 是精确到纳秒。
因为纳秒的精度一个 long 存不过来,所以分为两个字段分别存 seconds 和 nanos。
1 | |
java.time 包下的时间都是基于纳秒精度为计算核心的,所以脱离了 System.currentTimeMillis
LocalDateTime
Instant 没有时区,是绝对的时间值,但是 toString,显示的是 UTC0 的时间,也不能处理日期相关的东西,例如当前是几月几号。
LocalDateTime 就是对 Date 的替代品,后者核心是基于一个毫秒,前者则更精细了到了纳秒。两个重要属性是 LocalDate(年月日),LocalTime(
时分秒纳秒)
1 | |
ZonedDateTime
包含了 LocalDateTime 和 Zone 两部分信息。
1 | |
345 都提供了 isBefore 这样的比较方法,但是不建议在 localDateTime 类使用该方法,因为可能是不同时区。
格式化
old : SimpleDateFormat 线程不安全
1 | |
new : DateTimeFormatter 线程安全 jdk 1.8
1 | |
字符串拼接
字符串拼接【性能对比以及优缺点】
字符串拼接性能调优:最重要的就是内存的申请
- 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 的拼接,但是 ③ 会。
参考资料
启示录
富贵岂由人,时会高志须酬。
能成功于千载者,必以近察远。