spring boot + thymeleaf 3 国际化

** 原创文章,请勿转载 **

在给 spring boot 1.5.6 + thymeleaf 3 进行国际化时,踩了一个坑(其实不止一个)。 现象:


 

看到了吧, 就是取值的 key, 后面被加了 _en_US 或 _zh_CN, 以及前后的问号。

 

先看下代码,首先两个资源文件:

messages_en_US.properties

page.content=this is a test string.

message_zh_CN.properties, 在 eclipse 里打开的,内容是: 这是一个测试字符串

page.content=\u8FD9\u662F\u4E00\u4E2A\u6D4B\u5B57\u7B26\u4E32\u3002

 

i18n.html:

<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-4.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
    <h1>spring boot, thymeleaf 3 国际化</h1>
       <hr>
    <a href="/i18n?lang=en_US">English</a> | <a href="/i18n?lang=zh_CN">中文</a>
<span style="color: rgba(0, 0, 255, 1)">&lt;</span><span style="color: rgba(128, 0, 0, 1)">br</span><span style="color: rgba(0, 0, 255, 1)">&gt;</span>
<span style="color: rgba(0, 0, 255, 1)">&lt;</span><span style="color: rgba(128, 0, 0, 1)">br</span><span style="color: rgba(0, 0, 255, 1)">&gt;</span>
<span style="color: rgba(0, 0, 255, 1)">&lt;</span><span style="color: rgba(128, 0, 0, 1)">br</span><span style="color: rgba(0, 0, 255, 1)">&gt;</span>
<span style="color: rgba(0, 0, 255, 1)">&lt;</span><span style="color: rgba(128, 0, 0, 1)">br</span><span style="color: rgba(0, 0, 255, 1)">&gt;</span><span style="color: rgba(0, 0, 0, 1)">
from thymeleaf engine:      </span><span style="color: rgba(0, 0, 255, 1)">&lt;</span><span style="color: rgba(128, 0, 0, 1)">span </span><span style="color: rgba(255, 0, 0, 1)">th:text</span><span style="color: rgba(0, 0, 255, 1)">="#{page.content}"</span><span style="color: rgba(0, 0, 255, 1)">&gt;&lt;/</span><span style="color: rgba(128, 0, 0, 1)">span</span><span style="color: rgba(0, 0, 255, 1)">&gt;</span> 
<span style="color: rgba(0, 0, 255, 1)">&lt;</span><span style="color: rgba(128, 0, 0, 1)">br</span><span style="color: rgba(0, 0, 255, 1)">&gt;</span>
<span style="color: rgba(0, 0, 255, 1)">&lt;</span><span style="color: rgba(128, 0, 0, 1)">br</span><span style="color: rgba(0, 0, 255, 1)">&gt;</span><span style="color: rgba(0, 0, 0, 1)">
from controller model:      </span><span style="color: rgba(0, 0, 255, 1)">&lt;</span><span style="color: rgba(128, 0, 0, 1)">span </span><span style="color: rgba(255, 0, 0, 1)">th:text</span><span style="color: rgba(0, 0, 255, 1)">="${content}"</span><span style="color: rgba(0, 0, 255, 1)">&gt;&lt;/</span><span style="color: rgba(128, 0, 0, 1)">span</span><span style="color: rgba(0, 0, 255, 1)">&gt;</span> 

</body>
</html>

其中, #{page.content} 来源于 thymeleaf 3,  ${content} 则由 controller 的 Model 返回,代码是 hardcode, 返回的都是中文

 

controller:

