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

如何定义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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.15 04:44:40