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

如何让Freemarker ObjectWrapper访问模板设置?附本地化场景用例

嘿,这个需求确实很贴合实际场景——让系统管理员不用写一堆重复的get()调用,直接把LanguageStringMap当成普通字符串用,体验流畅多了!我来给你拆解具体的实现步骤和代码示例:

核心思路:自定义ObjectWrapper + 模板上下文感知的TemplateModel

Freemarker的ObjectWrapper负责把Java对象转成模板能识别的TemplateModel。我们的目标是:让LanguageStringMap自动适配当前模板上下文里的Language参数,直接输出对应语言的字符串。

关键在于,我们需要一个能访问当前模板环境(Environment)的TemplateScalarModel实现,它会从环境中取预设的Language变量,再调用LanguageStringMap.get()返回对应值;然后通过自定义ObjectWrapper,把所有LanguageStringMap实例都转换成这个自定义模型。

1. 实现上下文感知的TemplateScalarModel

写一个类,既实现TemplateScalarModel(让模板把它当成字符串),又能从当前模板环境里拿到Language参数:

import freemarker.core.Environment;
import freemarker.template.*;

public class LocalizedLanguageStringModel implements TemplateScalarModel, AdapterTemplateModel {
    private final LanguageStringMap languageStringMap;

    public LocalizedLanguageStringModel(LanguageStringMap languageStringMap) {
        this.languageStringMap = languageStringMap;
    }

    @Override
    public String getAsString() throws TemplateModelException {
        // 从当前模板环境中获取预先设置的Language变量
        Environment env = Environment.getCurrentEnvironment();
        TemplateModel langModel = env.getVariable("currentLanguage");
        
        if (!(langModel instanceof Language)) {
            throw new TemplateModelException("模板上下文缺少有效的currentLanguage变量");
        }
        
        Language currentLang = (Language) langModel;
        String localizedValue = languageStringMap.get(currentLang);
        
        // 处理找不到对应语言的情况,这里可以根据业务调整(比如返回默认语言、空串或提示)
        if (localizedValue == null) {
            localizedValue = languageStringMap.get(Language.ENGLISH); // 假设你有默认的Language枚举值
        }
        
        return localizedValue;
    }

    @Override
    public Object getAdaptedObject(Class<?> hint) throws TemplateModelException {
        return languageStringMap; // 支持反向获取原始Java对象,可选但实用
    }
}

2. 自定义ObjectWrapper拦截LanguageStringMap

继承默认的DefaultObjectWrapper,重写wrap方法,遇到LanguageStringMap就返回上面的自定义模型:

import freemarker.template.*;

public class CustomObjectWrapper extends DefaultObjectWrapper {
    public CustomObjectWrapper(Version incompatibleImprovements) {
        super(incompatibleImprovements);
    }

    @Override
    public TemplateModel wrap(Object obj) throws TemplateModelException {
        if (obj instanceof LanguageStringMap) {
            return new LocalizedLanguageStringModel((LanguageStringMap) obj);
        }
        // 其他类型交给默认Wrapper处理,保证原有功能不受影响
        return super.wrap(obj);
    }
}

3. 在Spring Boot中配置Freemarker

把自定义的ObjectWrapper配置到Spring的Freemarker环境里,替换默认的Wrapper:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer;
import freemarker.template.Configuration;

@Configuration
public class FreemarkerConfig {

    @Bean
    public FreeMarkerConfigurer freeMarkerConfigurer() {
        FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
        // 如果你的模板是从数据库加载的,这里需要自定义TemplateLoader,比如实现DatabaseTemplateLoader
        // configurer.setTemplateLoader(new DatabaseTemplateLoader(yourTemplateRepository));
        
        configurer.setConfigurationCustomizer(freemarkerConfig -> {
            // 设置Freemarker版本,和你的依赖版本保持一致(比如2.3.32)
            freemarkerConfig.setIncompatibleImprovements(Configuration.VERSION_2_3_32);
            // 启用自定义ObjectWrapper
            freemarkerConfig.setObjectWrapper(new CustomObjectWrapper(Configuration.VERSION_2_3_32));
        });
        
        return configurer;
    }
}

4. 在Controller中传递当前Language到模板上下文

处理请求时,把从浏览器Locale转换来的Language对象放到模板模型里,变量名要和LocalizedLanguageStringModel中获取的currentLanguage一致:

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import java.util.Locale;

@RestController
@RequestMapping("/review")
public class ReviewController {

    @GetMapping("/user-response")
    public ModelAndView getUserResponse(HttpServletRequest request) {
        ModelAndView mav = new ModelAndView("user-review-template"); // 你的模板名称
        
        // 从请求Locale转换为自定义的Language对象
        Locale requestLocale = request.getLocale();
        Language currentLanguage = convertLocaleToLanguage(requestLocale);
        
        // 把Language放到模板上下文
        mav.addObject("currentLanguage", currentLanguage);
        
        // 从数据库获取受访者的LanguageStringMap数据
        LanguageStringMap userResponse = fetchUserResponseFromDB();
        mav.addObject("userResponse", userResponse);
        
        // 如果是列表的情况,直接传List<LanguageStringMap>即可,模板遍历会自动处理每个元素
        // List<LanguageStringMap> userResponses = fetchUserResponsesFromDB();
        // mav.addObject("userResponses", userResponses);
        
        return mav;
    }
    
    // 实现Locale到Language的转换逻辑,根据你的Language枚举调整
    private Language convertLocaleToLanguage(Locale locale) {
        String langCode = locale.getLanguage();
        // 比如Language枚举有EN、ZH等值,对应locale的"en"、"zh"
        return Language.valueOf(langCode.toUpperCase());
    }
    
    // 模拟从数据库获取数据
    private LanguageStringMap fetchUserResponseFromDB() {
        // 替换成实际的数据库查询逻辑
        return new LanguageStringMap(...);
    }
}

5. 模板中的简洁用法

现在系统管理员写模板时,直接用变量名即可,完全不用写get()调用:

<div class="review-content">
    <h2>受访者回答</h2>
    <p>${userResponse}</p>
    
    <!-- 如果是列表的情况 -->
    <!-- <#list userResponses as response>
        <p>${response}</p>
    </#list> -->
</div>

额外注意事项

  • 空值容错:一定要处理languageStringMap.get(currentLang)返回null的情况,避免模板抛出异常,比如返回默认语言或友好提示。
  • 变量名一致性:Controller中设置的模板变量名(currentLanguage)必须和LocalizedLanguageStringModel里取的变量名完全一致。
  • 模板加载:如果你的模板存在数据库中,需要自定义TemplateLoader实现从数据库读取模板内容,这个Spring Boot也支持配置。

你之前考虑的“结合数据库资源包”确实不是最优解,资源包更适合静态的系统文本,而你的场景是动态的用户响应,用自定义ObjectWrapper的方式更直接灵活,也完全符合Freemarker的扩展机制。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.14 08:07:57