Apache FreeMarker递归遍历LinkedHashMap的模板问题及解决
I recently ran into an issue while trying to recursively traverse a nested LinkedHashMap with Apache FreeMarker—where values could be sub-LinkedHashMaps, ArrayLists, or plain Strings. I had a working Java implementation to generate the nested HTML list output, but my initial FreeMarker template kept throwing a null error on obj.value.entrySet(). After some debugging, I fixed the macro and wanted to share the solution.
My Original Problem
I wanted to replicate the output of this Java recursive method in FreeMarker:
private String printLinkedHashMap(LinkedHashMap<?, ?> map, int counter) { if(map == null) { return ""; } StringBuilder sb = new StringBuilder(); Set<?> set = map.entrySet(); sb.append(getSpaces(counter) + "<ul>\n"); for (Object object : set) { if(object instanceof Entry<?, ?>) { Entry<?, ?> entry = (Entry<?, ?>) object; sb.append(getSpaces(counter) + "<li>" + entry.getKey() + " = "); if(entry.getValue() instanceof LinkedHashMap<?, ?>) { sb.append("</li>\n" + printLinkedHashMap((LinkedHashMap<?, ?>) entry.getValue(), counter+1)); } else if(entry.getValue() instanceof ArrayList<?>){ ArrayList<?> listOfValues = (ArrayList<?>) entry.getValue(); sb.append("</li>" + printArrayList(listOfValues, counter+1)); } else { sb.append(entry.getValue().toString() + "</li>\n"); } } } sb.append(getSpaces(counter) + "</ul>\n"); return sb.toString(); } private String printArrayList(ArrayList<?> listOfValues, int counter) { StringBuilder sb = new StringBuilder(); if(listOfValues.size() > 1) { for (int i = 0; i < listOfValues.size(); i++) { sb.append('\n' + getSpaces(counter)); sb.append("<li>" + listOfValues.get(i).toString() + "</li>"); } } else { sb.append(listOfValues.get(0).toString()); } return sb.toString(); }
Expected output (nested HTML list):
<ul> <li>defaultVar = </li> <ul> <li>subdefaultVar = </li> <ul> <li>subsubdefaultVar = defaultValue</li> <li>subsubdefaultVariable = defaultValue2</li> </ul> <li>anothersubdefaultVar = </li> <ul> <li>anothersubsubdefaultVar = anotherdefaultValue</li> <li>anothersubsubdefaultVariable = anotherdefaultValue2</li> </ul> </ul> <li>defaultVariable = Defaultshort</li> </ul>
But my initial template threw this error:
The following has evaluated to null or missing:
==> obj.value.entrySet [in template "template.ftl" at line 55, column 26]Tip: It's the step after the last dot that caused this error, not those before it.
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: #list obj.value.entrySet() as entry [in template "template.ftl" in macro "printLinkedHashMap" at line 55, column 19]
- Reached through: @printLinkedHashMap entry [in template "template.ftl" in macro "printLinkedHashMap" at line 56, column 21]
- Reached through: @printLinkedHashMap entry [in template "template.ftl" in macro "printLinkedHashMap" at line 56, column 21]
- Reached through: @printLinkedHashMap entry [in template "template.ftl" at line 41, column 11]
Root Causes of the Error
- Incorrect Macro Parameter Handling: My initial macro accepted an
objparameter, but didn't properly validate if it was a valid entry before accessingobj.value.entrySet(). - Unchecked entrySet() Access: Not all hash-like objects in FreeMarker support the
entrySet()method, and I didn't add a null check before calling it. - Broken HTML Tag Nesting: The
<li>and<ul>tags were being closed at the wrong times, leading to invalid structure that compounded the error.
Fixed FreeMarker Macro
Here's the corrected macro that works as expected:
<#macro printLinkedHashMap entry> <#if entry.key??> <ul> <#if entry.key?is_string> <li>${entry.key} = </li> </#if> <#if entry.value??> <#if entry.value?is_hash_ex> <#if (entry.value.entrySet())??> <#list entry.value.entrySet() as newEntry> <#if newEntry.key?? && newEntry.value??> <@printLinkedHashMap newEntry/> </#if> </#list> </#if> <#elseif entry.value?is_collection> <ul> <#list entry.value as currentValue> <li>${currentValue}</li> </#list> </ul> <#else> </li>${entry.value} </#if> </#if> </ul> </#if> </#macro>
Key Fixes Explained
- Explicit Entry Parameter: The macro now takes an
entryparameter, ensuring we always work with a key-value pair from the map. - Defensive Null Checks: Added checks for
entry.key??,entry.value??, and(entry.value.entrySet())??to avoid null/missing value errors. - Proper HTML Tag Closure: Fixed the timing of closing
<li>tags to match the Java implementation's structure. - Collection Handling: Correctly iterates over collections (like
ArrayList) and outputs each element in a<li>tag. - Type Validation: Uses
is_hash_exto check for hash structures andis_collectionfor list-like values, ensuring we handle each type correctly.
How to Use It
Call the macro by iterating over your map's entry set:
<#list yourLinkedHashMap.entrySet() as entry> <@printLinkedHashMap entry/> </#list>
This will generate the nested HTML list exactly as the Java method does.
内容的提问来源于stack exchange,提问作者Crazy-Tony

