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

生产环境XMLWorkerHelper.parseXHtml偶发阻塞致RabbitMQ消息未确认问题

问题分析与解决方案

核心原因

线程dump显示阻塞在SocketInputStream.socketRead0,说明XMLWorkerHelper.parseXHtml()在解析HTML时尝试加载外部网络资源(比如HTML中引用的远程图片、CSS文件等),但由于目标服务无响应、网络波动或超时设置缺失,导致线程永久阻塞,进而让RabbitMQ消费者线程无法ACK消息,整个队列消费停滞。

具体解决方案

1. 禁用外部资源加载(优先推荐)

如果HTML转PDF不需要依赖外部资源,直接自定义资源解析器,阻止所有外部资源的加载请求:

// 自定义资源解析器,不加载任何外部资源
class NoOpResourceResolver implements ResourceResolver {
    @Override
    public InputStream resolve(String uri) {
        return new ByteArrayInputStream(new byte[0]); // 返回空输入流,避免网络请求
    }

    @Override
    public InputStream resolve(URI uri) {
        return new ByteArrayInputStream(new byte[0]);
    }
}

// 修改原代码中的parseXHtml调用逻辑
XMLWorker worker = XMLWorkerHelper.getInstance().createXMLWorker(writer);
worker.setResourceResolver(new NoOpResourceResolver());
worker.parse(new StringReader(fileAttachmentString));

2. 给外部资源加载设置超时(若必须加载外部资源)

如果确实需要加载远程资源,要为HTTP请求设置严格的超时时间,避免无限阻塞:

class TimeoutResourceResolver implements ResourceResolver {
    private static final int CONNECTION_TIMEOUT = 5000; // 5秒连接超时
    private static final int READ_TIMEOUT = 10000; // 10秒读取超时

    @Override
    public InputStream resolve(String uri) {
        try {
            URL url = new URL(uri);
            URLConnection conn = url.openConnection();
            conn.setConnectTimeout(CONNECTION_TIMEOUT);
            conn.setReadTimeout(READ_TIMEOUT);
            return conn.getInputStream();
        } catch (IOException e) {
            log.error("加载外部资源失败: {}", uri, e);
            return new ByteArrayInputStream(new byte[0]);
        }
    }

    @Override
    public InputStream resolve(URI uri) {
        return resolve(uri.toString());
    }
}

// 使用带超时的资源解析器
XMLWorker worker = XMLWorkerHelper.getInstance().createXMLWorker(writer);
worker.setResourceResolver(new TimeoutResourceResolver());
worker.parse(new StringReader(fileAttachmentString));

3. 为RabbitMQ消费者添加超时保护

在消费者配置中设置消息消费超时,当消费线程超过指定时间未完成时,自动让RabbitMQ重新投递消息,避免队列阻塞:

  • 若使用Spring AMQP,可配置SimpleMessageListenerContainer的receiveTimeout,并结合RetryTemplate实现超时重试逻辑
  • 原生RabbitMQ客户端可通过Future+get(timeout)的方式执行转PDF任务,超时则抛出异常并拒绝消息,触发RabbitMQ的重新投递机制

4. 隔离转PDF任务到独立线程池

将HTML转PDF的耗时任务放到单独的线程池执行,并设置线程超时,避免阻塞RabbitMQ的核心消费线程:

// 初始化带超时控制的线程池
ExecutorService pdfExecutor = Executors.newFixedThreadPool(5);

// 在消费逻辑中提交任务并设置超时
Future<byte[]> pdfFuture = pdfExecutor.submit(() -> {
    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    Document document = new Document();
    PdfWriter writer = PdfWriter.getInstance(document, outputStream);
    document.open();
    XMLWorker worker = XMLWorkerHelper.getInstance().createXMLWorker(writer);
    worker.setResourceResolver(new NoOpResourceResolver()); // 或使用超时解析器
    worker.parse(new StringReader(fileAttachmentString));
    document.close();
    return outputStream.toByteArray();
});

try {
    byte[] bytes = pdfFuture.get(30, TimeUnit.SECONDS); // 设置30秒超时阈值
    // 后续邮件发送逻辑...
} catch (TimeoutException e) {
    log.error("HTML转PDF超时", e);
    pdfFuture.cancel(true);
    // 拒绝消息,让RabbitMQ将消息转入死信队列或重新投递
    throw new AmqpRejectAndDontRequeueException("PDF转换超时");
} catch (Exception e) {
    log.error("PDF转换失败", e);
    throw new AmqpRejectAndDontRequeueException("PDF转换失败");
}

内容的提问来源于stack exchange,提问作者msagala25

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.15 06:53:24