起因
在一个迭代的需求中,需要记录用户的行为,想着记录用户行为这部分业务逻辑可以修改为异步执行,就使用了CompletableFuture的runAsync()方法实现异步,本来本地环境自己测试也没报错,可是发到开发环境服务器上的时候却报错了。
报错伪代码
- CompletableFuture.runAsync(()->{
- //掉用user服务保存用户行为
- userClient.saveUserBehavior();
- });
- 复制代码
错误信息以及使用的环境
环境:jdk使用的是openjdk11,SpringCloud版本是2020.0.3
- java.lang.ClassNotFoundException: org.springframework.boot.autoconfigure.condition.OnPropertyCondition
- 复制代码
问题排除过程
因为本地环境是没有报错的,但是开发环境却报错了,所以一开始检查代码时候没有合并,是否没有将代码提交到开发环境。但是重新检查之后,发现代码是已经合并并且提交到开发分支。随后开始分析报错原因,看到这个错误的时候,相信大家和我一样第一反应会认为是Jar包冲突了,但是在仔细检查依赖的Spring的版本,发现Spring的版本也并没有冲突。想着难道是我改的代码改出问题了?我慌了,我也只是把代码修改成了异步啊,于是乎将代码改回同步,果然就没有问题了。
尝试寻找产生问题的原因
虽然问题解决了,但是我是一个有着好奇心的程序员,为什么在CompletableFuture掉用Fegin接口会报错,是什么原因导致的?带着这样的疑问,我开始在GitHub上逛了起来。心想这样使用的人应该不止我一个吧,所以应该也有不少同学也遇到了这样的问题吧。果然github没有让我失望,上面真的有我想要的东西。(所以程序员要多在Github上摸鱼)
在这个issues中,我找到了答案,issues的提交者和我遇到的问题一样,甚至连报错信息都那么的像,我顿时心里乐开了花。总的来说就是Fegin接口是懒加载的,只有在我们第一次使用该Fegin接口的时候才会对Fegin接口进行初始化,但是如果在ForkJoinWorkerThread中使用Fegin接口的话,就会出现ClassNotFoundException。使用其他普通的JVM线程池是不会出现问题的。
接下来我们看下Spring Cloud团队对于这个问题的回答
Spring Cloud团队的人给出的回答是这样的:
大概说的就是在并行流(多线程)中调用 FeignBlockingLoadBalancerClient时会出现 ClassNotFoundException。
但是经过在本地测试,只有在使用ForkJoinPool线程池中调用Fegin接口才会报错,如果我们在ThreadPoolExecutor自定义线程池或者使用jdk提供的线程池(例如FixedThreadPool)中调用Fegin接口是不会报错的,而恰巧CompletableFuture中的默认线程池也是通过ForkJoinPool线程池来实现的。难道真的有这么巧吗?这两位开发者证实了在ForkJoinPool中使用Fegin接口会出现这个错误,但是其他JVM线程池是可以正常运行的。
那为什么在本地环境测试没有问题,但是发布到dev环境却出现了问题呢?
带着这个疑问我检查了开发环境与本地环境的差异,发现本地环境使用的JDk版本是11,但是dev环境使用的JDK是Openjdk11。随后我在本地替换jdk的版本为openJdk,在本地进行进一步测试。
果然代码再次报错。
- java.lang.ClassNotFoundException: org.springframework.boot.autoconfigure.condition.OnPropertyCondition
- at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(Unknown Source) ~[na:na]
- at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(Unknown Source) ~[na:na]
- at java.base/java.lang.ClassLoader.loadClass(Unknown Source) ~[na:na]
- at java.base/java.lang.Class.forName0(Native Method) ~[na:na]
- at java.base/java.lang.Class.forName(Unknown Source) ~[na:na]
- 复制代码
那么这个问题Spring Cloud团队解决了吗?
很遗憾,并没有解决。Spring Cloud团队认为这个问题的优先级并不高,并且不推荐在CompletableFuture中调用Fegin接口,还表示如果社区有人愿意为这个问题提交代码,Spring Cloud团队也会认真审核代码,如果通过的话也会合并代码。
好吧,那看来我只能使用自定义TreadPool来实现异步了(当然也可以使用mq)。
总结
只有在openJDK11版本下才会出现这个问题(回家后我也试过JDK8,并不会出现异常),并且如果在ForkJoinPool中调用Fegin接口,那么就会出现ClassNotFoundException。因为Spring Cloud团队认为这个问题的优先级不高,并没有解决这个问题。虽然CompletableFuture真的很好用,但是如果你需要在CompletableFuture中调用Fegin接口,那么建议定义一个自定义的线程池,不要使用默认的ForkJoinPool.
使用下面这样的api,CompletableFuture就会使用我们传入的线程池去执行任务。
- public static CompletableFuture<Void> runAsync(Runnable runnable,
- Executor executor) {
- return asyncRunStage(screenExecutor(executor), runnable);
- }
- 复制代码
所以小伙伴们如果需要异步调用Fegin接口,还需要多加注意呀,避免踩坑。
如果有任何疑问,欢迎在下方评论区留言。最后,原创不易,如果本文对你有所帮助,那么点个赞再走吧。