SpringBoot实现国际化的教程
作者:对酒当歌丶人生几何
前言
SpringBoot提供了国际化功能,其原理是将配置的各个语言资源文件信息,以Map的形式进行缓存。
当前端请求给定某个语言标识时(一般是放到请求头中
),拿去指定的语言标识去获取响应的响应信息。
在Springboot项目启动时,由MessageSourceAutoConfiguration类进行消息资源自动配置。
该类存在 @Conditional 条件注解,也就是说必须满足某个条件是才会进行自动装载配置。
- ResourceBundleCondition类用于判断是否满足自动注入条件。
- getMatchOutcome用于返回一个 ConditionOutcome 对象,用于后续判断是否满足自动注入条件。该方法会自动读取spring.messages.basename配置的资源文件地址信息,通过getResources方法获取默认的文件资源。如果该资源不存在,则不满足自动注入条件。
- getResources明确标注了只能从
classpath*
下拿去资源文件,文件类型为properties。
/** * 判断是否满足自动注入条件 * @param context the condition context * @param metadata the annotation metadata * @return */ @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()); } //读取消息资源文件,从classpath下寻找格式为properties类型的资源文件 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; } }
MessageSourceProperties类则用于配置消息源的配置属性。在ResourceBundleCondition返回条件成立的情况下,会通过注解Bean进行注入。读取前缀带有spring.messages的配置信息。
@Bean @ConfigurationProperties(prefix = "spring.messages") public MessageSourceProperties messageSourceProperties() { return new MessageSourceProperties(); }
MessageSourceProperties类可配置的属性并不多,具体属性含义用途如下:
#配置国际化资源文件路径,基础包名名称地址 spring.messages.basename=il8n/messages #编码格式,默认使用UTF-8 spring.messages.encoding=UTF-8 #是否总是应用MessageFormat规则解析信息,即使是解析不带参数的消息 spring.messages.always-use-message-format=false # 是否使用消息代码作为默认消息,而不是抛出NoSuchMessageException.建议在开发测试时开启,避免不必要的抛错而影响正常业务流程 spring.messages.use-code-as-default-message=true
在MessageSourceProperties完成读取配置之后,将会自动注入MessageSource,而默认注入的MessageSource的实现类ResourceBundleMessageSource。ResourceBundleMessageSource是SpringBoot实现国际化的核心。其采用的是及时加载文件的形式。
即:只有当某种特定的语言要求被返回时,才会去读取资源文件,将消息内容缓存起来并通过响应码进行返回具体消息。ResourceBundleMessageSource的源码并不复杂,这里就不展开讲解。
@Bean public MessageSource messageSource(MessageSourceProperties properties) { //消息资源绑定类,用于缓存资源消息 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()); } //是否总是应用MessageFormat规则解析信息,即使是解析不带参数的消息 messageSource.setAlwaysUseMessageFormat(properties.isAlwaysUseMessageFormat()); //是否使用消息代码作为默认消息,而不是抛出NoSuchMessageException.建议在开发测试时开启,避免不必要的抛错而影响正常业务流程 messageSource.setUseCodeAsDefaultMessage(properties.isUseCodeAsDefaultMessage()); return messageSource; }
一、配置消息资源文件
消息资源文件用于存储不同国家语言响应的消息,我们通过源码知道该文件必须是properties类型(在你不去更改源码的时候),因此我们需要在resources资源文件下存放消息文件。
这里我存放三个文件,message基础资源文件(内容可以为空),messages_en_US.properties英文文件,messages_zh_CN.properties中文文件。
messages_zh_CN.properties和messages_en_US.properties内容如下:
二、配置消息源
这里采用的是以application.properties形式进行配置,您也可以采用yaml文件形式进行配置:
#配置国际化资源文件路径 spring.messages.basename=il8n/messages #编码格式,默认使用UTF-8 spring.messages.encoding=UTF-8 #是否总是应用MessageFormat规则解析信息,即使是解析不带参数的消息 spring.messages.always-use-message-format=false # 是否使用消息代码作为默认消息,而不是抛出NoSuchMessageException.建议在开发测试时开启,避免不必要的抛错而影响正常业务流程 spring.messages.use-code-as-default-message=true
三、配置消息拦截器
拦截器用于从请求头中获取语言标识,以便于后续根据语言标识响应不同的响应信息。
package com.il8n.config; import lombok.Getter; import lombok.Setter; import org.jetbrains.annotations.NotNull; import org.springframework.util.ObjectUtils; import org.springframework.web.servlet.LocaleResolver; import org.springframework.web.servlet.i18n.LocaleChangeInterceptor; import org.springframework.web.servlet.support.RequestContextUtils; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * @Author: Greyfus * @Create: 2024-01-12 13:06 * @Version: 1.0.0 * @Description:国际化拦截器 */ @Setter @Getter public class IL8nLangInterceptor extends LocaleChangeInterceptor { private String langHeader; @Override public boolean preHandle(HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull Object handler) throws ServletException { String locale = request.getHeader(getLangHeader()); if (locale != null) { if (iL8nCheckHttpMethod(request.getMethod())) { LocaleResolver localeResolver = RequestContextUtils.getLocaleResolver(request); if (localeResolver == null) { throw new IllegalStateException("No LocaleResolver found: not in a DispatcherServlet request?"); } try { localeResolver.setLocale(request, response, parseLocaleValue(locale)); } catch (IllegalArgumentException ex) { if (isIgnoreInvalidLocale()) { if (logger.isDebugEnabled()) { logger.debug("Ignoring invalid locale value [" + locale + "]: " + ex.getMessage()); } } else { throw ex; } } } } // Proceed in any case. return true; } public boolean iL8nCheckHttpMethod(String currentMethod) { String[] configuredMethods = getHttpMethods(); if (ObjectUtils.isEmpty(configuredMethods)) { return true; } for (String configuredMethod : configuredMethods) { if (configuredMethod.equalsIgnoreCase(currentMethod)) { return true; } } return false; } }
注入消息拦截器,并设置默认语言为中文:
package com.il8n.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.LocaleResolver; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.i18n.LocaleChangeInterceptor; import org.springframework.web.servlet.i18n.SessionLocaleResolver; import java.util.Locale; /** * @Author: Greyfus * @Create: 2024-01-12 00:22 * @Version: 1.0.0 * @Description:语言国际化配置 */ @Configuration public class IL8nLangConfig implements WebMvcConfigurer { private static final String LANG_HEADER = "lang"; @Bean public LocaleResolver localeResolver() { SessionLocaleResolver sessionLocaleResolver = new SessionLocaleResolver(); sessionLocaleResolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE); return sessionLocaleResolver; } @Bean public LocaleChangeInterceptor localeChangeInterceptor() { IL8nLangInterceptor lci = new IL8nLangInterceptor(); //设置请求的语言变量 lci.setLangHeader(LANG_HEADER); return lci; } /** * 注册拦截器 * @param registry */ @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(localeChangeInterceptor()); } }
四、编写消息工具类
消息工具类用于通过指定的消息码获取对应的响应消息
package com.il8n.config; import lombok.Getter; import org.jetbrains.annotations.NotNull; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.stereotype.Component; import java.util.Map; /** * @Author: Greyfus * @Create: 2024-01-12 00:36 * @Version: 1.0.0 * @Description: 工具 */ @Component public class SpringUtils implements ApplicationContextAware { @Getter private static ApplicationContext applicationContext; public SpringUtils() { } public void setApplicationContext(@NotNull ApplicationContext applicationContext) { SpringUtils.applicationContext = applicationContext; } public static <T> T getBean(String name) { return (T) applicationContext.getBean(name); } public static <T> T getBean(Class<T> clazz) { return applicationContext.getBean(clazz); } public static <T> T getBean(String name, Class<T> clazz) { return applicationContext.getBean(name, clazz); } public static <T> Map<String, T> getBeansOfType(Class<T> type) { return applicationContext.getBeansOfType(type); } public static String[] getBeanNamesForType(Class<?> type) { return applicationContext.getBeanNamesForType(type); } public static String getProperty(String key) { return applicationContext.getEnvironment().getProperty(key); } public static String[] getActiveProfiles() { return applicationContext.getEnvironment().getActiveProfiles(); } }
package com.il8n.config; import org.springframework.context.MessageSource; import org.springframework.context.i18n.LocaleContextHolder; /** * @Author: Greyfus * @Create: 2024-01-12 00:29 * @Version: 1.0.0 * @Description: IL8N语言转换工具 */ public class IL8nMessageUtils { private static final MessageSource messageSource; static { messageSource = SpringUtils.getBean(MessageSource.class); } /** * 获取国际化语言值 * * @param messageCode * @param args * @return */ public static String message(String messageCode, Object... args) { return messageSource.getMessage(messageCode, args, LocaleContextHolder.getLocale()); } }
五、测试国际化
编码一个Controller用于模拟测试效果,代码如下:
package com.il8n.controller; import com.il8n.config.IL8nMessageUtils; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; /** * @Author: DI.YIN * @Date: 2025/1/6 9:37 * @Version: * @Description: **/ @RestController @RequestMapping("/mock") public class IL8nTestController { @RequestMapping(value = "/login") public ResponseEntity<String> login(@RequestParam(value = "userName") String userName, @RequestParam(value = "password") String password) { if (!"admin".equals(userName) || !"admin".equals(password)) { return new ResponseEntity<>(IL8nMessageUtils.message("LoginFailure", (Object) null), HttpStatus.OK); } return new ResponseEntity<>(IL8nMessageUtils.message("loginSuccess", userName), HttpStatus.OK); } }
消息拦截器会尝试从请求头中获取属性为lang的值,将其作为语言标识,因此我们在使用PostMan模拟时,需要在请求头中增加lang属性。
模拟结果如下:
中文语言标识
:
英文语言标识
:
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。