• 舒服,给Spring贡献一波源码。


    这周我在 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/insertUser?age=18

    输出结果如下:

    说明配置生效了。

    然后,提出 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 里面:

    1. if (beanFactory instanceof ConfigurableBeanFactory) {
    2. EmbeddedValueResolver embeddedValueResolver = new EmbeddedValueResolver((ConfigurableBeanFactory)beanFactory);
    3. qualifier = embeddedValueResolver.resolveStringValue(qualifier);
    4. }

    输入代码片段,记得点击一下这个图标:

    点击执行之后是这样的:

    然后看输出日志,你可以看到这样一行:

    说明我的“偷梁换柱”大法成功了。

    这不比你去编译一份 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 的未来是继续向前,再攀高峰,还是由盛转衰,锋芒挫缩,你我拭目以待。

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

    总结了很多有关于java面试的资料,希望能够帮助正在学习java的小伙伴。由于资料过多不便发表文章,创作不易,望小伙伴们能够给我一些动力继续创建更好的java类学习资料文章,
    请多多支持和关注小作,别忘了点赞+评论+转发。右上角私信我回复【999】即可领取免费学习资料谢谢啦!

     

  • 相关阅读:
    中小企业如何降低网络攻击和数据泄露的风险?
    加速乐源码(golang版本)
    Spring Boot 系列 —— Spring Webflux
    Python面向对象
    机器学习实战应用案例100篇(十)-蝙蝠算法从原理到实战应用案例
    Android OpenGL ES 学习(五) -- 渐变色
    Qt学习26 布局管理综合实例
    贪心算法实例(一):多任务分配问题
    Python教程之正则表达式实现拼音与中文提取并存储excel(教程含源码)
    重难点详解-根据函数依赖求第几范式
  • 原文地址:https://blog.csdn.net/m0_67322837/article/details/125599797