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.读源码能让自己对一个功能的实现有更好的理解。

结束语

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容