Logback未将ThreadPoolTaskExecutor线程抛出的异常记录到日志文件
你遇到的异常没有写入日志文件的核心原因是:ThreadPoolTaskExecutor执行execute()方法提交的任务抛出未捕获异常时,异常会被线程的默认UncaughtExceptionHandler处理,默认实现是直接将堆栈打印到System.err,并未走Slf4j/Logback的日志体系。
控制台能看到异常是因为System.err默认输出到控制台,但Logback的文件Appender仅采集日志框架输出的内容,自然不会采集到System.err的输出,所以日志文件里只有你主动打印的INFO级日志。
有三种常用的修复方式,可根据业务场景选择:
方案1:业务代码主动捕获异常打印日志(最稳妥,适用所有场景)
在所有提交到线程池的Runnable任务外层包裹try-catch,捕获异常后主动调用log.error输出,示例如下:goodsCommissionJobPool.execute(() -> { try { log.info("first level.."); goodsCommissionAlertPool.execute(() -> { try { log.info("second level.."); throw new RuntimeException("funny error"); } catch (Exception e) { log.error("二级任务执行异常", e); } }); throw new RuntimeException("first error"); } catch (Exception e) { log.error("一级任务执行异常", e); } });注意输出异常时必须把异常对象作为第二个参数传给
log.error,才能打印完整堆栈。方案2:给线程池配置全局异常处理器(统一管控,不需要每个任务加try-catch)
自定义线程工厂,为所有池化线程设置UncaughtExceptionHandler,在处理器中统一打印异常,修改线程池配置示例:@Bean public ThreadPoolTaskExecutor goodsCommissionJobPool() { ThreadPoolTaskExecutor pool = new ThreadPoolTaskExecutor(); pool.setThreadFactory(new ThreadFactoryBuilder() .setNameFormat("Goods_Commission_Job_%d") .setUncaughtExceptionHandler((thread, throwable) -> { log.error("线程{}执行任务异常", thread.getName(), throwable); }) .build()); // 其余原有配置保持不变 pool.setWaitForTasksToCompleteOnShutdown(true); pool.setAwaitTerminationSeconds(10); pool.setQueueCapacity(1000); pool.setMaxPoolSize(8); pool.setCorePoolSize(4); pool.setKeepAliveSeconds(60); return pool; }两个线程池都按同样方式配置即可,ThreadFactoryBuilder可以用Guava提供的,也可以自己实现ThreadFactory接口。
方案3:用
submit()方法提交任务(仅适用需要获取返回值的场景,注意坑点)
如果用线程池的submit()方法提交任务,异常会被封装到返回的Future对象中,只有你调用Future.get()方法时才会抛出异常,你可以在调用get的位置捕获异常打印。注意如果不调用get()方法,异常会被直接吞掉,连控制台都看不到。
你提供的Logback配置本身没有问题,root节点已经绑定了error级别的文件Appender,只要异常是通过Logback的API输出的,会自动写入对应的error日志文件。
内容的提问来源于stack exchange,提问作者Khirye Zhou

