最近公司拓展海外市场,要做海外版app,因此需要做国际化,在此对后端国际化的实现过程做了梳理。
几个改动方面
首先整理了项目中需要国际化的地方,大体来说有异常提示,入参非空校验,枚举类返回等。
这几种方式的代码写法如下:
1.异常提示:
throw new FeignException(UserResultCode.*ADD_COLLECT_FAILED*.getCode(),
I18nUtils.*i18n*("UserResultCode.ADD_COLLECT_FAILED"));
2.入参非空校验:
@NotNull(message = "{reqData.goodsId}")
private Long goodsId;
3.枚举类:
public enum PayStatus {
PAY_STATUS_1(1, "待支付"),
PAY_STATUS_2(2, "部分支付"),
PAY_STATUS_3(3, "全部支付"),
;
private Integer key;
private String value;
PayStatus(Integer key, String value) {
this.key = key;
this.value = value;
}
public Integer getKey() {
return key;
}
public void setKey(Integer key) {
this.key = key;
}
public String getValue() {
return I18nUtils.tryI18n("payStatusEnum"+"."+this.getDeclaringClass().getSimpleName()+"."+this.name());
}
public void setValue(String value) {
this.value = value;
}
}
具体实现思路
如下:
1. 配置文件添加配置,指定国际化文件路径,编码。
spring:
messages:
basename: i18n/messages
cache-duration: -1
encoding: UTF-8
fallback-to-system-locale: true
2.在指定的路径下创建国际化配置文件。
3.创建国际化工具类,使用该工具类进行文案的国际化。
@Component
public class I18nUtils {
private static MessageSource messageSource;
@Autowired
public void setMessageSource(MessageSource messageSource) {
I18nUtils.messageSource = messageSource;
}
public static String i18n(String code, Object... args) {
return messageSource.getMessage(code, args, LocaleContextHolder.getLocale());
}
public static String i18nOrDefault(String code, String defaultMessage, Object... args) {
return messageSource.getMessage(code, args, defaultMessage, LocaleContextHolder.getLocale());
}
@NonNull
public static String tryI18nOrDefault(@NonNull String code,String defaultMessage, @NonNull Object... args) {
String res;
try {
res = i18n(code, args);
} catch (Exception ignored) {
res = code;
if(StringUtils.hasLength(defaultMessage)){
res = defaultMessage;
}
}
return res;
}
@NonNull
public static String tryI18n(@NonNull String code, @NonNull Object... args) {
String res;
try {
res = i18n(code, args);
} catch (Exception ignored) {
res = code;
}
return res;
}
}
4.创建配置类。
@Configuration
public class LocaleConfig implements WebMvcConfigurer {
@Bean
public SessionLocaleResolver localeResolver() {
SessionLocaleResolver localeResolver = new SessionLocaleResolver();
localeResolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
return localeResolver;
}
@Bean
public WebMvcConfigurer localeInterceptor() {
return new WebMvcConfigurer() {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LocaleInterceptor());
}
};
}
@Bean
public LocalValidatorFactoryBean localValidatorFactoryBean() {
LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean();
// 设置消息源
bean.setValidationMessageSource(resourceBundleMessageSource());
return bean;
}
@Bean
public MessageSource resourceBundleMessageSource() {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setDefaultEncoding(StandardCharsets.UTF_8.toString());
// 多语言文件地址
messageSource.addBasenames("i18n/messages");
return messageSource;
}
@Bean
public MethodValidationPostProcessor validationPostProcessor() {
MethodValidationPostProcessor processor = new MethodValidationPostProcessor();
processor.setValidator(localValidatorFactoryBean().getValidator());
return processor;
}
@Override
public Validator getValidator() {
return localValidatorFactoryBean();
}
}
5.创建拦截器,获取请求头中的语言类型。
@Slf4j
public class LocaleInterceptor extends LocaleChangeInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String newLocale = request.getHeader(GrayscaleConstant.DW_LANG);
if(StringUtils.isEmpty(newLocale)){
newLocale = "zh_CN";
}
LocaleContextHolder.setLocale(parseLocaleValue(newLocale),true);
return true;
}
}
国际化原理
spring-webmvc提供了支持国际化的i18n包,可以根据前端传来的参数动态读取配置。 MessageSource 接口定义了获取国际化消息的方法,而 ResourceBundleMessageSource 是其默认实现类。
ResourceBundleMessageSource类使用 java.util.ResourceBundle 来加载和解析属性文件作为国际化资源。以下是其关键源码逻辑:
LocaleResolver 接口用于解析用户的区域设置(Locale),以确定使用哪种语言。而 AcceptHeaderLocaleResolver 是 LocaleResolver 接口的默认实现类。
在它们的抽象子类实现中提供了默认的Locale对象(Locale是java.util包下提供了所有语言和国家地区映射关系的枚举对象)。
AcceptHeaderLocaleResolver 是根据请求头中的 Accept-Language 头部信息来解析用户的首选语言。
请求头中没有Accept-Language字段会用默认的语言环境,如果有,会从request对象中获取Locale对象,如果当前解析器不支持该类型的语言对象(即!supportedLocales.contains(requestLocale)),那么findSupportedLocale会在已经支持的Locale集合里和request.getLocales()里寻找到一对locale.getLanguage()相同的结果,并返回当前的Locale对象。
ReloadableResourceBundleMessageSource类负责读取配置文件。
calculateAllFilenames方法负责根据用户指定的basename名称解析出绑定了具体语言、区域的资源配置文件。
当根据basename找不到对应的Loadle映射的资源文件名时,calculateFilenamesForLocale方法会根据当前传入的Locale对象和basename解析出对应的资源文件名。
getProperties方法源码:
加载国际化资源文件:
然后将PropertiesHolder对象和文件名的映射关系保存到全局变量cachedProperties中去:
同一个basename下的其它资源配置文件会在遍历中被逐个加载。 这时,回到resolveCode方法,在通过getProperties方法获取了PropertiesHolder对象后,直接getMessageFormat调用即可。
这里返回的是一个MessageFormat对象。 而PropertiesHolder是作为ReloadableResourceBundleMessageSource的一个内部类存在。
重点看下getMessageFormat方法,因为国际化的功能都是通过此方法来获取资源值的:
通过properties.getProperty获取到code对应的配置值msg,然后再将这个msg和Locale对象封装成一个MessageFormat对象result,又将result和locale再做一层映射关系保存到localeMap,再将这个localeMap和code做绑定存储到ConcurrentMap<String,Map<Locale,MessageFormat>>中。
spring-boot-autoconfigure:2.2.5.RELEASE的META-INF/spring.factories文件中对MessageSourceAutoConfiguration类进行自动装配:
messageSource方法配置了@Bean注解并返回了ResourceBundleMessageSource对象,所以在spring boot中资源文件是由ResourceBundleMessageSource类去解析的。
DispatcherServlet类在初始化方法中,会拿到语言环境解析器的bean:
initLocaleResolver方法如下:
DispatcherServlet类继承了父类FrameworkServlet,在父类实现中,完成了上下文和环境等一系列的初始化工作
同时重写了HTTP请求的八大方法,让请求调用统一的前置处理器,再进入到子类DispatcherServlet的doService中:
前置处理器processRequest如下:
processRequest通过LocaleContextHolder获取LocaleContext对象,然后通过buildLocaleContext方法获取Locale。
buildLocaleContext方法从DispatcherServlet初始化时拿到的LocaleResolver去调用了resolveLocale方法获取Locale对象,如果没有的话就直接取HttpServletRequest中的Locale返回,然后this.initContextHolders方法将解析后的Locale对象设值到LocaleContextHolder中:
通常语言环境解析器会配合LocaleChangeInterceptor一起使用,LocaleChangeInterceptor是i18n包下提供的拦截器,可以通过setParamName方法设置该拦截器的拦截参数名,它的preHandle方法会从请求中获取设置的拦截参数值,并设置到LocaleResolver里去:
总的来说步骤如下:
- MessageSource 和 ResourceBundleMessageSource:
- MessageSource 接口定义了获取国际化消息的方法,其中最常用的方法是 getMessage()。
- ResourceBundleMessageSource 是 MessageSource 接口的默认实现类,用于加载和解析属性文件(.properties)作为国际化资源。
- LocaleResolver 和 AcceptHeaderLocaleResolver:
- LocaleResolver 接口定义了解析用户区域设置(Locale)的方法,其中最常用的方法是resolveLocale()。
- AcceptHeaderLocaleResolver 是 LocaleResolver 接口的默认实现类,根据请求头中的 Accept-Language 头部信息解析用户的首选语言。
- LocaleContextHolder:
- LocaleContextHolder 是一个用于存储当前用户区域设置的上下文工具类。它使用 ThreadLocal 来保存当前的 Locale,确保在应用程序的任何地方都可以访问当前的区域设置。
- LocaleContextHolder类提供了静态方法 getLocale() 和 setLocale() 用于获取和设置当前的 Locale。
- 配置文件加载:
- Spring Boot 的配置文件(如 application.properties 或 application.yml)中可以设置 spring.messages.basename 属性来指定国际化资源文件的基本名称。
- 通过加载配置文件,Spring Boot 会在内部创建一个 MessageSource 实例,并将基本名称传递给 ResourceBundleMessageSource。
- 获取国际化消息的源码调用链:
- 在代码中,通过注入 MessageSource 实例来获取国际化消息。
- 具体调用链通常涉及以下类和方法:Controller -> Service -> MessageSource.getMessage()。
作者:洞窝-廷双