Spring resource bundle多语言,单引号format异常

source code

前言

十一假期被通知出现大bug,然后发现是多语言翻译问题。法语中有很多单引号,单引号在format的时候出现无法匹配问题。这个问题是由spring resource bundle 并调用MessageFormat引起的,根本原因是MessageFormat会转义单引号。

创建一个简单的多语言demo,重现异常

1.配置

    @Bean
public ResourceBundleMessageSource messageSource(){
ResourceBundleMessageSource source = new ResourceBundleMessageSource();
source.setBasenames("msg");
source.setDefaultEncoding("UTF-8");
source.setFallbackToSystemLocale(false); return source;
}

这里没有指定localeResolver, 默认会使用AcceptHeaderLocaleResolver,也就是说从request的header中获取Accept-Language来解析语言。

ResourceBundleMessageSource是多语言翻译的逻辑处理。source.setBasenames("msg")绑定一个多语言的集合。这里我创建一个叫做msg的集合:

.

2.创建多语言方法

在main下右键创建一个文件夹i18n,然后将其设置为resources类型。在gradle中,可以在build.gradle里添加:

sourceSets {
main {
resources {
srcDir 'src/main/i18n'
}
}
}

然后-New-Resource Bundle. 起一个集合的名字,比如msg, 添加需要的语言包。

在里面添加内容

#msg.properties
user.name=default for en_US, I'm {0}
user.age='18' #msg_en_US.properties
user.name=test, the user's name is {0}. #msg_fr_FR.properties
user.name=This is french, I'm {0} #msg_zh_CN.properties
user.name=测试 ,用户名是 '{0}'

3.编写一个controller测试

	@Autowired
private MessageSource messageSource;
@ResponseBody
@RequestMapping(value = "/i18n/{name}", method = RequestMethod.GET)
public Map resource(Locale locale,
@PathVariable("name") String name){
Map map = new HashMap(); String[] arr = {name};
String message = messageSource.getMessage("user.name", arr, locale);
String age = messageSource.getMessage("user.age", null, locale);
map.put("username", message);
map.put("age", age); return map;
}
  • java中通过MessageSource来获取配置语言包中内容
  • 在controller的参数中添加Locale会自动注入LocaleResolver解析后的Locale, 当前是采用默认的AcceptHeaderLocaleResolver。当然也可以自己添加locale拦截器来自定义locale, 这个后面再去设置。
  • 本例中获取name和age。其中name需要插入参数,而age不需要参数,原样输出即可。

如果在jsp中可以使用spring标签:

<%@ taglib uri="http://www.springframework.org/tags" prefix="spring" %>
<spring:message code="user.name" arguments="Ryan"/>
<spring:message code="user.age"/>

4.访问

通过postman来访问get请求:

  • 可以看到age的单引号会原样输出,但name的单引号没了,不仅如此,参数也并没有传入
  • 这是因为messageSource在getMessage的时候采用了两种策略,一种是原样输出,一种是采用MessageFormat来处理参数。
  • 因此,主要原因就是MessageFormat的问题了。

5.测试MessageFormat

@Test
public void testQuote() throws Exception{
String message = "I'm {0}.";
String ryan = MessageFormat.format(message, "Ryan");
System.out.println(ryan);
Assert.assertEquals("Im {0}.", ryan); message = "I''m {0}.";
ryan = MessageFormat.format(message, "Ryan");
System.out.println(ryan);
Assert.assertEquals("I'm Ryan.", ryan);
}

通过测试用例可以发现,MessageFormat会转义(escape)单引号(quote)。因此,如果想要输出一个单引号就需要针对的用两个单引号来替换。

所以,解决上述问题的关键就是在语言包中涉及单引号的地方都做一下转义,即两个单引号。然而,这个步骤会比较繁琐,而且会使得语言包的内容和显示的内容不一致。因此,最好可以通过一个工具来将单引号自动转义。

6.设置单引号转义

既然已经知道问题原因所在了,那么只要在Format之前做一下转义就可以了。

追踪getMessage方法到AbstractMessageSource可以发现有参数和无参数的不同处理:

