Spring Integration遇FolderClosedException:邮件过滤流程故障排查
问题描述
我想对接收到的邮件做过滤:如果邮件正文包含github.com、trello.com或bitbucket.com这些域名,就不自动回复。我写了对应的Spring Integration Flow,但运行时Transformer抛出javax.mail.FolderClosedException,提示无法打开收件箱文件夹,怀疑是Java Mail API的问题,求解决办法。
代码实现
IntegrationFlow Bean
@Bean open fun flow(): IntegrationFlow { return IntegrationFlows .from("emailReceiveChannel") .transform(transformer()) .filter("@messageFilter.containsDomainNames('payload')") .handle(MessageHandler(MailServiceImpl(javaMailSender(), mailStore()))) .get() }
Transformer定义
@Bean @Transformer(inputChannel = "emailReceiveChannel", outputChannel = "outputChannel") open fun transformer(): org.springframework.integration.transformer.Transformer { return MailToStringTransformer() }
MessageFilter类
@Component class MessageFilter { @Filter open fun containsDomainNames(messageBody: String): Boolean { return messageBody.contains("github.com") || messageBody.contains("trello.com") || messageBody.contains("bitbucket.com") } }
其他相关Bean
@Bean @InboundChannelAdapter(autoStartup = "true", value = "emailReceiveChannel", poller = (arrayOf(Poller(fixedDelay = "10000", maxMessagesPerPoll = "10")))) open fun mailReceivingMessageSource(mailReceiver: ImapMailReceiver): MailReceivingMessageSource { return MailReceivingMessageSource(mailReceiver) } @Bean open fun pollingConsumer(): PollingConsumer { return PollingConsumer(emailReceiveChannel(), MessageHandler(MailServiceImpl(javaMailSender(), mailStore()))) } @Bean open fun outputChannel(): PollableChannel { return QueueChannel() } @Bean open fun emailReceiveChannel(): PollableChannel { return QueueChannel(10) }
错误栈追踪
2018-01-20 13:05:53.840 ERROR 8204 --- [ask-scheduler-6] o.s.integration.handler.LoggingHandler : org.springframework.integration.transformer.MessageTransformationException: failed to transform mail message; nested exception is javax.mail.FolderClosedException, failedMessage=GenericMessage [payload=org.springframework.integration.mail.AbstractMailReceiver$IntegrationMimeMessage@3f0ef4a2, headers={id=e9eff05d-e9af-70e4-73f7-c4d39c740291, timestamp=1516449953838}] at org.springframework.integration.mail.transformer.AbstractMailMessageTransformer.transform(AbstractMailMessageTransformer.java:83) at org.springframework.integration.transformer.MessageTransformingHandler.handleRequestMessage(MessageTransformingHandler.java:89) at org.springframework.integration.handler.AbstractReplyProducingMessageHandler.handleMessageInternal(AbstractReplyProducingMessageHandler.java:109) at org.springframework.integration.handler.AbstractMessageHandler.handleMessage(AbstractMessageHandler.java:131) at org.springframework.integration.endpoint.PollingConsumer.handleMessage(PollingConsumer.java:129) at org.springframework.integration.endpoint.AbstractPollingEndpoint.doPoll(AbstractPollingEndpoint.java:271) at org.springframework.integration.endpoint.AbstractPollingEndpoint$Poller.lambda$run$0(AbstractPollingEndpoint.java:372) at org.springframework.integration.util.ErrorHandlingTaskExecutor.lambda$execute$0(ErrorHandlingTaskExecutor.java:53) at org.springframework.core.task.SyncTaskExecutor.execute(SyncTaskExecutor.java:50) at org.springframework.integration.util.ErrorHandlingTaskExecutor.execute(ErrorHandlingTaskExecutor.java:51) at org.springframework.integration.endpoint.AbstractPollingEndpoint$Poller.run(AbstractPollingEndpoint.java:366) at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54) at org.springframework.scheduling.concurrent.ReschedulingRunnable.run(ReschedulingRunnable.java:83) at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) at java.util.concurrent.FutureTask.run(FutureTask.java:266) at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:180) at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748) Caused by: javax.mail.FolderClosedException at com.sun.mail.imap.IMAPMessage.getProtocol(IMAPMessage.java:153) at com.sun.mail.imap.IMAPBodyPart.loadHeaders(IMAPBodyPart.java:390) at com.sun.mail.imap.IMAPBodyPart.getNonMatchingHeaderLines(IMAPBodyPart.java:371) at javax.mail.internet.MimeBodyPart.writeTo(MimeBodyPart.java:1536) at javax.mail.internet.MimeBodyPart.writeTo(MimeBodyPart.java:948) at javax.mail.internet.MimeMultipart.writeTo(MimeMultipart.java:538) at org.springframework.integration.mail.transformer.MailToStringTransformer.doTransform(MailToStringTransformer.java:62) at org.springframework.integration.mail.transformer.AbstractMailMessageTransformer.transform(AbstractMailMessageTransformer.java:80) ... 19 more
解决办法
这个问题本质是IMAP文件夹在消息还没处理完就被关闭了。Spring Integration的ImapMailReceiver默认拉取完消息后就会关闭文件夹,但你的MailToStringTransformer要读取邮件内容时,文件夹已经关闭,所以抛出了FolderClosedException。下面是几个实用的解决思路:
1. 让Receiver提前加载邮件内容到内存
最简单的办法是配置ImapMailReceiver在拉取消息时就把邮件内容读取到本地,后续Transformer处理时就不需要再访问远程IMAP文件夹了。只需要给ImapMailReceiver设置simpleContent=true:
@Bean open fun imapMailReceiver(): ImapMailReceiver { val receiver = ImapMailReceiver("imap://your-email:password@your-imap-server:port/inbox") receiver.setSimpleContent(true) // 提前加载邮件内容到内存 return receiver }
这个方案不需要管理文件夹生命周期,适合大多数场景,推荐优先尝试。
2. 禁止Receiver自动关闭文件夹
如果需要保留IMAP连接的状态,可以设置autoCloseFolder=false,让Receiver保持文件夹打开,直到手动关闭或者应用停止:
@Bean open fun imapMailReceiver(): ImapMailReceiver { val receiver = ImapMailReceiver("imap://your-email:password@your-imap-server:port/inbox") receiver.setAutoCloseFolder(false) // 关闭自动关闭文件夹的行为 receiver.setShouldDeleteMessages(false) // 根据需求设置是否删除邮件,默认是false return receiver }
注意:如果用这个方案,要确保应用关闭时正确清理资源,避免连接泄漏。如果是长时间运行的服务,推荐搭配IdleImapMailReceiver使用,它会保持IMAP连接的活跃状态。
3. 修复Integration Flow的配置冲突
看你的代码,你同时定义了IntegrationFlow和PollingConsumer两个Bean,这会导致消息被重复消费或者通道逻辑混乱。IntegrationFlows.from()已经会自动创建对应的消费者端点,所以建议删除单独的PollingConsumer Bean,只保留IntegrationFlow的定义。
4. 调整Transformer的通道配置
你的Transformer注解指定了inputChannel和outputChannel,但在IntegrationFlow中又调用了.transform(transformer()),这会导致Transformer被重复注册,引发通道冲突。可以二选一:
- 要么去掉Transformer的
@Transformer注解,只在IntegrationFlow中使用它; - 要么修改
IntegrationFlow直接使用Transformer定义的输出通道:
@Bean open fun flow(): IntegrationFlow { return IntegrationFlows .from("emailReceiveChannel") .channel("outputChannel") // 直接用Transformer的输出通道 .filter("@messageFilter.containsDomainNames('payload')") .handle(MessageHandler(MailServiceImpl(javaMailSender(), mailStore()))) .get() }
内容的提问来源于stack exchange,提问作者user3285241

