这周我在 Spring 的 github 上闲逛的时候,一个 issues 引起了我的兴趣。
这篇文章,是我顺着这个 issues 往下写,始于它,但是不止于它:
github.com/spring-proj…
这个 issues 标题翻译过来,就是说希望 @Async 这个注解能够支持占位符或 SpEL 表达式。
而我关注到这个 issues 的原因,完全是因为我之前写过 @Async 相关的文章,看着眼熟,就随手点进来看了一下。
在这个问题里面,提到了一个编号为 27775 的 issues:
github.com/spring-proj…
这个说的是个啥事儿呢?
估计你看一眼我截图中标注的地方也就看出来了,他想把线程池的名称放到配置文件里面去。而这个需求我觉得并不奇怪,基于 Spring 框架来说,是一个很合理的需求。
搞个 Demo
我还是先给你搞个 Demo,验收一下它想要干啥。
首先注入了一个名称为 why 的线程池。
然后有一个被 @Async 注解修饰的方法,而这个注解指定了一个值为 why 的 value,表明要使用名称为 why 的这个线程池:
接着我们还需要一个 Controller,触发一下:
最后在启动类上加上 @EnableAsync 注解,把项目启动起来。
调用下面的链接,发起调用:
http://127.0.0.1:8085/insertU...
输出结果如下:
说明配置生效了。
然后,提出 issues 的这个哥们,他想要这么一个功能:
也就是让 @Async 注解和配置文件进行联动。
目前 Spring 的版本是不支持这个东西的,比如我把项目启动起来之触发一次:
直接抛出了 NoSuchBeanDefinitionException,说明 @Async 的 value 注解并没有解析表达式的功能。
支持一波
好的,现在需求就很明确了:目前不支持,有人在社区提出该需求,想要 Spring 支持该功能。
然后这个叫 sbrannen 的哥们出来了:
他说了两句话:
1.如果提供的 BeanFactory 是 ConfigurableBeanFactory,我们似乎可以通过修改 org.springframework.aop.interceptor.AsyncExecutionAspectSupport.findQualifiedExecutor(BeanFactory,String) 的代码,使用 EmbeddedValueResolver 来支持。
可以看一下 org.springframework.context.annotation.CommonAnnotationBeanPostProcessor.setBeanFactory(BeanFactory),这是一个对应的例子。
第一句话中,他提到的 findQualifiedExecutor 方法,也就是需要修改的地方的代码,在我的 5.3.16 版本中是这样的:
你先记住入参中有一个 beanFactory 就行了。
而第二句话中提到的 setBeanFactory 方法,是这样的:
他说的 “for an example” 就是我框起来的部分。
这里面关键的地方有两个:
ConfigurableBeanFactory
EmbeddedValueResolver
首先 ConfigurableBeanFactory ,在 Spring 里面是一个非常重要的类,但是不是本文重点,一句话带过:你可以把它理解为是一个巨大的、功能齐全的工厂接口。
重点是 EmbeddedValueResolver 这个东西:
从注解上可以知道这个类是用来解析占位符和表达式。相当于是 Spring 给你封装好的一个工具类吧。
EmbeddedValueResolver 里面就这一个方法:
而这个方法里面调用了一个 resolveEmbeddedValue 方法:
org.springframework.beans.factory.support.AbstractBeanFactory#resolveEmbeddedValue
这个方法就是 Spring 里面解析表达式的核心代码。
我给你演示一下。
首先我们加一点代码:
这个代码不需要解释吧,已经很清晰了。
我只需要在我们前面分析的代码这里打上断点,然后把程序跑起来:
是不是很清晰了。
入參是 ${user.age} 表达式,出参是配置文件中对应的 18。
关于如何解析的所有秘密都藏在这一行代码里面:
你以为我要给你详细讲解吗?
不可能的,指个路而已,自己看去吧。
现在我要开始拐弯了,拐回到这个老哥的回复上:
现在我先带你捋一捋啊。
首先,有个老铁说:你这个 Spring 的 @Async 注解能不能支持表达式呀,比如这样式儿的 @Async("${thread-pool.name}")
然后官方出来回复说:没问题啊,我们可以修改 findQualifiedExecutor 方法,在里面使用 EmbeddedValueResolver 这个工具类来支持。比如就像是下面这个类中的 setBeanFactory 方法一样:
接着我带你去看了一下这个方法,然后知道了 EmbeddedValueResolver 的用法。
好的,那么现在问题来了:在 findQualifiedExecutor 方法中,我们怎么使用呢?
兜兜转转一大圈,现在就回到最开始的那个 issues 里面:
这个老哥说他基于 sbrannen,也就是官方人员的提示.提交了这次修改。
怎么修改的呢?
看他的 Files changed:
修改了三个文件,其中一个测试类。
剩下两个,一个是 @Async 注解:
这里面只是修改了 Javadoc,表示这个注解支持表达式的方式进行配置。
另外一个是 AsyncExecutionAspectSupport 这个类:
在 findQualifiedExecutor 方法里面加了五行代码,就完成了这个功能。
最后,官方在 review 代码的时候,又删除一行代码:
也就是 4 行代码,其实应该是 2 行核心代码,就完成了让 @Async 支持表达式的这个需求。
而且官方是先给你说了解决方案是什么,只要你稍微你跟进一下,发动你的小脑壳思考一下,我想你写出这 4 行代码也不是什么困难的事情。
这就是给 Spring 贡献源码了,而且是一个比较有价值的贡献。如果是你抓住了这个机会,你完全可以在简历上写一句:给 Spring 贡献过源码,让 @Async 注解支持表达式的配置方式。
一般来说对 Spring 了解不深入的朋友,看到这句话的时候,只会觉得很牛逼,想着应该是个大佬。
但是实际上,2 行核心代码就搞定了。
所以你说给 Spring 贡献源码这个事儿难吗?
机会总是有的,就看你有没有上心了。
什么,你问我有没有给 Spring 贡献过源码?
我没有,我就是不上心,咋的了。
这是我写这个文章想要表达的第个观点:
给开源项目贡献源码其实不是一件特别困难的事情,不要老想着一次就提交一整个功能上去。一点点改进,都是好的。
调试技巧
前面提到的代码改进, Spring 还没有发布官方的包,但是我想要自己试验一下,怎么办呢?
你当然可以把 Spring 的源码拉下来,然后自己编译一波,最后本地改改源码试一试。
但是这个过程太过复杂了,基本上可以说是一个劝退的流程。
为了这么一个小验证,完全不值当。
所以我教你一个我自己研究出来的“骚”操作。
首先,我本地的 Spring 版本是 5.3.16,对应这部分的源码是这样的:
还是先改造一下程序:
然后把程序跑起来,触发一次调用,就会停在断点的地方:
这个时候我们可以看到 qualifier 还是一个表达式的形式。
接着骚操作就来了。
你点击这个图标,对应的快捷键是 Alt+F8:
这是 ide 提供的 Evaluate Expression 功能,在这个里面是可以写代码的。
比如这样:
它还可以偷梁换柱,我在这里把 qualifier 修改为 “yyds” 字符串:
然后跑过断点,你可以从异常信息中看到,它是真的被修改了:
那么,如果我把这次提交的这 4 行代码,利用 Evaluate Expression 功能执行一下,是不是就算是模拟了对应的修改后的功能了?
我就问你:这个方法“骚”不“骚”。
接下来,我们就实操起来。
把这几行代码,填入到 Evaluate 里面:
if (beanFactory instanceof ConfigurableBeanFactory) {
EmbeddedValueResolver embeddedValueResolver = new EmbeddedValueResolver((ConfigurableBeanFactory)beanFactory);
qualifier = embeddedValueResolver.resolveStringValue(qualifier);
}
复制代码
输入代码片段,记得点击一下这个图标:
点击执行之后是这样的:
然后看输出日志,你可以看到这样一行:
说明我的“偷梁换柱”大法成功了。
这不比你去编译一份 Spring 源代码来的方便的多?
而且这个调试的方法,相当于是你在 debug 的时候还能再额外执行一些代码,所以有的时候真的有时候能起到奇效。
这是我写这篇文章的第二个目的,想要分享给你这个调试方法。
不同之处
细心的读者肯定发现了,官方的代码有点奇怪啊:
首先 instanceof 是 Java 的保留关键字,它的作用是测试它左边的对象是否是它右边的类的实例,返回 boolean 的数据类型。
但是我记得 instanceof 不是这样用的呀?这是个什么骚操作啊?
不慌,先粘出来,放到 ide 里面看看啥情况:
我们常用的写法都是标号为 ① 那样的,当我在我的环境里面写出标号为 ② 的代码的时候,ide 给我了一个提示:
Patterns in 'instanceof' are not supported at language level '8'
大概意思是说 instanceof 的这个用法在 JDK 8 里面是不支持的。
看到这个提示的一瞬间,我突然想起了,这个写法好像是 JDK 某个高级版本之后支持的,很久之前在某个地方瞟到过一眼。
然后我用 “Patterns instanceof” 关键词查了一下,发现果然是 JDK 14 版本之后支持的一个新特性。
www.baeldung.com/java-patter…
我就直接把文章中的例子拿出来给你说一下。
我们用 instanceof 的时候,基本上都是需要检查对象的类型的场景,不同的类型对应不同的逻辑。
好,我问你,你使用 instanceof,在类型匹配上了之后,你的下一步操作是什么?
是不是对对象进行强制类型转换?
比如这样的:
在上述代码截图中,我们每种情况要通过 instanceof 判断 animal 的具体类型,然后强制类型转换声明为局部变量,接着根据具体的类型执行指定的函数。
这有的写法有很多缺点:
这么写非常单调乏味,需要检测类型然后强制类型转换。
每个 if 都要出现三次类型名。
类型转换和变量声明可读性很差
重复声明类型名意味着很容易出错,可能导致未预料到的运行时错误。
每新增一个animal 类型就要修改这里的函数。
注意我加粗的地方,和原文是一样的,这波强调和细节是拉满了的:
为了解决上面提到的部分缺点,Java 14 提供了可以将参数类型检查和绑定局部变量类型合并到一起的 instanceof 操作。
就像这样式儿的:
首先在 if 代码块对 animal 的类型和 Cat 进行匹配。先看 animal 变量是否为 Cat 类型的实例,如果是,强转为 Cat 类型,并赋值给 cat。
需要注意的是变量名 cat 并不是一个真正存在的变量,只是模式变量的一个声明而已。你可以理解为固定语法。
变量 cat 和 dog 只有当模式匹配表达式的结果为 true 时才生效和赋值。所以如果你一不小心把变量用在别的地方,直接会提醒你编译错误。
所以你对比一下上面两个版本的代码,肯定是 Java 14 版本的代码更简洁,也更易懂。减少了大量的类型转换,而且可读性大大提高。
回到 Spring
你看,本来是看 Spring 的,怎么突然写到了 JDK 的新特性了呢?
那必然是我埋下的伏笔啊。
我给你看一个东西:
spring.io/blog/2021/0…
官方在去年的 SpringOne 大会上就宣布了:Spring 6.0 和 Spring Boot 3 这两大框架的 JDK 基线版本是 17。
也就是说:我们很有可能在 JDK 8 之后,下一个要拥抱的版本是 JDK 17。
而我,作为一个技术爱好者的角度来说:这是好事,得支持,大力支持。
但是,作为一个写着 CRUD 的 Java 从业者来说:想想升级之后各种兼容性问题就头疼,所以希望这个拥抱不要发生在我短暂的职业生涯中。去让那帮年轻力壮,刚刚入行的小伙子们去折腾吧。
而当我把视角局限在这篇文章的角度,电光火石之间,我又想到了一个给 Spring 贡献源码的“骚”操作。
历史代码中这么多用 instanceof 的地方,我只要在 6.0 分支里面,把这些地方都换成新特性的写法,那岂不是一个更简单的贡献源码的方式?
但是,在提交 issues 之前,一般流程都是要先去查询一下有没有类似的提交。
所以在干这事之前,我还是先冷静的查询了一下。
一查,我都笑了...
我都能想到,肯定其他人也能想到,果然有人已经捷足先登了。
比如这里:
github.com/spring-proj…
这次对应提交的代码是这样的:
然后,官方还在里面小小的吐槽了一波:
简单来说就是:老哥,这样的小改进,就还是不要提 issue 了吧。你得整个大的啊,别只改一个类啊。
我觉得也是,你改你改一个模块也行呀,比如这位老哥,改了 Spring-beans 模块下的 8 个文件:
这样才是针对这类改动的正确姿势。
反正我把路指在这里了,你要是有兴趣,可以去看看 Spring 6.0 的代码是不是还有一些没有改的地方,你去试着提交一把。
这个话题又回到我最开始表达的第一个观点了:
给开源项目贡献源码其实不是一件特别困难的事情,不要老想着一次就提交一整个功能上去。一点点改进,都是好的。
提交的东西确实是和 Spring 框架关系不大,但是你至少能体验一下给开源项目做贡献的流程和感觉吧,而且越大的项目,流程约精细,肯定是能学到东西。
而这个过程中学到的东西,绝对比你提交一个 instanceof 改进大的多,所以你还能说这样的提交是没有什么营养的嘛?
比如我去年的一篇文章中,就提到了 Dubbo 在对响应报文进行解码的时候有一个没必要的重复操作,可以删除一行校验相关的代码。
我没有去提对应的 pr,但是我写在了文章中。
有个读者看到后,当天中午就去提交了,官方也很快入库了。
去年年底的时候 Dubbo 社区搞了一个回馈活动,就给他送了一个咖啡杯:
意外惊喜,一行代码,不仅可以学点知识,还可以免费得个咖啡杯,就问香不香。
升华一下
好了,回顾一下这篇文章。
我从 @Async 支持表达式作为引子,引到了 instanceof 的新特性,接着又引到了 Spring 6 会以 JDK 17 作为基线版本。
其实我写这篇文章的时候,脑海中一直在萦绕着一句话:大风起于青萍之末。
instanceof,是青萍之末。
大风就是 JDK 17 作为基线版本。
关于为什么要用 JDK 17 作为基线版本,其实这是风华正茂的 Java 的一次渡劫。渡劫是否成功,关系着我们每一个从业者。
在云原生的“喧哗”之下,走在前面的人已经感受到:大风已经吹起来了。
比如周志明博士在一次名为《云原生时代,Java 的危与机》中说了这样的一段话:
icyfenix.cn/tricks/2020…
未来一段时间,是 Java 重要的转型窗口期,如果作为下一个 LTS 版的 Java 17,能够成功集 Amber、Portola、Valhalla、Loom 和 Panama 的新能力、新特性于一身,GraalVM 也能给予足够强力支持的话,那 Java 17 LTS 大概率会是一个里程碑式的版本,带领着整个 Java 生态从大规模服务端应用,向新的云原生时代软件系统转型。
可能成为比肩当年从面向嵌入式设备与浏览器 Web Applets 的 Java 1,到确立现代 Java 语言方向(Java SE/EE/ME 和 JavaCard)雏形的 Java 2 转型那样的里程碑。
但是,如果 Java 不能加速自己的发展步伐,那由强大生态所构建的护城河终究会消耗殆尽,被 Golang、Rust 这样的新生语言,以及 C、C++、C#、Python 等老对手蚕食掉很大一部分市场份额,以至被迫从“天下第一”编程语言的宝座中退位。
Java 的未来是继续向前,再攀高峰,还是由盛转衰,锋芒挫缩,你我拭目以待。
而我,还只是看到了青萍之末。