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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 23:50:00