likes
comments
collection
share

我人都傻了,CompletableFuture和OpenFegin一起使用竟然报错

作者站长头像
站长
· 阅读数 17

起因

在一个迭代的需求中,需要记录用户的行为,想着记录用户行为这部分业务逻辑可以修改为异步执行,就使用了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和OpenFegin一起使用竟然报错

尝试寻找产生问题的原因

虽然问题解决了,但是我是一个有着好奇心的程序员,为什么在CompletableFuture掉用Fegin接口会报错,是什么原因导致的?带着这样的疑问,我开始在GitHub上逛了起来。心想这样使用的人应该不止我一个吧,所以应该也有不少同学也遇到了这样的问题吧。果然github没有让我失望,上面真的有我想要的东西。(所以程序员要多在Github上摸鱼)

github.com/spring-clou…

在这个issues中,我找到了答案,issues的提交者和我遇到的问题一样,甚至连报错信息都那么的像,我顿时心里乐开了花。总的来说就是Fegin接口是懒加载的,只有在我们第一次使用该Fegin接口的时候才会对Fegin接口进行初始化,但是如果在ForkJoinWorkerThread中使用Fegin接口的话,就会出现ClassNotFoundException。使用其他普通的JVM线程池是不会出现问题的。

我人都傻了,CompletableFuture和OpenFegin一起使用竟然报错

接下来我们看下Spring Cloud团队对于这个问题的回答

Spring Cloud团队的人给出的回答是这样的:

我人都傻了,CompletableFuture和OpenFegin一起使用竟然报错

大概说的就是在并行流(多线程)中调用 FeignBlockingLoadBalancerClient时会出现 ClassNotFoundException。

但是经过在本地测试,只有在使用ForkJoinPool线程池中调用Fegin接口才会报错,如果我们在ThreadPoolExecutor自定义线程池或者使用jdk提供的线程池(例如FixedThreadPool)中调用Fegin接口是不会报错的,而恰巧CompletableFuture中的默认线程池也是通过ForkJoinPool线程池来实现的。难道真的有这么巧吗?这两位开发者证实了在ForkJoinPool中使用Fegin接口会出现这个错误,但是其他JVM线程池是可以正常运行的。

我人都傻了,CompletableFuture和OpenFegin一起使用竟然报错

我人都傻了,CompletableFuture和OpenFegin一起使用竟然报错

那为什么在本地环境测试没有问题,但是发布到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团队解决了吗?

我人都傻了,CompletableFuture和OpenFegin一起使用竟然报错

很遗憾,并没有解决。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接口,还需要多加注意呀,避免踩坑。

如果有任何疑问,欢迎在下方评论区留言。最后,原创不易,如果本文对你有所帮助,那么点个赞再走吧。