这周在浏览Spring的github时,一个问题引起了我的兴趣。这篇文章,我记下了这个问题,从它开始,但不局限于:github.com/spring-proj...这个问题的标题是翻译的,意思是希望@Async注解可以支持占位符或者SpEL表达。而我之所以关注这个问题,完全是因为之前写过一篇@Async相关的文章,看着眼熟,就点进去看了看。在这个问题中,提到了一个编号为27775的问题:github.com/spring-proj...这是什么意思?估计大家看一下我截图标注的地方,就知道他是想把线程池的名字放在配置文件里。而且我认为这个要求并不令人惊讶。基于Spring框架,是一个非常合理的需求。做个demo,我先给大家做个demo,看看它要做什么。首先注入一个名为why的线程池。然后是@Async注解修饰的方法,这个注解指定了一个why的值,表示要使用名为why的线程池:然后我们需要一个Controller,触发它:最后在启动类中添加@EnableAsync注释来启动项目。调用以下链接发起调用:http://127.0.0.1:8085/insertU...输出如下:表示配置生效。然后提issue的哥们想要这样一个功能:就是把@Async注解和配置文件关联起来。现在的Spring版本不支持这个东西。比如我在启动项目的时候,触发了一次:直接抛出NoSuchBeanDefinitionException,说明@Async的值注解不具备解析表达式的功能。支持一波不错,现在需求很明确:暂时不支持,社区有人提出这个需求,希望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方法是这样的:他说的“forexample”就是我陷害的部分。这里有两个关键点:ConfigurableBeanFactoryEmbeddedValueResolver首先,ConfigurableBeanFactory是Spring中一个非常重要的类,但不是本文的重点。一句话概括就是:你可以把它理解为一个庞大的、功能齐全的工厂接口。重点是EmbeddedValueResolver:从注解可以知道这个类是用来解析占位符和表达式的。相当于Spring给你封装的一个工具类。EmbeddedValueResolver:只有一个方法,这个方法调用了一个resolveEmbeddedValue方法:org.springframework.beans.factory.support.AbstractBeanFactory#resolveEmbeddedValue这个方法是Spring解析表达式的核心代码。我来给你展示。首先我们加一点代码:这段代码不用解释,已经很清楚了。我只需要在我们之前分析的代码中打个断点,然后运行程序:是不是很清楚了?入参为${user.age}表达式,出参为配置文件中对应的18。关于如何解析的所有秘密都隐藏在这一行代码中:你以为我会向你解释吗?不可能,只是指路,自己看吧。现在我要开始转弯了,回头看这位老人的回复:现在让我先带你过一遍。首先有个老铁说:你的Spring@Async注解能不能支持表达式,比如@Async("${thread-pool.name}")这种风格,官方出来回复:没问题,我们可以修改find??QualifiedExecutor方法,使用EmbeddedValueResolver工具类支持。比如像下面这个类中的setBeanFactory方法:那我就带大家看看这个方法,然后就知道EmbeddedValueResolver的用法了。好了,那么问题来了:我们如何在findQualifiedExecutor方法中使用呢?兜了一个大圈,回到第一期:这位老哥说他提交的这个修改是基于sbrannen的,这是官方工作人员的小费。你是怎么修改的?看他的Fileschanged:修改了三个文件,其中一个是测试类。剩下两个,一个是@Async注解:这里只修改了Javadoc,说明这个注解支持表达式配置。另一个是AsyncExecutionAspectSupport类:在findQualifiedExecutor方法中添加五行代码来完成这个功能。最后官方在review代码的时候又删掉了一行代码:也就是4行代码,其实应该是2行核心代码,满足了@Async支持表达式的需求。而官方首先告诉你解决方案是什么。只要你稍微跟进一点,启动你的小脑袋想一想,我想你写出这4行代码并不难。这是为Spring贡献源码,是更有价值的贡献。如果你抓住这个机会,你可以在简历上写上一句话:你为Spring贡献了源码,让@Async注解支持表达式的配置。一般来说,对Spring不太了解的朋友看到??这句话只会觉得牛逼,觉得自己应该是??个大佬。但实际上,2行核心代码就搞定了。所以你说给Spring贡献源码很难?机会总是有的,就看你在意与否。什么,你问我有没有给Spring贡献源码?我没有,我只是不在乎,管他呢。这是我写这篇文章想表达的第一点:为开源项目贡献源代码其实并不是一件特别困难的事情,不要总是想着一次提交一个完整的功能。一点点改进是好的。调试技巧上面提到的代码改进,Spring官方没有出包,但是想自己试试,怎么办?当然你也可以拉下Spring的源码,自己编译,最后在本地尝试修改源码。但是这个过程太复杂了,基本上可以说是一个劝退的过程。为了这么小的验证,根本不值得。所以教大家一个我自己研究的“骚”操作。首先我本地的Spring版本是5.3.16,这部分对应的源码如下:我们先修改一下程序:然后运行程序,触发调用,停在断点处:此时我们可以看到限定符还是表达式的形式。然后是野蛮的操作。当你点击这个图标时,对应的快捷键是Alt+F8:这是ide提供的EvaluateExpression功能,你可以在里面写代码。比如:也可以改,我这里是把限定符改成“yyds”字符串:然后跑过断点,从异常信息可以看出,真的是改了:那么,如果我改这4行第一次提交的代码,用EvaluateExpression函数执行,是不是也模拟了对应的修改函数?请问:这个方法是不是“花哨”。接下来,我们将练习。将这些代码行填充到Evaluate中:qualifier=embeddedValueResolver.resolveStringValue(qualifier);}记得复制代码,输入代码这个图标:点击执行后是这样的:再看输出日志,可以看到这一行:意思就是我的“偷梁换柱”的方法已经成功了。这岂不是比自己编译一个Spring源码方便多了?而这种调试方式相当于你在调试的时候可以执行一些额外的代码,所以有时候真的可以起到奇效。这是我写这篇文章的第二个目的,想把这个调试方法分享给大家。细心的读者一定已经注意到了其中的区别。官方代码有点奇怪:首先,instanceof是Java中的保留关键字。它的作用是测试左边的对象是否是右边类的实例,返回boolean数据类型。但是我记得instanceof不是这样用的吗?这是什么节目?别着急,先贴出来,放到ide里看看怎么回事:我们平时的写法都标为①。当我在我的环境中编写标有②的代码时,ide给了我一个提示:Patternsin'instanceof'arenotsupportedatlanguagelevel'8'probablymeansthatthisusageofinstanceofisnotsupportedinJDK8.我看到的那一刻这个提示,突然想起这个写法好像是JDK的高级版本支持的,很久以前在哪里看过。然后用关键字“Patternsinstanceof”查了一下,发现确实是JDK14版本后支持的新特性。www.baeldung.com/java-patter...我就拿文章里的例子来说说吧。我们在使用instanceof的时候,基本上就是一个需要检查对象类型的场景,不同的类型对应不同的逻辑。好吧,我问你,你用了instanceof,类型匹配之后,你下一步怎么办?它是对对象的转换吗?比如上面代码截图中,我们需要在每种情况下通过instanceof判断动物的具体类型,然后将强制类型转换声明为局部变量,然后根据具体类型执行指定的函数。这种写法有很多缺点:这样写很繁琐,需要先检查类型再进行类型转换。每个if都出现了3次类型名称。类型转换和变量声明可读性差重复声明类型名称意味着容易出错,可能导致意外的运行时错误。每增加一个新的动物类型,就必须修改这里的函数。注意我加粗的地方和原文是一样的。这一波重点和细节满满的:为了解决上面提到的一些不足,Java14提供了一种方法,可以将参数类型检查和绑定局部变量类型结合到instanceof操作中。它是这样的:首先在if块中将动物类型与Cat匹配。首先检查animal变量是否是Cat类型的实例,如果是则强制为Cat类型,赋值给cat。需要注意的是,变量名cat并不是一个真正的变量,而是一个模式变量的声明。你可以理解为固定语法。仅当模式匹配表达式的结果为真时,变量cat和dog才会被验证和赋值。所以如果你不小心在其他地方使用了这个变量,它会直接提醒你编译错误。所以如果对比上面两个版本的代码,一定是Java14版本的代码更加简洁易懂。减少了大量的类型转换,大大提高了可读性。回到Spring,你看,我本来是看Spring的,怎么突然写JDK的新特性了?那一定是我的伏笔。给大家看个东西:spring.io/blog/2021/0...在去年的SpringOne大会上官方公布了:Spring6.0和SpringBoot3两大框架的JDK基线版本是17。那也就是说:我们很有可能在JDK8之后,下一个要拥抱的版本就是JDK17。而我,站在一个技术爱好者的角度:这是个好东西,需要支持,大力支持。但是,作为一个写CRUD的Java从业者:想着升级后的各种兼容性问题是很头疼的,所以希望这种拥抱不要出现在我短暂的职业生涯中。让那些刚入行的小伙子们去折腾吧。而当我将自己的视角局限于本文的视角时,我想到了一个“show”的操作,为Spring贡献源码。历史代码中用到instanceof的地方太多了,我只需要在6.0分支中用新特性替换这些地方,这样贡献源码岂不是更简单?不过在提交issue之前,一般流程是查看是否有类似的提交。所以在做这件事之前,我冷静地打听了一下。查了一下,我笑了……我能想到,我相信别人也能想到,而且已经有人先到了。比如这里:github.com/spring-proj...这次提交的对应代码是这样的:然后,官方还在里面吐槽了一句:简单的说:兄弟,这么小的改进还是不要提问题。你要大干一场,不要只改一个班。我想是的,你可以换一个模块。比如这位老哥改了Spring-beans模块下的8个文件:这才是这种改的正确姿势。不管怎样,我在这里指明了方向。有兴趣的可以去看看Spring6.0的代码还有没有未修改的部分,可以尝试提交。这个话题又回到了我一开始表达的第一点:为开源项目贡献源代码其实并不是一件特别困难的事情,不要总是想着一次提交一个完整的功能。一点点改进是好的。提交的东西确实和Spring框架无关,但是至少可以体会一下为开源项目做贡献的过程和感觉,而且项目越大,过程差不多,肯定能学到东西。而你在这个过程中学到的东西绝对比提交一个instanceofimprovement要大得多,所以你还能说这样的提交没有营养吗?比如我在去年写的一篇文章中提到Dubbo在解码响应报文时有一个不必要的重复操作,一行验证相关的代码可以删掉。对应的pr我没提,但是写在文章里了。一位读者看到后,当天中午提交,很快就正式入库了。去年年底,Dubbo社区组织了一次回馈活动,送了他一个咖啡杯:一个惊喜,一行代码,不仅可以学到一些知识,还可以免费得到一个咖啡杯,问问有没有味道好的。升华吧,回顾这篇文章。我从@Async支持表达式开始作为介绍,介绍了instanceof的新特性,然后介绍了Spring6将使用JDK17作为基线版本。其实,在写这篇文章的时候,有一句话一直萦绕在我的脑海中:大风起于清平末。instanceof是清平结束。Gale是以JDK17为基线版本。至于为什么用JDK17作为基线版本,这其实是Java全盛时期的一场劫难。能否渡过劫难,关系到我们每一位修炼者。云远的“喧闹”之下,走在前面的人已经感觉到狂风大作。比如周志明博士在一个名为《云原生时代,Java 的危与机》的活动中是这样说的:icyfenix.cn/tricks/2020……在未来的一段时间内,这将是Java的一个重要的转型窗口。如果下一个LTS版本Java17能够成功结合Amber、Portola、Valhalla、Loom和Panama的新能力和特性,并且如果GraalVM能够给予足够强大的支持,那么Java17LTS将极有可能成为一个里程碑版本,引领整个Java生态从大规模到Server端应用正在向新云原生时代的软件系统转型。它可能是一个里程碑,堪比从用于嵌入式设备和浏览器WebApplets的Java1过渡到Java2,后者确立了现代Java语言(JavaSE/EE/ME和JavaCard)的萌芽方向。但是,如果Java不能加快自己的发展步伐,强大的生态所筑起的护城河终将被耗尽,很大一部分市场将被Golang、Rust等新语言蚕食掉与C、C++、C#和Python等老对手一样。份额,甚至被迫退位“世界第一”编程语言的宝座。Java的未来是继续前行,攀登新的高峰,还是由盛转衰,锋芒缩小,你我拭目以待。而我也只是看到了清平的尽头。
