通过解读源码理解springboot使用MessageSourceAutoConfiguration自动配置类实现加载国际化资源文件

707 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第10天,点击查看活动详情

一、国际化

之前在springmvc的文章中有介绍国际化的使用,感兴趣的同学可以去看一下:juejin.cn/post/708479…

在springboot项目中实现国际化主要分为以下几步:

  1. 添加国际化资源文件 resource
  2. 配置messageResource 设置国际化资源文件
  3. 需要去解析请求头中的accept-language 或者 解析url参数中?local=
  4. 随意切换本地语言,进行缓存
  5. 通过messageResource 获取国际化信息

按照上面的步骤来逐个处理。

二、springboot国际化实现过程

2-1、在resource中添加国际化资源文件

2-1-1、创建国际化资源文件

在一般项目中,都是采用前后端分离的,因此我们在后端只要负责后端相关信息的国际化即可,拿之前文章中的对用户CRUD操作来进行国际化。

首先在resource中创建i18n文件夹,用于存放我们国际化资源文件。我们做中英文的国际化(如想创建其他语言,可搜一下i18n地区对照语言码),因此在i18n中,我们创建三个资源文件,如下:

image.png

2-1-2、编辑国际化资源文件

目前我这边只有三个国际化资源文件,如果多了编辑起来就比较麻烦了,因此需要给idea安装一个插件: image.png

这样再打开资源文件的时候,就会有两种编辑方式,我们使用resource Bundle进行编辑如下: image.png

2-1-2-1、添加属性字段

可以点击加号添加属性 image.png

输入key的值save_success,然后保存 image.png

然后逐个给资源文件添加对应的value image.png

2-2、配置messageResource 设置国际化资源文件

之前在springmvc的时候,我们实在xml中进行配置,在springboot中就不用这么繁琐了,Springboot中提供了MessageSourceAutoConfiguration 所以,我们不需要去配置messageResource。

2-2-1、MessageSourceAutoConfiguration工作原理的解析

那MessageSourceAutoConfiguration是怎么处理的呢,我们还通过源码来看一下,首先打开MessageSourceAutoConfiguration这个类,如下:

image.png

其中各个注解含义如下:

2-2-1-1、@Configuration(proxyBeanMethods = false)

1: 如果为true, 则表⽰被@Bean标识的⽅法都会被CGLIB进⾏代理,⽽且会⾛bean的⽣命周期中的⼀些⾏为(⽐如:@PostConstruct,@Destroy等 spring中提供的⽣命周期), 如果bean是单例的,那么在同⼀个configuration中调⽤@Bean标识的⽅法,⽆论调⽤⼏次得到的都是同⼀个bean,就是说这个bean只初始化⼀次。 2: 如果为false,则标识被@Bean标识的⽅法,不会被拦截进⾏CGLIB代理,也就不会⾛bean的⽣命周期中的⼀些⾏为(⽐如:@PostConstruct,@Destroy等 spring中提供的⽣命周期),如果同⼀个configuration中调⽤@Bean标识的⽅法,就只是普通⽅法的执⾏⽽已,并不会从容器中获取对象。所以如果单独调⽤@Bean标识的⽅法就是普通的⽅法调⽤,⽽且不⾛bean的⽣命周期。

所以,如果配置类中的@Bean标识的⽅法之间不存在依赖调⽤的话,可以设置为false,可以避免拦截⽅法进⾏代理操作,也是提升性能的⼀种优化。但是需要注意,@Bean标识的返回值对象还是会放⼊到容器中的,从容器中获取bean还是可以是单例的,会⾛⽣命周期

2-2-1-2、@ConditionalOnMissingBean(name = AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME, search = SearchStrategy.CURRENT)

@ConditionalOnMissingBean这个注解在之前说明,只要在IOC容器中能找到后面的类,就不走加载当前的类。

通过下图我们可以得知,最终要找的类为:messageSource image.png

2-2-1-3、@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)

自动配置的排序,默认情况下遵循从低到高的顺序,即最低值具有高优先级,这意味着它们首先出现在列表或数组中。 因为默认情况下,排序优先级为LOWEST_PRECEDENCE。 如果您首先需要最高值,那么我们需要将此值更改为Ordered.HIGHEST_PRECEDENCE。

2-2-1-4、@Conditional(ResourceBundleCondition.class)

@Conditional 自定义条件匹配 会传入一个实现了Condition接口的类——ResourceBundleCondition // ResourceBundleCondition 必须重写matches方法,自定义匹配规则,如果该方法返回true 就匹配成功

简单来说就是通过这个注解来判断当前配置类是否生效。

可以通过在application.properties中设置debug=true,就可以看哪些自动配置类生效,哪些不生效了。如下: image.png 通过上图我们可以得知,当前这个自动配置类是没有生效的,也就是说@Conditional(ResourceBundleCondition.class)返回的是false,我们要想这个自动配置类生效就需要让它返回true,下面通过源码看下ResourceBundleCondition类的原理机制

2-2-1-4-1、ResourceBundleCondition的源码跟进

首先进入这个类,可以看到他继承了SpringBootCondition这个类, image.png

然后再进入SpringBootCondition这个类,其中核心有个matchs方法,如下: image.png

通过上图可以看到返回了outcome.isMatch(),而outcome为getMatchOutcome(context,metadata)得到,可见getMatchOutcome方法的返回成了关键代码,

进入这个getMatchOutcome方法,可以看到它是SpringBootCondition抽象类的一个抽象方法,如下: image.png

可还记得一开始的ResourceBundleCondition就继承了SpringBootCondition抽象类,下面我们在返回头看下ResourceBundleCondition,它重写了我们刚刚说的核心方法:getMatchOutcome

image.png

2-2-1-4-1-1、getMatchOutcome方法的解读

首先看下这个方法,只要outcome==true,那么springboot的MessageSourceAutoConfiguration就可以加载了。

@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;
}

其中 String basename = context.getEnvironment().getProperty("spring.messages.basename", "messages"); 含义为:获取配置文件中spring.messages.basename ,由于我们没有配置,默认值是:messages

ConditionOutcome outcome = cache.get(basename); 然后通过basename去缓存中去获得outcome,如果取到了则直接返回,如果未取到,又进入getMatchOutcomeForBasenam方法,下面我们在进入这个犯法

2-2-1-4-1-2、getMatchOutcomeForBasenam方法解读
private ConditionOutcome getMatchOutcomeForBasename(ConditionContext context, String basename) {
   ConditionMessage.Builder message = ConditionMessage.forCondition("ResourceBundle");
   for (String name : StringUtils.commaDelimitedListToStringArray(StringUtils.trimAllWhitespace(basename))) {
   // 根据messages获取 该类路径下的所有propeties的资源文件
      for (Resource resource : getResources(context.getClassLoader(), name)) {
      // 这个条件非常关键: 只要basename的类路径下又资源文件就会匹配成功
         if (resource.exists()) {
            return ConditionOutcome.match(message.found("bundle").items(resource));
         }
      }
   }
   return ConditionOutcome.noMatch(message.didNotFind("bundle with basename " + basename).atAll());
}

上面代码中通过迭代判断,if(resource.exists())则运行ConditionOutcome.match(message.found("bundle").items(resource));,再进入这个match看一下,赋值了true image.png

2-2-2、总结

通过以上的源码分析,我们可以得知,只要在resource中有messages.properties配置文件,MessageSourceAutoConfiguration这个自动配置类就可以生效了。下面我们来测试一下

如下,将messages三个配置文件放到resource中就可以加载到这个自动配置类了。 image.png