Spring Boot 国际化功能实现出现 No message found under code 'xx' for locale 'xx'问题处理

Spring Boot 国际化功能实现

Spring Boot 集成了很开源项目的初始化配置,开发者只需做很少的配置或者不需要任何配置就可以实现所需要的功能。

Spring Boot 实现国际化功能步骤

1.在 src/main/resources 下创建 messages.properties,messages_zh.properties,messages_en.properties 文件,
messages.properties 文件中内容如下:
welcome = 欢迎
messages_zh.properties 文件中内容如下:
welcome = 欢迎
messages_en.properties 文件中内容如下:
welcome = welcome

2.继承 HandlerInterceptorAdapter 实现自定义处理器拦截器,在 preHandle 中根据请求中的参数进行语言设置
,可以通过 url、session或cookie进行参数的传递。我这里使用 cookie 的方式。代码如下:

public class LocaleInterceptor extends HandlerInterceptorAdapter {
    // ... 其他代码省略

   @Override
   public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        Cookie[] cookies = request.getCookies();
        Cookie langCookie = null;
        if (ArrayUtils.isNotEmpty(cookies)) {
            for (Cookie cookie : cookies) {
                if (cookie.getName().equals("lang")) {
                    langCookie = cookie;
                    break;
                }
            }
        }
        if (langCookie == null) {
            langCookie = new Cookie("lang", Locale.SIMPLIFIED_CHINESE.getLanguage());
            langCookie.setMaxAge(604800);
            response.addCookie(langCookie);
        }
        
        Locale locale = new Locale(langCookie.getValue());
        LocaleContextHolder.setLocale(locale);
        
        return true;
    }   
}

通过配置将自定义拦截器加入应用中,如果不加入,拦截器无法执行,代码如下:

@Configuration
@ConditionalOnClass(SpringfoxWebMvcConfiguration.class)
public class WebMvcConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LocaleInterceptor()).addPathPatterns("/**");
    }
}

3.控制器测试国际化效果,代码如下:

@RestController
public class MessagesController {
    @Autowired
    private MessageSource messageSource;
    
    @GetMapping("/messages")
    public String messages() {
        Locale locale = LocaleContextHolder.getLocale();
        String welcome = messageSource.getMessage("welcome", null, locale);
        return welcome;
    }
}

4.测试
访问 http://127.0.0.1:8080/messages
结果返回下面结果则说明功能实现成功

欢迎

打开浏览器,手动修改 lang 的属性为 en(我这里主要是为了省事,项目开发还是通过代码实现)


image.png

再次访问 http://127.0.0.1:8080/messages
返回结果

welcome

实现过程中遇到的问题

代码编写完之后,访问 http://127.0.0.1:8080/messages 后台报错如下,为了不占篇幅,这里只截取部分报错信息

org.springframework.context.NoSuchMessageException: No message found under code 'welcome' for locale 'en'.
at org.springframework.context.support.DelegatingMessageSource.getMessage(DelegatingMessageSource.java:76) ~[spring-context-5.0.10.RELEASE.jar:5.0.10.RELEASE]

问题分析

  • messages_zh.properties,messages_en.properties 配置信息设置正确,怎么会提示找不到 welcome 对应的信息?

代码分析

  • 跟踪报错的第一行代码信息如下:

org.springframework.context.support.DelegatingMessageSource.getMessage(DelegatingMessageSource.java:76) ~[spring-context-5.0.10.RELEASE.jar:5.0.10.RELEASE]

很明显该类属于 spring-context-5.0.10.RELEASE.jar ,通过开发工具跟踪进去,找到报错的代码

@Override
    public String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException {
        if (this.parentMessageSource != null) {
            return this.parentMessageSource.getMessage(code, args, locale);
        }
        else {
// 这里抛出异常,说明 this.parentMessageSource 为空
            throw new NoSuchMessageException(code, locale);
        }
    }

在该类的描述为

Empty {@link MessageSource} that delegates all calls to the parent MessageSource.If no parent is available, it simply won't resolve any message.
<p>Used as placeholder by AbstractApplicationContext, if the context doesn't
define its own MessageSource. Not intended for direct use in applications.

意思是 MessageSource 为 null,则所有的调用由父级 MessageSource 执行,如果没有父级 MessageSource 可用,则不会解析任何资源,那么 MessageSource 为什么为空呢?那么 MessageSource 是在什么时候创建的呢?
之前也没怎么看过 Spring Boot 的源代码,但问题出现了总要解决不是,带着对 Spring Boot 的敬畏之心,试着去看 Spring Boot 的源代码。

Spring Boot 的几种包

1.spring-boot
2.spring-boot-autoconfigure
3.spring-boot-starter
4.spring-boot-starter-xx
5.spring-boot-test
通过从官方文档和包结构上了解到 Spring 的相关类大都是通过 spring-boot-autoconfigure 包中代码创建,那么我们通过工具找到 MessageSource 类创建的位置

@Configuration
@ConditionalOnMissingBean(value = MessageSource.class, search = SearchStrategy.CURRENT)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@Conditional(ResourceBundleCondition.class)
@EnableConfigurationProperties
public class MessageSourceAutoConfiguration {

