4
头图

你好呀,我是歪歪。

这周我在 Spring 的 github 上闲逛的时候,一个 issues 引起了我的兴趣。

这篇文章,是我顺着这个 issues 往下写,始于它,但是不止于它:

https://github.com/spring-pro...

这个 issues 标题翻译过来,就是说希望 @Async 这个注解能够支持占位符或 SpEL 表达式。

而我关注到这个 issues 的原因,完全是因为我之前写过 @Async 相关的文章,看着眼熟,就随手点进来看了一下。

在这个问题里面,提到了一个编号为 27775 的 issues:

https://github.com/spring-pro...

这个说的是个啥事儿呢?

估计你看一眼我截图中标注的地方也就看出来了,他想把线程池的名称放到配置文件里面去。而这个需求我觉得并不奇怪,基于 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 版本之后支持的一个新特性。

https://www.baeldung.com/java...

我就直接把文章中的例子拿出来给你说一下。

我们用 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 的新特性了呢?

那必然是我埋下的伏笔啊。

我给你看一个东西:

https://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 之前,一般流程都是要先去查询一下有没有类似的提交。

所以在干这事之前,我还是先冷静的查询了一下。

一查,我都笑了...

我都能想到,肯定其他人也能想到,果然有人已经捷足先登了。

比如这里:

https://github.com/spring-pro...

这次对应提交的代码是这样的:

然后,官方还在里面小小的吐槽了一波:

简单来说就是:老哥,这样的小改进,就还是不要提 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 的危与机》中说了这样的一段话:

https://icyfenix.cn/tricks/20...

未来一段时间,是 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 的未来是继续向前,再攀高峰,还是由盛转衰,锋芒挫缩,你我拭目以待。

而我,还只是看到了青萍之末。

最后,文章首发于公众号[why技术],欢迎关注,第一时间接收最新文章。


why技术
2.2k 声望6.8k 粉丝