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

Apache Camel+SpringBoot+Email集成中FTL模板渲染失败排查

Tip: If the failing expression is known to legally refer to something that's sometimes null or missing, either specify a default value like myOptionalVar!myDefault, or use <#if myOptionalVar??>when-present<#else>when-missing</#if>. (These only cover the last step of the expression; to cover the whole expression, use parenthesis: (myOptionalVar.foo)!myDefault, (myOptionalVar.foo)??
FTL stack trace ("~" means nesting-related):
- Failed at: #if ftlDataMap.replyMessages?size [in template "MyFtl.ftl" at line 61, column 9]

## 相关代码
### MyFtl.ftl模板代码
```html
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Email Reply</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            line-height: 1.6;
            margin: 0;
            padding: 0;
            background-color: #f4f4f4;
        }
        .email-container {
            width: 100%;
            max-width: 800px;
            margin: 20px auto;
            padding: 20px;
            border: 1px solid #ddd;
            border-radius: 5px;
            background-color: #fff;
        }
        .title-ribbon {
            background-color: #007bff;
            color: #fff;
            padding: 10px;
            border-radius: 5px 5px 0 0;
            font-size: 20px;
            font-weight: bold;
            text-align: center;
        }
        .reply-section {
            margin: 20px 0;
        }
        .reply-section p {
            margin: 10px 0;
        }
        .content-html {
            background-color: #f9f9f9;
            padding: 10px;
            border: 1px solid #ddd;
            border-radius: 5px;
            word-wrap: break-word;
        }
        .original-message {
            border-top: 1px solid #ddd;
            padding-top: 20px;
        }
        .separator {
            margin: 20px 0;
            border-top: 2px dashed #ddd;
        }
    </style>
</head>
<body>
    <div class="email-container">
        <div class="title-ribbon">
            Email Reply
        </div>

        <#if ftlDataMap.replyMessages?size > 0>
            <#list ftlDataMap.replyMessages as reply>
                <#if reply.replyMessage?size > 0>
                    <#list reply.replyMessage?reverse as item>
                        <#if item.type == "AI Reply">
                            <p><strong>AI Reply:</strong> ${item.value}</p>
                        <#else>
                            <div class="content-html">${item.value?html}</div>
                        </#if>
                    </#list>
                <#else>
                    <p>No replies found.</p>
                </#if>
            </#list>
        <#else>
            <p>No reply messages found.</p>
        </#if>

        <div class="separator"></div>

        <div class="original-message">
            <p><strong>-----Original Message-----</strong></p>
            <#if ftlDataMap.replyMessages?size > 0>
                <#list ftlDataMap.replyMessages[0].originalMessage as item>
                    <p><strong>${item.label}</strong> ${item.value}</p>
                </#list>
            <#else>
                <p>No original message content found.</p>
            </#if>
        </div>
    </div>
</body>
</html>

Java处理器代码

public class CamelIMAPProcessor implements Processor {

    private static final Logger LOG = LoggerFactory.getLogger(CamelIMAPProcessor.class);

    @Override
    public void process(Exchange exchange) throws Exception {
        
        Map<String, Object> ftlDataMap = new LinkedHashMap<>();
        List<ValueExampleObject> originalMessage = new ArrayList<>();
        Map<String, Object> reply = new LinkedHashMap<>();
        List<Object> previousResp = new ArrayList<>();
        List<ValueExampleObject> chatBotReply = new ArrayList<>();

        String backendResponse = exchange.getIn().getBody(String.class);
        String originalEmailBody = (String) exchange.getProperty("originalEmailBody");
        String user_message = "";
        String fromAddress = exchange.getProperty("From", String.class);
        String toAddress = exchange.getIn().getHeader("To", String.class);
        String subject = exchange.getIn().getHeader("Subject", String.class);
        String timeStamp = exchange.getIn().getHeader("Date", String.class);

        ObjectMapper objectMapper = new ObjectMapper();
        JsonNode rootNode = objectMapper.readTree(backendResponse);
        JsonNode groupInfo = rootNode.path("group_info");
        JsonNode conversationObjects = groupInfo.path("conversation_objects");

        // Extract "chat-text" and "html-embed" objects
        for (JsonNode objectNode : conversationObjects) {
            String objectType = objectNode.path("object_type").asText();
            JsonNode objectContent = objectNode.path("object_content");

            if ("chat-text".equals(objectType)) {
                LOG.info("Inside chat-text....");
                String myBackendResponseResponse = objectContent.path("response").path("textData").asText();
                user_message = objectContent.path("query").asText();
                chatBotReply.add(new ValueExampleObject("AI Reply", myBackendResponseResponse));
            }

            if ("html-embed".equals(objectType)) {
                LOG.info("Inside html-embed-response....");
                String htmlContent = objectContent.path("content").asText();
                chatBotReply.add(new ValueExampleObject("Embedded Content", htmlContent));
            }
        }
        reply.put("replyMessage", chatBotReply);
        originalMessage.add(new ValueExampleObject("-----Original Message-----", ""));
        originalMessage.add(new ValueExampleObject("From: ", fromAddress));
        originalMessage.add(new ValueExampleObject("Sent: ", timeStamp != null ? timeStamp : ""));
        originalMessage.add(new ValueExampleObject("To: ", toAddress));
        originalMessage.add(new ValueExampleObject("Subject: ", subject != null ? subject : ""));
        originalMessage.add(new ValueExampleObject("User Query:", user_message));
        originalMessage.add(new ValueExampleObject("", ""));
        reply.put("originalMessage", originalMessage);
        if (!reply.isEmpty()) {
            previousResp.add(reply);
            ftlDataMap.put("replyMessages", previousResp);
        }
        String output = createHtmlFromFreeMarkerTemplate(ftlDataMap);
        exchange.getIn().setBody(output);
    }

    private String createHtmlFromFreeMarkerTemplate(Map<String, Object> obj) {
        try {
            Configuration cfg = new Configuration(Configuration.VERSION_2_3_28);
            cfg.setClassForTemplateLoading(CamelIMAPProcessor.class, "/templates");
            cfg.setDefaultEncoding("UTF-8");
            cfg.setLocale(Locale.US);
            cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);

            Template template = cfg.getTemplate("MyFtl.ftl");
            StringWriter writer = new StringWriter();
            template.process(obj, writer);

            return writer.toString();
        } catch (Exception e) {
            // handle exc
        }
        return null;
    }
}

问题原因

模板中引用了ftlDataMap变量,但Java代码传递给模板的是直接包含replyMessages键的Map对象,并没有将这个Map包装在名为ftlDataMap的键下。FreeMarker找不到该变量,因此抛出空引用异常。

修复方案

两种方案任选其一即可:

方案1:修改模板,直接访问根级变量

将模板中所有ftlDataMap的引用删除,直接使用replyMessages,因为Java代码传递的Map本身就包含这个键。修改后的关键代码如下:

<#if replyMessages?size > 0>
    <#list replyMessages as reply>
        <#if reply.replyMessage?size > 0>
            <#list reply.replyMessage?reverse as item>
                <#if item.type == "AI Reply">
                    <p><strong>AI Reply:</strong> ${item.value}</p>
                <#else>
                    <div class="content-html">${item.value?html}</div>
                </#if>
            </#list>
        <#else>
            <p>No replies found.</p>
        </#if>
    </#list>
<#else>
    <p>No reply messages found.</p>
</#if>

<div class="original-message">
    <p><strong>-----Original Message-----</strong></p>
    <#if replyMessages?size > 0>
        <#list replyMessages[0].originalMessage as item>
            <p><strong>${item.label}</strong> ${item.value}</p>
        </#list>
    <#else>
        <p>No original message content found.</p>
    </#if>
</div>

方案2:修改Java代码,包装数据结构

在调用模板渲染前,将现有的ftlDataMap放入一个新Map中,键设为ftlDataMap,让模板能找到对应变量:

// 替换原调用createHtmlFromFreeMarkerTemplate的代码
Map<String, Object> templateData = new HashMap<>();
templateData.put("ftlDataMap", ftlDataMap);
String output = createHtmlFromFreeMarkerTemplate(templateData);

额外优化建议

  • 模板中添加空值保护:使用replyMessages![]替代直接引用,避免replyMessages为null时触发异常,示例:
<#if (replyMessages![])?size > 0>
  • 处理reply为空的场景:当前代码仅在reply不为空时才添加replyMessages,若reply为空,模板仍会触发空引用。建议不管reply是否为空,都初始化replyMessages为空列表:
// 去掉原有的if判断,直接添加
previousResp.add(reply);
ftlDataMap.put("replyMessages", previousResp);

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.19 11:30:55