Spring中使用FreeMarker并行流时触发TemplateNotFoundException
并行处理FreeMarker模板时出现TemplateNotFoundException的解决办法
问题场景
在Spring项目中使用FreeMarker生成邮件模板时,通过parallelStream并行遍历条目列表处理:
reports.parallelStream().forEach { creationProcess.startCreationProcess(it) }
当列表仅含1条条目时模板处理正常,但条目数量增加时,抛出如下异常:
freemarker.template.TemplateNotFoundException: Template not found for name "my_template.ftl". The name was interpreted by this TemplateLoader: MultiTemplateLoader(loader1 = org.springframework.ui.freemarker.SpringTemplateLoader@3a1332a3, loader2 = ClassTemplateLoader(resourceLoaderClass=org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer, basePackagePath="" /* relatively to resourceLoaderClass pkg */)). at freemarker.template.Configuration.getTemplate(Configuration.java:3026) at freemarker.template.Configuration.getTemplate(Configuration.java:2828) at my.project.smtp.service.DefaultSmtpService.sendEmailWithTemplateAndAttachment(DefaultSmtpService.kt:36) at my.project.adapters.driven.SmtpAdapter.sendEmail(SmtpAdapter.kt:46) at my.project.core.services.mail.CompletionMailService.sendMailToAuditorAndAdditionalMailAddresses(CompletionMailService.kt:74) at my.project.core.services.mail.CompletionMailService.sendMails(CompletionMailService.kt:33) at my.project.core.services.CreationProcess.scheduledAutomaticProcess(CreationProcess.kt:80) at my.project.core.services.CreationService.startCreationProcess(CreationService.kt:52) at my.project.core.services.CreationService.access$startCreationProcess(CreationService.kt:15) at my.project.core.services.CreationService$startAsyncCreationProcess$1.invoke(CreationService.kt:31) at my.project.core.services.CreationService$startAsyncCreationProcess$1.invoke(CreationService.kt:30) at my.project.core.services.CreationService.startAsyncCreationProcess$lambda$0(CreationService.kt:30) at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:183) at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1625) at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509) at java.base/java.util.stream.ForEachOps$ForEachTask.compute(ForEachOps.java:290) at java.base/java.util.concurrent.CountedCompleter.exec(CountedCompleter.java:754) at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:373) at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1182) at java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1655) at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1622) at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:165)
获取模板的代码如下:
val emailTemplate = freemarkerConfigurer.configuration.getTemplate(templateName)
问题原因
Spring的FreeMarkerConfigurer内部默认使用的模板加载器(如SpringTemplateLoader)并非线程安全,并行流的多线程环境下同时调用getTemplate会导致加载器内部状态混乱,无法正确定位模板文件。
解决办法
提前预加载模板:在应用启动的单线程环境下,预先加载所有需要的模板并存入线程安全的缓存中,并行处理时直接从缓存获取模板。
示例代码:// 线程安全的模板缓存 private val templateCache = ConcurrentHashMap<String, Template>() // 应用初始化时执行(单线程环境) fun preloadTemplates() { val requiredTemplates = listOf("my_template.ftl") requiredTemplates.forEach { name -> templateCache[name] = freemarkerConfigurer.configuration.getTemplate(name) } } // 并行处理时使用缓存模板 val emailTemplate = templateCache[templateName] ?: throw IllegalArgumentException("模板 $templateName 未预加载")对模板加载加锁:通过全局锁保证
getTemplate调用的线程安全性,避免多线程同时操作加载器。
示例代码:private val templateLoadLock = Any() fun getTemplateSafely(templateName: String): Template { synchronized(templateLoadLock) { return freemarkerConfigurer.configuration.getTemplate(templateName) } } // 业务代码中调用安全加载方法 val emailTemplate = getTemplateSafely(templateName)替换线程安全的模板加载器:将默认的
SpringTemplateLoader替换为线程安全的实现(如ClassTemplateLoader),注意配置正确的模板基础路径。
内容的提问来源于stack exchange,提问作者ochs.tobi
相关产品推荐
相关产品推荐

