如何定义Freemarker自定义指令的渲染顺序?单阶段实现反转需求
实现Freemarker自定义指令的单阶段反转渲染顺序
我刚好做过类似的需求,要解决顶部指令依赖底部指令的问题,同时不想用两阶段渲染的话,可以通过自定义容器指令+上下文缓存的方式实现单阶段反转执行。核心思路很简单:用一个容器指令包裹需要调整顺序的子指令,在容器内部先执行那些"提供数据"的底部指令,把数据存入Freemarker的请求上下文缓存,再执行顶部的"依赖数据"指令,这样就能保证依赖指令执行时,需要的数据已经准备完毕。
具体实现步骤
1. 定义共享上下文缓存工具
我们可以利用Freemarker的Environment对象存储共享数据,它是每个模板渲染请求的专属上下文,天然适合存放临时共享数据:
public class DirectiveDataHolder { private static final String DATA_KEY = "reverse_render_data"; @SuppressWarnings("unchecked") public static Map<String, Object> getDataMap(Environment env) { Map<String, Object> dataMap = (Map<String, Object>) env.getVariable(DATA_KEY); if (dataMap == null) { dataMap = new HashMap<>(); env.setVariable(DATA_KEY, dataMap); } return dataMap; } public static void putData(Environment env, String key, Object value) { getDataMap(env).put(key, value); } public static Object getData(Environment env, String key) { return getDataMap(env).get(key); } }
2. 实现"提供数据"的自定义指令
这个指令负责把需要共享的数据存入上下文缓存:
public class ProvideDataDirective implements TemplateDirectiveModel { @Override public void execute(Environment env, Map params, TemplateModel[] loopVars, TemplateDirectiveBody body) throws TemplateException, IOException { // 从指令参数中获取数据的键名和值 String key = params.get("key").toString(); Object value = params.get("value"); // 将数据存入上下文缓存 DirectiveDataHolder.putData(env, key, value); // 如果指令包含body内容,也可以正常渲染(按需调整) if (body != null) { body.render(env.getOut()); } } }
3. 实现"依赖数据"的自定义指令
这个指令从上下文缓存中取出数据,供自身的模板内容使用:
public class DependsDataDirective implements TemplateDirectiveModel { @Override public void execute(Environment env, Map params, TemplateModel[] loopVars, TemplateDirectiveBody body) throws TemplateException, IOException { String key = params.get("key").toString(); Object data = DirectiveDataHolder.getData(env, key); if (data == null) { throw new TemplateException("依赖的数据不存在:" + key, env); } // 将数据放入模板上下文,供指令body使用 env.setVariable("data", ObjectWrapper.DEFAULT_WRAPPER.wrap(data)); if (body != null) { body.render(env.getOut()); } } }
4. 实现核心的反转渲染容器指令
这个容器指令会遍历所有子节点,先执行所有provideData指令(不管它们在模板中的位置),再执行dependsData指令,实现逻辑上的反转顺序:
public class ReverseRenderDirective implements TemplateDirectiveModel { @Override public void execute(Environment env, Map params, TemplateModel[] loopVars, TemplateDirectiveBody body) throws TemplateException, IOException { if (body == null) { return; } // 遍历容器内的子节点,分类存放提供数据和依赖数据的指令 TemplateElement bodyElement = ((TemplateDirectiveBodyModel) body).getElement(); List<TemplateElement> provideElements = new ArrayList<>(); List<TemplateElement> dependsElements = new ArrayList<>(); traverseElements(bodyElement, provideElements, dependsElements); // 先执行所有提供数据的指令,把数据存入缓存 Writer originalOut = env.getOut(); for (TemplateElement elem : provideElements) { elem.accept(new Visitor(env, originalOut)); } // 再执行所有依赖数据的指令,使用缓存中的数据 for (TemplateElement elem : dependsElements) { elem.accept(new Visitor(env, originalOut)); } } // 递归遍历模板节点,分类指令 private void traverseElements(TemplateElement elem, List<TemplateElement> provideElements, List<TemplateElement> dependsElements) { if (elem instanceof DirectiveCall) { DirectiveCall call = (DirectiveCall) elem; if ("provideData".equals(call.getDirectiveName())) { provideElements.add(elem); return; } else if ("dependsData".equals(call.getDirectiveName())) { dependsElements.add(elem); return; } } // 递归处理子节点 for (int i = 0; i < elem.getChildCount(); i++) { traverseElements(elem.getChild(i), provideElements, dependsElements); } } // 自定义Visitor执行模板节点 private static class Visitor extends AbstractTemplateVisitor { private final Environment env; private final Writer out; public Visitor(Environment env, Writer out) { this.env = env; this.out = out; } @Override public void visit(TemplateElement elem) throws TemplateException, IOException { elem.render(env, out); } } }
5. 在模板中使用
把上述指令注册到Freemarker配置后,就可以这样编写模板:
<@reverseRender> <!-- 顶部的依赖指令,原本会先执行但无数据,现在会在数据准备后执行 --> <@dependsData key="userInfo"> 当前用户名:${data.username} </@dependsData> <!-- 底部的提供数据指令,现在会优先执行,存入数据到缓存 --> <@provideData key="userInfo" value={"username": "Farhan"} /> </@reverseRender>
关键说明
- 这是纯单阶段渲染:所有指令的执行都在
reverseRender容器的一次execute调用中完成,没有先渲染整个页面再二次处理的过程。 - 如果你的需求是反转所有子指令的渲染顺序(不区分类型),可以去掉分类逻辑,直接收集所有子节点后反转列表再执行。
- 注意
DirectiveCall的类型判断可能需要根据Freemarker版本调整,部分版本可能需要用UserDirectiveCall。
内容的提问来源于stack exchange,提问作者Farhan Ali
相关产品推荐
相关产品推荐

