生产环境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
相关产品推荐
相关产品推荐

