关于后端:舒服给Spring贡献一波源码

2次阅读

共计 6603 个字符,预计需要花费 17 分钟才能阅读完成。

这周我在 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 的将来是持续向前,再攀顶峰,还是由盛转衰,矛头挫缩,你我刮目相待。
而我,还只是看到了青萍之末。

正文完
 0