Object[] argsToUse = args;
if(!this.isAlwaysUseMessageFormat() && ObjectUtils.isEmpty(args)) {
String commonMessages1 = this.resolveCodeWithoutArguments(code, locale);
if(commonMessages1 != null) {
return commonMessages1;
}
} else {
argsToUse = this.resolveArguments(args, locale);
MessageFormat commonMessages = this.resolveCode(code, locale);
if(commonMessages != null) {
synchronized(commonMessages) {
return commonMessages.format(argsToUse);
}
}
}

那么,按道理,我们只要处理有参数的情况下就好了。接下来就应该是重写resolveCode方法,将取出来的结果中的单引号替换。

要重写的就是ResourceBundleMessageSource类, 但是发现这些方法都是私有的。这是因为我当前spring的版本是4.1.1。 意外升级成4.3.2之后发现这些方法已经变成protected。

接着发现由于私有成员变量能重写的是getStringOrNull方法,但重写后也会影响无参数的获取。所以,设置ResourceBundleMessageSource

source.setAlwaysUseMessageFormat(true);

将所有的语言包获取都走传参路线,即都会经过MessageFormat处理,即单引号都要转义。如此,便可以重写getStringOrNull了。

创建ResourceFormat

public class ResourceFormat extends ResourceBundleMessageSource {

    @Override
protected String getStringOrNull(ResourceBundle bundle, String key) {
if(bundle.containsKey(key)) {
try {
String val = bundle.getString(key);
return val.replaceAll("'","''");
} catch (MissingResourceException var4) {
;
}
} return null;
} }

然后修改配置类:

@Bean
public ResourceBundleMessageSource messageSource(){
ResourceBundleMessageSource source = new ResourceFormat();
source.setBasenames("msg");
source.setDefaultEncoding("UTF-8");
source.setFallbackToSystemLocale(false);
source.setAlwaysUseMessageFormat(true); return source;
}

这样,再次访问:



这样就正常了,单引号可以显示,并且参数可以传进去。

后记

关于locale resolver有多个实现类,通常使用SessionLocaleResolver, 这时候需要添加一个拦截器,来将locale注入进去。注入locale的方法有很多,比如header,比如url直接传参,比如cookie。通过各种手段获取浏览器的语言之后,设置到locale里就可以了。

spring自带了一个LocaleChangeInterceptor,可以将参数locale拦截并注入。

因此,只要自己在拦截器里设置:

Locale langLocale = Locale.forLanguageTag(localeString);
LocaleResolver localeResolver = RequestContextUtils.getLocaleResolver(request);
localeResolver.setLocale(request, response, langLocale);
request.setAttribute("javax.servlet.jsp.jstl.fmt.locale", langLocale);
  • localeString就是语言代码,比如en-US, zh-CN
参考

