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

