Java中如何捕获被中断的Future抛出的所有异常?
为PDF保存操作实现超时机制时的异常捕获问题
我正尝试为Apache PDFBOX的PDDocument保存操作实现超时机制,目前编写了如下代码:
private Boolean saveDocumentWithTimeout(Path path, PDDocument pdDocument) { ExecutorService executor = Executors.newSingleThreadExecutor(); ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); try { Callable<Boolean> taskSavePDF = () -> { try { pdDocument.save(path.toString()); return true; } catch (ClosedChannelException e) { return false; // 从未触发 } catch(Exception e) { return false; // 从未触发 } }; Future<Boolean> future = executor.submit(taskSavePDF); scheduler.schedule(() -> future.cancel(true), 1, TimeUnit.MINUTES); boolean success; try { success = future.get(); } catch (CancellationException e) { success = false; } catch (ExecutionException | InterruptedException e) { success = false; } if (!success) { pdDocument.close(); System.out.println("Could not save PDF"); } return success; } catch (IOException | ConcurrentModificationException e) { return false; } finally { executor.shutdownNow(); scheduler.shutdownNow(); } }
说明:PDDocument来自Apache PDFBOX。
我遇到的问题是:当超时触发、调用future.cancel(true)中断保存操作后,pdDocument.save(path.toString())会抛出大量异常(比如ClosedChannelException),这些异常会被软件的全局异常捕获机制捕获并展示给用户。我尝试在Future的Callable内部用try/catch,但这些异常根本没被捕获到。请问有没有办法在中断Future的同时,捕获线程在整个关闭阶段抛出的所有异常?
以下是其中一个未被捕获的异常日志:
2024-07-08 16:24:35,019 INFO [stdout] (pool-25-thread-1) java.nio.channels.ClosedChannelException: null 2024-07-08 16:24:35,019 INFO [stdout] (pool-25-thread-1) at java.base/sun.nio.ch.FileChannelImpl.ensureOpen(Unknown Source) 2024-07-08 16:24:35,019 INFO [stdout] (pool-25-thread-1) at java.base/sun.nio.ch.FileChannelImpl.position(Unknown Source) 2024-07-08 16:24:35,019 INFO [stdout] (pool-25-thread-1) at deployment.lovisaear-7.185.0-SNAPSHOT.ear//org.apache.pdfbox.io.RandomAccessReadBufferedFile.seek(RandomAccessReadBufferedFile.java:143) 2024-07-08 16:24:35,019 INFO [stdout] (pool-25-thread-1) at deployment.lovisaear-7.185.0-SNAPSHOT.ear//org.apache.pdfbox.io.RandomAccessReadView.seek(RandomAccessReadView.java:90) 2024-07-08 16:24:35,019 INFO [stdout] (pool-25-thread-1) at deployment.lovisaear-7.185.0-SNAPSHOT.ear//org.apache.pdfbox.cos.COSStream.createRawInputStream(COSStream.java:143) 2024-07-08 16:24:35,019 INFO [stdout] (pool-25-thread-1) at deployment.lovisaear-7.185.0-SNAPSHOT.ear//org.apache.pdfbox.cos.COSStream.createView(COSStream.java:196) 2024-07-08 16:24:35,019 INFO [stdout] (pool-25-thread-1) at deployment.lovisaear-7.185.0-SNAPSHOT.ear//org.apache.pdfbox.pdfparser.PDFObjectStreamParser.<init>(PDFObjectStreamParser.java:51) 2024-07-08 16:24:35,019 INFO [stdout] (pool-25-thread-1) at deployment.lovisaear-7.185.0-SNAPSHOT.ear//org.apache.pdfbox.pdfparser.COSParser.parseObjectStreamObject(COSParser.java:825) 2024-07-08 16:24:35,019 INFO [stdout] (pool-25-thread-1) at deployment.lovisaear-7.185.0-SNAPSHOT.ear//org.apache.pdfbox.pdfparser.COSParser.parseObjectDynamically(COSParser.java:674) 2024-07-08 16:24:35,019 INFO [stdout] (pool-25-thread-1) at deployment.lovisaear-7.185.0-SNAPSHOT.ear//org.apache.pdfbox.pdfparser.COSParser.dereferenceCOSObject(COSParser.java:623) 2024-07-08 16:24:35,019 INFO [stdout] (pool-25-thread-1) at deployment.lovisaear-7.185.0-SNAPSHOT.ear//org.apache.pdfbox.cos.COSObject.getObject(COSObject.java:121) 2024-07-08 16:24:35,019 INFO [stdout] (pool-25-thread-1) at deployment.lovisaear-7.185.0-SNAPSHOT.ear//org.apache.pdfbox.pdfwriter.compress.COSWriterCompressionPool.addStructure(COSWriterCompressionPool.java:164) 2024-07-08 16:24:35,019 INFO [stdout] (pool-25-thread-1) at deployment.lovisaear-7.185.0-SNAPSHOT.ear//org.apache.pdfbox.pdfwriter.compress.COSWriterCompressionPool.<init>(COSWriterCompressionPool.java:84) 2024-07-08 16:24:35,019 INFO [stdout] (pool-25-thread-1) at deployment.lovisaear-7.185.0-SNAPSHOT.ear//org.apache.pdfbox.pdfwriter.COSWriter.doWriteBodyCompressed(COSWriter.java:474) 2024-07-08 16:24:35,019 INFO [stdout] (pool-25-thread-1) at deployment.lovisaear-7.185.0-SNAPSHOT.ear//org.apache.pdfbox.pdfwriter.COSWriter.visitFromDocument(COSWriter.java:1295) 2024-07-08 16:24:35,019 INFO [stdout] (pool-25-thread-1) at deployment.lovisaear-7.185.0-SNAPSHOT.ear//org.apache.pdfbox.cos.COSDocument.accept(COSDocument.java:429) 2024-07-08 16:24:35,019 INFO [stdout] (pool-25-thread-1) at deployment.lovisaear-7.185.0-SNAPSHOT.ear//org.apache.pdfbox.pdfwriter.COSWriter.write(COSWriter.java:1568) 2024-07-08 16:24:35,020 INFO [stdout] (pool-25-thread-1) at deployment.lovisaear-7.185.0-SNAPSHOT.ear//org.apache.pdfbox.pdfwriter.COSWriter.write(COSWriter.java:1444) 2024-07-08 16:24:35,020 INFO [stdout] (pool-25-thread-1) at deployment.lovisaear-7.185.0-SNAPSHOT.ear//org.apache.pdfbox.pdmodel.PDDocument.save(PDDocument.java:1034) 2024-07-08 16:24:35,020 INFO [stdout] (pool-25-thread-1) at deployment.lovisaear-7.185.0-SNAPSHOT.ear//org.apache.pdfbox.pdmodel.PDDocument.save(PDDocument.java:984) 2024-07-08 16:24:35,020 INFO [stdout] (pool-25-thread-1) at deployment.lovisaear-7.185.0-SNAPSHOT.ear//org.apache.pdfbox.pdmodel.PDDocument.save(PDDocument.java:940) 2024-07-08 16:24:35,020 INFO [stdout] (pool-25-thread-1) at deployment.lovisaear-7.185.0-SNAPSHOT.ear//org.apache.pdfbox.pdmodel.PDDocument.save(PDDocument.java:923) 2024-07-08 16:24:35,020 INFO [stdout] (pool-25-thread-1) at deployment.lovisaear-7.185.0-SNAPSHOT.ear.dokumentsamling-server-7.185.0-SNAPSHOT.jar//com.computas.lovisa.dokumentsamling.server.model.PDFIDokumentsamling.la 2024-07-08 16:24:35,020 INFO [stdout] (pool-25-thread-1) at java.base/java.util.concurrent.FutureTask.run(Unknown Source) 2024-07-08 16:24:35,020 INFO [stdout] (pool-25-thread-1) at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source) 2024-07-08 16:24:35,020 INFO [stdout] (pool-25-thread-1) at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source) 2024-07-08 16:24:35,020 INFO [stdout] (pool-25-thread-1) at java.base/java.lang.Thread.run(Unknown Source)
问题根源
当调用future.cancel(true)时,会给执行保存任务的线程发送中断信号,但PDFBOX的save方法内部可能存在后台子线程(比如压缩池线程),这些子线程抛出的异常不会进入你Callable的try/catch块,而是直接进入线程的未捕获异常处理器。另外,你在中断后立即调用pdDocument.close(),会触发更多IO异常——此时文档资源处于未完成保存的不一致状态,关闭操作会引发额外错误。
修复方案
1. 给线程池设置自定义未捕获异常处理器
创建线程池时,通过ThreadFactory给每个线程设置异常处理器,直接拦截所有后台异常:
private Boolean saveDocumentWithTimeout(Path path, PDDocument pdDocument) { // 自定义线程工厂,拦截未捕获异常 ThreadFactory threadFactory = r -> { Thread t = new Thread(r); t.setUncaughtExceptionHandler((thread, throwable) -> { // 这里可以记录日志或静默处理,避免异常暴露给用户 // log.debug("PDF保存线程异常", throwable); }); return t; }; ExecutorService executor = Executors.newSingleThreadExecutor(threadFactory); ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1, threadFactory); try { Callable<Boolean> taskSavePDF = () -> { try { pdDocument.save(path.toString()); return true; } catch (Exception e) { // 捕获save方法直接抛出的异常 return false; } }; Future<Boolean> future = executor.submit(taskSavePDF); scheduler.schedule(() -> future.cancel(true), 1, TimeUnit.MINUTES); boolean success; try { success = future.get(); } catch (CancellationException | ExecutionException | InterruptedException e) { success = false; } // 关闭文档前先检查状态,避免重复关闭引发异常 if (!success && !pdDocument.isClosed()) { try { pdDocument.close(); } catch (IOException e) { // 捕获关闭时的异常 } System.out.println("Could not save PDF"); } return success; } catch (IOException | ConcurrentModificationException e) { return false; } finally { executor.shutdownNow(); scheduler.shutdownNow(); } }
2. 中断后等待线程池清理,再关闭文档
中断任务后,先给线程池一点时间完成资源清理,再尝试关闭文档,减少额外异常:
finally { executor.shutdown(); scheduler.shutdown(); try { // 等待线程池任务结束,最多等5秒 if (!executor.awaitTermination(5, TimeUnit.SECONDS)) { executor.shutdownNow(); } if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) { scheduler.shutdownNow(); } } catch (InterruptedException e) { executor.shutdownNow(); scheduler.shutdownNow(); } // 最后统一关闭文档 if (!pdDocument.isClosed()) { try { pdDocument.close(); } catch (IOException e) { // 处理关闭异常 } } }
3. 在Callable中响应中断信号
主动检查线程中断状态,提前退出任务,减少后续异常:
Callable<Boolean> taskSavePDF = () -> { try { pdDocument.save(path.toString()); // 检查中断状态,避免任务完成前被中断 if (Thread.currentThread().isInterrupted()) { throw new InterruptedException("保存任务被中断"); } return true; } catch (InterruptedException e) { Thread.currentThread().interrupt(); // 重置中断状态 return false; } catch (Exception e) { return false; } };
关键说明
- 未捕获异常处理器是拦截PDFBOX后台线程异常的核心,因为这些子线程的异常不会传递到你的Callable代码中。
- 中断后不要立即强制关闭文档,给线程池留足清理时间,能大幅减少额外异常。
future.cancel(true)仅发送中断信号,PDFBOX的save方法在IO操作时会响应这个信号,但后台线程的异常仍需单独处理。
内容的提问来源于stack exchange,提问作者Tmfwang
相关产品推荐
相关产品推荐

