应使用自定义异常收集器(如batcherroraccumulator)累积异常及上下文,避免空catch或仅存错误消息;需记录throwable、业务标识、时间戳,保证线程安全与实例隔离。

Java批量处理时如何不中断流程、收集全部异常
直接用 CollectingExceptionHandler 这类自定义容器,而不是靠 try-catch 单点吞异常。吞了没记录、丢了堆栈、后续没法定位——这是最常踩的坑。
核心思路是把异常当数据来“累积”,不是“拦截”。适用于 Stream 处理、for 循环批量校验、RPC 批量调用等场景。
- 别在循环里写空
catch (Exception e) { }—— 异常对象被丢弃,连日志都没留 - 不要用
List<string></string>存错误消息——丢失getCause()、getStackTrace()等关键上下文 - 如果用
CompletableFuture并行处理,记得每个分支都独立捕获并塞进共享的ConcurrentLinkedQueue<throwable></throwable>,否则异常会静默消失
用 List 还是自定义 Accumulator 类
用裸 List<throwable></throwable> 看似简单,但容易漏掉「原始输入上下文」。比如第 17 条订单解析失败,光有 NullPointerException 没法反查是哪个 orderNo 导致的。
推荐封装一个轻量 accumulator:
立即学习“Java免费学习笔记(深入)”;
public class BatchErrorAccumulator {
private final List<BatchFailure> failures = new ArrayList<>();
public void add(Throwable t, Object context) {
failures.add(new BatchFailure(t, context));
}
}
BatchFailure 至少带三个字段:throwable、context(如 Map<string object></string> 或原始对象引用)、timestamp。别省这十几行代码——线上排查时差的就是这一条线索。
- 并发场景下,用
CopyOnWriteArrayList或加锁,别用普通ArrayList配synchronized块,性能差还容易锁错对象 - 如果批量量级超万,避免把整个失败对象深拷贝进 accumulator,只存关键标识(如
id、index),事后查原始数据源补全
Stream API 中怎么安全地 collect 异常
Stream.forEach() 无法传播异常;map() 遇到异常直接中断。想边处理边攒错,必须绕过中间操作的短路逻辑。
正确做法:用 peek() + 外部 accumulator,或改用传统 for 循环。别信“函数式更优雅”的说法——这里它不适用。
List<Order> orders = ...;
BatchErrorAccumulator errors = new BatchErrorAccumulator();
orders.forEach(order -> {
try {
process(order);
} catch (Exception e) {
errors.add(e, order.getOrderNo()); // 记录具体订单号
}
});
-
Stream.collect()不适合做这事——它的combiner函数要求线程安全合并,而异常不是可结合的值 - 如果非要用
parallelStream(),确保 accumulator 的add()方法是线程安全的,且context是不可变或只读视图 - 注意
ForkJoinPool默认大小,大批量时可能压爆线程数,导致RejectedExecutionException被误当成业务异常收进 accumulator
异常收集后怎么用、哪些信息不能丢
收集只是第一步。真正麻烦的是后续:怎么导出、怎么告警、怎么让业务方能自助查。
至少保留三项:throwable.getClass().getSimpleName()、throwable.getMessage()、context 中的唯一业务键(如 userId 或 batchId)。堆栈可以截断,但第一行和 root cause 必须完整。
- 别把
errors.toString()直接打日志——默认只打印异常类名,看不到消息和堆栈 - 导出 CSV 时,对
getMessage()做replace("\n", " | "),否则 Excel 打开换行错乱 - 如果走监控系统(如 Prometheus),别用异常数量当指标——要按
throwable.getClass()分维度打点,否则NullPointerException和TimeoutException全混在一起,失去分类价值
最常被忽略的一点:accumulator 实例生命周期必须和单次批量任务严格对齐。复用旧实例、清空不彻底、或跨请求共享,都会导致错误污染——前一批的异常出现在后一批结果里,查半天发现是状态没隔离。