    private static final Resource[] NO_RESOURCES = {};

    @Bean
    @ConfigurationProperties(prefix = "spring.messages")
    public MessageSourceProperties messageSourceProperties() {
        return new MessageSourceProperties();
    }

    @Bean
    public MessageSource messageSource() {
        MessageSourceProperties properties = messageSourceProperties();
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        if (StringUtils.hasText(properties.getBasename())) {
            messageSource.setBasenames(StringUtils.commaDelimitedListToStringArray(
                    StringUtils.trimAllWhitespace(properties.getBasename())));
        }
        if (properties.getEncoding() != null) {
            messageSource.setDefaultEncoding(properties.getEncoding().name());
        }
        messageSource.setFallbackToSystemLocale(properties.isFallbackToSystemLocale());
        Duration cacheDuration = properties.getCacheDuration();
        if (cacheDuration != null) {
            messageSource.setCacheMillis(cacheDuration.toMillis());
        }
        messageSource.setAlwaysUseMessageFormat(properties.isAlwaysUseMessageFormat());
        messageSource.setUseCodeAsDefaultMessage(properties.isUseCodeAsDefaultMessage());
        return messageSource;
    }

    // ... 其它代码省略
}
  • debug 跟踪下面代码
@Bean
public MessageSource messageSource() {
    MessageSourceProperties properties = messageSourceProperties();
    ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
    if (StringUtils.hasText(properties.getBasename())) {
        messageSource.setBasenames(StringUtils.commaDelimitedListToStringArray(
        StringUtils.trimAllWhitespace(properties.getBasename())));
    }
    if (properties.getEncoding() != null) {
        messageSource.setDefaultEncoding(properties.getEncoding().name());
    }
    messageSource.setFallbackToSystemLocale(properties.isFallbackToSystemLocale());
    Duration cacheDuration = properties.getCacheDuration();
    if (cacheDuration != null) {
        messageSource.setCacheMillis(cacheDuration.toMillis());
    }
    messageSource.setAlwaysUseMessageFormat(properties.isAlwaysUseMessageFormat());
    messageSource.setUseCodeAsDefaultMessage(properties.isUseCodeAsDefaultMessage());
    return messageSource;
}

发现根本就没进来

  • 继续分析和查阅相关 Spring Boot 资料知道,@Configuration 注解声明的类会声明多个方法,Spring 容器会在运行时通过这些方法实例化 bean,完成 bean 的相关配置,并返回 bean 实例。

  • 了解@Configuration 后继续查询该类的其它注解,发现 @Conditional(ResourceBundleCondition.class) ,该注解指定了该类在实例化之前,哪些类应该已经加载了。
    查看 ResourceBundleCondition 源码,然后 debug 发现找不到 messages.properties,而我没创建 messages.properties 文件

protected static class ResourceBundleCondition extends SpringBootCondition {

        private static ConcurrentReferenceHashMap<String, ConditionOutcome> cache = new ConcurrentReferenceHashMap<>();

        @Override
        public ConditionOutcome getMatchOutcome(ConditionContext context,
                AnnotatedTypeMetadata metadata) {
            String basename = context.getEnvironment()
                    .getProperty("spring.messages.basename", "messages");
            ConditionOutcome outcome = cache.get(basename);
            if (outcome == null) {
                outcome = getMatchOutcomeForBasename(context, basename);
                cache.put(basename, outcome);
            }
            return outcome;
        }

        private ConditionOutcome getMatchOutcomeForBasename(ConditionContext context,
                String basename) {
            ConditionMessage.Builder message = ConditionMessage
                    .forCondition("ResourceBundle");
            for (String name : StringUtils.commaDelimitedListToStringArray(
                    StringUtils.trimAllWhitespace(basename))) {
                for (Resource resource : getResources(context.getClassLoader(), name)) {
                    if (resource.exists()) {
                        return ConditionOutcome
                                .match(message.found("bundle").items(resource));
                    }
                }
            }
            return ConditionOutcome.noMatch(
                    message.didNotFind("bundle with basename " + basename).atAll());
        }

        private Resource[] getResources(ClassLoader classLoader, String name) {
            String target = name.replace('.', '/');
            try {
                return new PathMatchingResourcePatternResolver(classLoader)
                        .getResources("classpath*:" + target + ".properties");
            }
            catch (Exception ex) {
                return NO_RESOURCES;
            }
        }

    }

问题处理

  • 创建 messages.properties 后,重新测试,问题解决。

总结

1.在实现一个功能时应认真阅读文档,不然开发过程中遇到问题,读源码搞清楚挺花时间。
2.遇到问题,应沉下心来找到好的方式去解决。
3.读源码能让自己对一个功能的实现有更好的理解。

结束语

以前没怎么写过博客,总觉得自己能力不够,书面表达能力也不够,怕写出来的东西描述不清楚,所以本文也力争花心思去写,如有错误或表述不清的地方,还望指正,谢谢。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,734评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,931评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,133评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,532评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,585评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,462评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,262评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,153评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,587评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,792评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,919评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,635评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,237评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,855评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,983评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,048评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,864评论 2 354

推荐阅读更多精彩内容