CompletableFuture调用uniApply时出现NullPointerException的问题排查
偶发NullPointerException排查疑惑
我遇到了如下异常:
Caused by: java.lang.NullPointerException at java.util.concurrent.CompletableFuture.uniApply(CompletableFuture.java:616) ~[?:1.8.0_302] at java.util.concurrent.CompletableFuture$UniApply.tryFire(CompletableFuture.java:591) ~[?:1.8.0_302] at java.util.concurrent.CompletableFuture.postComplete(CompletableFuture.java:488) ~[?:1.8.0_302] at java.util.concurrent.CompletableFuture.complete(CompletableFuture.java:1975) ~[?:1.8.0_302] at com.tcom.concurrent.ConcurrentUtils$3.onSuccess(ConcurrentUtils.java:140) ~[framework-20220815.38-RELEASE.jar:?]
该异常属于偶发现象,无法在系统中复现。
查看CompletableFuture.java源码后,初步怀疑是f这个Function变量为空导致的,但uniApply()方法第二行已经对f做了空检查,f不可能为空。
那为什么JVM会报告在函数调用行出现NPE?如果NPE是在被调用函数内部抛出的,栈跟踪应该会体现出来才对,这是怎么回事?
ConcurrentUtils.java相关代码:
public static <I, O> CompletableFuture<O> buildCompletableFuture(final ListenableFuture<I> listenableFuture, final Function<I, O> responseApplier, final Consumer<I> onSuccessConsumer, final Consumer<Throwable> onFailureConsumer, final Supplier<O> defaultValueOnExceptionSupplier, final Executor callBackExecutor) { //create an instance of CompletableFuture final CompletableFuture<I> innerComplete = new CompletableFuture<I>() { @Override public boolean cancel(boolean mayInterruptIfRunning) { // propagate cancel to the listenable future boolean result = listenableFuture.cancel(mayInterruptIfRunning); super.cancel(mayInterruptIfRunning); return result; } }; // add callback Futures.addCallback(listenableFuture, new FutureCallback<I>() { @Override public void onSuccess(I result) { innerComplete.complete(result); if (onSuccessConsumer != null) { onSuccessConsumer.accept(result); } } @Override public void onFailure(Throwable t) { innerComplete.completeExceptionally(t); if (onFailureConsumer != null) { onFailureConsumer.accept(t); } } }, callBackExecutor); CompletableFuture<O> returnedFuture = innerComplete.thenApply(responseApplier); if (defaultValueOnExceptionSupplier != null) { //when we have default value the exception will not thrown returnedFuture = returnedFuture.exceptionally((ex) -> defaultValueOnExceptionSupplier.get()); } return returnedFuture; }
CompletableFuture相关代码:
final <S> boolean uniApply(CompletableFuture<S> a, Function<? super S,? extends T> f, UniApply<S,T> c) { Object r; Throwable x; if (a == null || (r = a.result) == null || f == null) return false; tryComplete: if (result == null) { if (r instanceof AltResult) { if ((x = ((AltResult)r).ex) != null) { completeThrowable(x, r); break tryComplete; } r = null; } try { if (c != null && !c.claim()) return false; @SuppressWarnings("unchecked") S s = (S) r; completeValue(f.apply(s)); // This is CompletableFuture.java:616 } catch (Throwable ex) { completeThrowable(ex); } } return true; }
调用ConcurrentUtils.buildCompletableFuture的方式:
ConcurrentUtils.buildCompletableFuture(asyncTask, ObjectWithTimestamp::getObject, s -> {}, e -> logger.error("error message"), null, MoreExecutors.directExecutor())
uniApply()中的f实际就是ObjectWithTimestamp::getObject
问题分析与解决方向:
NPE的真实来源:
栈跟踪指向f.apply(s),但f已通过空检查,真实原因是**s为null时调用实例方法引用ObjectWithTimestamp::getObject抛出NPE**。JDK 8部分版本中,Lambda/方法引用抛出异常时,栈跟踪会被优化,导致异常的实际触发点没有完整打印,只显示到调用Lambda的位置。偶发原因:
异常偶发说明asyncTask返回的结果偶尔为null,或返回了AltResult类型且内部异常为null的对象——后者会在uniApply逻辑中被转为null,最终导致s为null。解决方案:
- 提前校验输入:将方法引用改为显式Lambda,提前检查参数是否为null:
s -> { if (s == null) { // 可返回默认值或抛出明确异常 return null; } return s.getObject(); } - 拦截null结果:在
ConcurrentUtils的onSuccess方法中添加null检查,避免将null传入innerComplete:@Override public void onSuccess(I result) { if (result == null) { innerComplete.completeExceptionally(new NullPointerException("ListenableFuture返回结果为null")); } else { innerComplete.complete(result); } if (onSuccessConsumer != null) { onSuccessConsumer.accept(result); } } - 排查上游任务:检查
asyncTask为何会偶尔返回null或异常结果,从根源避免无效数据流入后续逻辑。
- 提前校验输入:将方法引用改为显式Lambda,提前检查参数是否为null:
内容的提问来源于stack exchange,提问作者Lior Halfon
相关产品推荐
相关产品推荐

