You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 12:37:02