如何让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

