Java自学者论坛

 找回密码
 立即注册

手机号码,快捷登录

恭喜Java自学者论坛(https://www.javazxz.com)已经为数万Java学习者服务超过8年了!积累会员资料超过10000G+
成为本站VIP会员,下载本站10000G+会员资源,会员资料板块,购买链接:点击进入购买VIP会员

JAVA高级面试进阶训练营视频教程

Java架构师系统进阶VIP课程

分布式高可用全栈开发微服务教程Go语言视频零基础入门到精通Java架构师3期(课件+源码)
Java开发全终端实战租房项目视频教程SpringBoot2.X入门到高级使用教程大数据培训第六期全套视频教程深度学习(CNN RNN GAN)算法原理Java亿级流量电商系统视频教程
互联网架构师视频教程年薪50万Spark2.0从入门到精通年薪50万!人工智能学习路线教程年薪50万大数据入门到精通学习路线年薪50万机器学习入门到精通教程
仿小米商城类app和小程序视频教程深度学习数据分析基础到实战最新黑马javaEE2.1就业课程从 0到JVM实战高手教程MySQL入门到精通教程
查看: 398|回复: 0

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

[复制链接]
  • TA的每日心情
    奋斗
    2024-11-24 15:47
  • 签到天数: 804 天

    [LV.10]以坛为家III

    2053

    主题

    2111

    帖子

    72万

    积分

    管理员

    Rank: 9Rank: 9Rank: 9

    积分
    726782
    发表于 2021-6-29 04:21:32 | 显示全部楼层 |阅读模式

    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
    参考
    哎...今天够累的,签到来了1...
    回复

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    QQ|手机版|小黑屋|Java自学者论坛 ( 声明:本站文章及资料整理自互联网,用于Java自学者交流学习使用,对资料版权不负任何法律责任,若有侵权请及时联系客服屏蔽删除 )

    GMT+8, 2025-2-1 15:38 , Processed in 0.060057 second(s), 29 queries .

    Powered by Discuz! X3.4

    Copyright © 2001-2021, Tencent Cloud.

    快速回复 返回顶部 返回列表