前言
最新第一次在做SpringBoot的国际化,网上搜了很多相关的资料,都是一些简单的使用例子,达不到在实际项目中使用的要求,因此本次将结合查询的资料以及自己的实践整理出SpringBoot配置国际化的流程。
SpringBoot国际化
"i18n"是国际化(internationalization)的缩写,数字18代表了国际化这个单词中间的字母数量。类似这样的缩写还有k8s(kubernetes)等
Spring Boot国际化是指在Spring Boot应用中实现多语言支持的功能。通过国际化,应用可以根据用户的语言偏好自动切换显示的语言版本,从而提供贴近用户的界面和文本信息。
SpringBoot官方国际化支持文档:7.5. Internationalization(请点击我)
由于SpringBoot默认集成国际化,因此本次实践也是基于SpringBoot的自动配置来进行。
准备环境
本次实践我使用的环境或工具版本如下:
SpringBoot 3.0.6
、IDEA社区版2023.1
、OpenJDK 17
引入依赖
<!-- web中提供国际化支持 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>3.0.6</version> </dependency> <!-- 参数校验 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> <version>3.0.6</version> </dependency>
自定义配置
@AutoConfigureBefore(WebMvcAutoConfiguration.class) public class LocaleConfig { /** * 国际化消息源 */ @Resource private MessageSource messageSource; /** * 区域解析器,供消息源@MessageSource根据不同的区域@java.util.Locale读取不同的properties文件 * * @return {@code LocaleResolver} */ @Bean public LocaleResolver localeResolver() { AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver(); // 设置默认区域:简体中文 localeResolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE); return localeResolver; } /** * 使用自定义LocalValidatorFactoryBean, * 设置Spring国际化消息源,用户jsr303验证信息实现自定义国际化 * */ @Bean public Validator getValidator() { LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean(); bean.setValidationMessageSource(messageSource); return bean; } }
java.util.Locale: Locale对象表示特定的地理、政治或文化区域,用以区分区域。
org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver:通过请求头Accept-Language的值(zh-CN、en-US等)来改变当前的区域设置
org.springframework.context.MessageSource:用于解析消息的策略接口,支持此类消息的参数化和国际化。根据Locale区域读取不同的properties国际化文件
@AutoConfigureBefore(WebMvcAutoConfiguration.class):由于WebMvcAutoConfiguration
会注入一个默认的LocaleResolver
,因此自定义的LocaleConfig
要在WebMvcAutoConfiguration
之前先执行,且beanName是localeResolver
,目的就是用我们配置的LocaleConfig
替换掉WebMvcAutoConfiguration
自动注入的LocaleConfig
。
以下代码片段是WebMvcAutoConfiguration自动注入LocaleResolver 的方法
/** * DispatcherServlet.LOCALE_RESOLVER_BEAN_NAME的值就是"localeResolver" */ @Override @Bean @ConditionalOnMissingBean(name = DispatcherServlet.LOCALE_RESOLVER_BEAN_NAME) public LocaleResolver localeResolver() { if (this.webProperties.getLocaleResolver() == WebProperties.LocaleResolver.FIXED) { return new FixedLocaleResolver(this.webProperties.getLocale()); } AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver(); localeResolver.setDefaultLocale(this.webProperties.getLocale()); return localeResolver; }
国际化文件
在工程resources
目录下新建目录i18n
,在i18n
目录下新建三个国际化文件:messages.properties
、messages_en_US.properties
、messages_zh_CN.properties
zh-CN:简体中文
en-US:英语
在不配置默认的区域情况下,当没有找到匹配的语言文件时,会读取默认的messages.properties
。
配置application.yml
spring: ## note: ---------------- 国际化配置 ---------------- messages: basename: i18n/messages fallback-to-system-locale: false
basename
:基名,多个基名以逗号分隔(实质上是完全限定的类路径位置),每个基名都遵循 ResourceBundle 约定,对基于斜杠的位置提供了宽松的支持。如果它不包含包限定符(例如“org.mypackage”),则将从类路径根目录解析它。
基名可以理解为前缀,默认是从classpath根路径下找,配置的路径可以用斜杠/
,也可以用.
,即i18n/messages
或i18n.messages
,然后messages就是国际化文件的前缀,messages.properties
就是默认国际化文件。
fallback-to-system-locale
:如果未找到特定区域设置的文件,是否回退到系统区域设置。如果关闭此功能,则唯一的回退将是默认文件。就是我想要的区域语言文件没有的时候,就从系统中解析系统的区域,以系统的区域再找一次文件,找到就返回系统区域文件,如果找不到,就返回默认的文件。
国际化消息工具类
@Slf4j @Component public class LocaleUtil implements ApplicationContextAware { private static MessageSource messageSource; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { LocaleUtil.messageSource = applicationContext.getBean(MessageSource.class); } /** * 获取国际化message * * @param code code * @param args 占位参数 * @param defaultMessage 默认值 * @return 国际化文本 */ public static String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage) { return messageSource.getMessage(code, args, defaultMessage, LocaleContextHolder.getLocale()); } /** * 获取国际化message * * @param code code * @param args 占位参数 * @param defaultMessage 默认值 * @param locale 地区 * @return 国际化文本 */ public static String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale) { return messageSource.getMessage(code, args, defaultMessage, locale); } /** * 获取国际化message * * @param code code * @param defaultMessage 默认值 * @return 国际化文本 */ public static String getMessage(String code, @Nullable String defaultMessage) { return messageSource.getMessage(code, null, defaultMessage, LocaleContextHolder.getLocale()); }
封装国际化文本的读取接口,主要方便在代码中使用,不需要每次@Autowired
注入MessageSource
来使用。
国际化使用
异常提示国际化
在业务代码中,业务异常我们通常是抛出异常,由统一异常处理来根据异常code
和message
封装成统一的返回对象,国际化这里我们也是一样,code
不变,message
改成国际化消息的key
。
- 定义业务异常对象
public class BussinessException extends RuntimeException { private String code; public BussinessException (String code, String message) { super(message); this.code= code; } }
- 定义异常类型枚举
public enum ErrorCodeEnum { DEMO_ERROR(99999, "i18n.user.check.username.err"); /** * 提示 */ private final String message; /** * 状态吗 */ private final String code; ErrorCodeEnum (String code, String message) { this.code = code; this.message = message; } /** * 获取国际化错误提示 * * @return 错误提示文本 */ public String getMessage() { return LocaleUtil.getMessage(message, message); } /** * 获取国际化错误提示 * * @param args 错误提示参数 * @return 错误提示文本 */ public String getMessage(Object[] args) { return LocaleUtil.getMessage(message, args, message); } /** * 获取错误code * * @return 错误code */ public Integer getCode() { return code; } }
- 国际化文件配置
在国际化文件都配置上i18n.user.check.username.err
不同语言的翻译
## messages.properties i18n.user.check.username.err=用户名校验不通过 ## messages_en_US.properties i18n.user.check.username.err=The username does not comply with regulations ## messages_zh_CN.properties i18n.user.check.username.err=用户名不符合规范
- 业务逻辑校验(使用场景之一)
/** * 校验用户名是否符合规范 * * @param userName 用户名 * @return {@code String} */ public String checkUserName(String userName) { if (StringUtils.isBlank(userName)) { throw new BussinessException(ErrorCodeEnum.DEMO_ERROR.getCode(), ErrorCodeEnum.DEMO_ERROR.getMessage()); } // check logic code ... return userName; }
参数校验国际化
SpringBoot项目我们在做参数校验通常会使用JSR303、jakarta.validation参数校验快速失败。
import jakarta.validation.constraints.NotEmpty; import lombok.Data; @Data public class TestReq { @NotEmpty private String userName; }
我这里是导入开头的spring-boot-starter-validation
依赖就可以使用hibernate-validator
给我们提供的常用校验注解的国际化。
查看@NotEmpty
注解源码,message默认就读国际化文件里jakarta.validation.constraints.NotEmpty.message
@Documented @Constraint(validatedBy = { }) @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) @Retention(RUNTIME) @Repeatable(List.class) public @interface NotEmpty { String message() default "{jakarta.validation.constraints.NotEmpty.message}"; Class<?>[] groups() default { }; Class<? extends Payload>[] payload() default { }; /** * Defines several {@code @NotEmpty} constraints on the same element. * * @see NotEmpty */ @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) @Retention(RUNTIME) @Documented public @interface List { NotEmpty[] value(); } }
这里就贴ValidationMessages.properties
的jakarta.validation.constraints.NotEmpty.message
,其他文件大家自行查看。
jakarta.validation.constraints.NotEmpty.message = must not be empty
自定义校验错误提示信息
当我们不想使用hibernate-validator
给我们提供的默认提示信息时,我们可以自定义自己的错误提示信息。这里就复用前面配置的i18n.user.check.username.err
@Data public class TestReq { @NotEmpty(message = "{i18n.user.check.username.err}") private String userName; }
如此,在Accept-Language=en-US
即我想返回的信息是英文,参数校验不通过时,提示的不是must not be empty
,而是我们自定义的The username does not comply with regulations
。
结语
时间有限,国际化的实践流程截图没提供,大家根据文中的操作步骤也可以完成国际化的demo,后续有时间再完善,大家也期待下一篇的源码解读吧。
文中如有描述不清楚、读者不理解意思的地方,大家评论区打出,我来完善哈。