Spring resource bundle多语言,单引号format异常的更多相关文章

  1. 浅谈SQL中的单引号

    单引号:对很对计算机语言包括(SQL)是做字符串引用的:这个是大家通常知道的作用:但是对SQL语言来说:还有另外一个作用是作引号的转义 总结下:对oracle(sql)的作用. 做字符串引用:例如'a ...

  2. php 单引号,双引号,反引号区别

    PHP中单引号,双引号,反引号具有不同的含义,最大的几项区别如下: 一.单引号中,任何变量($var).特殊转义字符(如"\t \r \n"等)不会被解析,因此PHP的解析速度更快 ...

  3. python中的单引号,双引号,三引号

    转载自: http://blog.csdn.net/wanghai__/article/details/6285310 先说1双引号与3个双引号的区别,双引号所表示的字符串通常要写成一行 如: s1 ...

  4. Flex编译程序出现 Could not find compiled resource bundle 'SharedResources' for locale 'en_US'.

    Flex编译程序出现 Could not find compiled resource bundle 'SharedResources' for locale 'en_US'. 而且静态类居然为nul ...

  5. [译]JavaScript:将字符串两边的双引号转换成单引号

    原文:http://ariya.ofilabs.com/2012/02/from-double-quotes-to-single-quotes.html 代码的不一致性总是让人发狂,如果每位开发者都能 ...

  6. SQL中插入单引号,新增修改删除

    1.插入单引号如果不转化的话,字符串插入到数据库中错误的,只要在字符串中有单引号的地方在加一个单引号即可.    例如:在数据库插入'井下设备' :    insert into Static_Bel ...

  7. Linux 中的grep命令单引号,不加任何参数以及双引号的作用

    单引号: 可以说是所见即所得:即将单引号内的内容原样输出,或者描述为单引号里面看到的是什么就会输出什么.单引号''是全引用,被单引号括起的内容不管是常量还是变量者不会发生替换. 双引号: 把双引号内的 ...

  8. jackson json转实体 允许特殊字符和转义字符 单引号

    //允许出现特殊字符和转义符 mapper.configure(Feature.ALLOW_UNQUOTED_CONTROL_CHARS, true) ; //允许出现单引号 mapper.confi ...

  9. sql 去除列中内容的单引号

    update  [dbo].[历史数据补全$]  set  bankCardNo=replace(bankCardNo,'''','') 总共4个单引号,外边2个是字符串固有的2个,里边两个就表示是一 ...

随机推荐

  1. 一个免费的、跨平台的、开源音频编辑器Audacity

    Audacity 是一个免费的开源程序,用于编辑音频录制.它可在多个平台(windows/linux)上运行.Audacity 基于 GUI,是一个具有多种选项的强大程序.它支持您录制各种类型的声音. ...

  2. .NET Core中间件的注册和管道的构建(1)---- 注册和构建原理

    .NET Core中间件的注册和管道的构建(1)---- 注册和构建原理 0x00 问题的产生 管道是.NET Core中非常关键的一个概念,很多重要的组件都以中间件的形式存在,包括权限管理.会话管理 ...

  3. 异步编程 In .NET

    概述 在之前写的一篇关于async和await的前世今生的文章之后,大家似乎在async和await提高网站处理能力方面还有一些疑问,博客园本身也做了不少的尝试.今天我们再来回答一下这个问题,同时我们 ...

  4. Centos——安装JDK

    写在前面: Just mark! 创建linux虚拟机的时候经常要安装JDK,配置环境变量,却又经常忘记,这里记录一下. 环境:Centos-6.8-x86_64-minimal JDK :jdk-7 ...

  5. 使用AWS亚马逊云搭建Gmail转发服务(三)

    title: 使用AWS亚马逊云搭建Gmail转发服务(三) author:青南 date: 2015-01-02 15:42:22 categories: [Python] tags: [log,G ...

  6. WPF 微信 MVVM

    公司的同事离职了,接下来的日子可能会忙碌,能完善DEMO的时间也会少了,因此,把做的简易DEMO整体先记录一下,等后续不断的完善. 参考两位大神的日志:WEB版微信协议部分功能分析.[完全开源]微信客 ...

  7. 浅谈Slick(2)- Slick101:第一个动手尝试的项目

    看完Slick官方网站上关于Slick3.1.1技术文档后决定开始动手建一个项目来尝试一下Slick功能的具体使用方法.我把这个过程中的一些了解和想法记录下来和大家一起分享.首先我用IntelliJ- ...

  8. SAP CRM 将组件整合至导航栏中

    到现在,我们已经可以让组件独立地显示.我们只是运行它.让它显示在Web UI中.让我们把组件整合进导航栏,使我们可以在正常登录Web UI时访问它. 步骤一: 为你的UI组件主窗体创建一个内向插件. ...

  9. iOS开发 适配iOS10

    2016年9月7日,苹果发布iOS 10.2016年9月14日,全新的操作系统iOS 10将正式上线. 作为开发者,如何适配iOS10呢? 1.Notification(通知) 自从Notificat ...

  10. Linux下编译安装Vim8.0

    什么是Vim? Vim 是经典的 UNIX 编辑器 Vi 的深度改良版本.它增加了许多功能,包括:多级撤销.格式高亮.命令行历史.在线帮助.拼写检查.文件名补完.块操作.脚本支持,等等.除了字符界面版 ...