@Controller
public class I18nController {
    @Autowired
    private MessageSource messageSource;
@GetMapping(</span>"/i18n"<span style="color: rgba(0, 0, 0, 1)">)
</span><span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> String i18n(Model model) {
    String message </span>= messageSource.getMessage("page.content", <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">, Locale.SIMPLIFIED_CHINESE);
    System.out.println(</span>"message=" +<span style="color: rgba(0, 0, 0, 1)"> message);
    model.addAttribute(</span>"content"<span style="color: rgba(0, 0, 0, 1)">, message);
    </span><span style="color: rgba(0, 0, 255, 1)">return</span> "/i18n"<span style="color: rgba(0, 0, 0, 1)">;
}

}

 

从现象看, ${content} 这个是没有问题的, 也就是 controller 里 messageSource.getMessage() 是正常的,再也就是说,资源文件是成功加载的,可见,问题出在 thymeleaf 3.

再看下 thymeleaf 3 的配置:

@Configuration
public class ThymeleafConfig implements ApplicationContextAware {
    private static final String UTF8 = "UTF-8";
    private ApplicationContext applicationContext;
@Override
</span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">void</span> setApplicationContext(ApplicationContext applicationContext) <span style="color: rgba(0, 0, 255, 1)">throws</span><span style="color: rgba(0, 0, 0, 1)"> BeansException {
    </span><span style="color: rgba(0, 0, 255, 1)">this</span>.applicationContext =<span style="color: rgba(0, 0, 0, 1)"> applicationContext;
}


</span><span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> SpringResourceTemplateResolver htmlTemplateResolver() {
    SpringResourceTemplateResolver templateResolver </span>= <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> SpringResourceTemplateResolver();
    templateResolver.setApplicationContext(</span><span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.applicationContext);
    templateResolver.setPrefix(</span>"classpath:/templates/"<span style="color: rgba(0, 0, 0, 1)">);
    templateResolver.setSuffix(</span>".html"<span style="color: rgba(0, 0, 0, 1)">);
    templateResolver.setTemplateMode(TemplateMode.HTML);
    templateResolver.setCacheable(</span><span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">);
    templateResolver.setCharacterEncoding(</span>"UTF-8"<span style="color: rgba(0, 0, 0, 1)">);
    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> templateResolver;
}

@Autowired
</span><span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> MessageSource messageSource;

</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 如果显示 ??x_zh_CN??, 缺少spring-context-support</span>
<span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> SpringTemplateEngine templateEngine(SpringResourceTemplateResolver templateResolver) {
    SpringTemplateEngine templateEngine </span>= <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> SpringTemplateEngine();
    templateEngine.setTemplateResolver(templateResolver);
    templateEngine.setEnableSpringELCompiler(</span><span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">); 
    
    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> templateEngine;
}

@Bean
</span><span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> ViewResolver htmlViewResolver(SpringTemplateEngine templateEngine) {
    ThymeleafViewResolver resolver </span>= <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> ThymeleafViewResolver();
    resolver.setApplicationContext(applicationContext);
    resolver.setTemplateEngine(templateEngine(htmlTemplateResolver()));
    resolver.setCharacterEncoding(</span>"UTF-8"<span style="color: rgba(0, 0, 0, 1)">);
    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> resolver;
}

}

** 临时补一句,国际化要 spring-context-support 包的支持。

web 的配置:

@Configuration
public class MvcConfig extends WebMvcConfigurerAdapter {
@Bean
</span><span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> LocaleResolver localeResolver() {
    SessionLocaleResolver slr </span>= <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> SessionLocaleResolver();
    </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 默认语言</span>

slr.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
return slr;
}

@Bean
</span><span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> LocaleChangeInterceptor localeChangeInterceptor() {
    LocaleChangeInterceptor lci </span>= <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> LocaleChangeInterceptor();
    </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 参数名</span>
    lci.setParamName("lang"<span style="color: rgba(0, 0, 0, 1)">);
    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> lci;
}

@Override
</span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(localeChangeInterceptor());
}

// 这个 MessageSource 可有可无,spring boot 默认是有一个的。
// 如果没有自定义 messageSource, 要有一个 messages.properties 文件。
// 如果有这个定义, 就不需要 messages.properites
// @Bean
// public ResourceBundleMessageSource messageSource() {
// ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
// messageSource.setBasename("messages");
// return messageSource;
// }
}

 

查找原因, 在 thymeleaf.org 上看到这样一段:(在线文档 3.1: Using th:text and externalizing text)

thymeleaf 叫外部文本,无论是在外部的文件里,或者是在数据库,当然国际化使用的是外部文件。

从这段信息看, thymeleaf 使用 IMessageResolver 接口来加载外部文本的,而且有一个标准的实现:

org.thymeleaf.messageresolver.StandardMessageResolver,所以加载.properties 文件应该是和这些信息相关。

但 thymeleaf-spring4 里是怎么用的呢,看下代码吧:

org.thymeleaf.spring4.messageresolver.SpringMessageResolver:  

    private final StandardMessageResolver standardMessageResolver;
    private MessageSource messageSource;
</span><span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> SpringMessageResolver() {
    </span><span style="color: rgba(0, 0, 255, 1)">super</span><span style="color: rgba(0, 0, 0, 1)">();
    </span><span style="color: rgba(0, 0, 255, 1)">this</span>.standardMessageResolver = <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> StandardMessageResolver();
}</span></pre>

它就是使用 org.thymeleaf.messageresolver.StandardMessageResolver, 这样看来,我只要在模板引擎的设置里加上这个 IMessageResolver 的实现即可,新的代码是这样:

private SpringTemplateEngine templateEngine(SpringResourceTemplateResolver templateResolver) {
        SpringTemplateEngine  templateEngine  = new SpringTemplateEngine();
        SpringMessageResolver messageResolver = new SpringMessageResolver();  //
        messageResolver.setMessageSource(messageSource);                      // 加入这三行,即为解决方案
        templateEngine.setMessageResolver(messageResolver);                   //
        templateEngine.setTemplateResolver(templateResolver);
        templateEngine.setEnableSpringELCompiler(false); 
        return templateEngine;
}

 

最终呈现: