你好,我是leo,在之前的文章业务系统国际化方案落地实践中,leo从项目整体的角度分析了国际化项目需要考虑的各种因素,其中提到了对表单信息和异常信息的国际化,可以使用Spring的MessageSource接口来实现。具体应该怎么实现呢,本文就来展开说说。
Spring Boot国际化功能的基础用法
首先来看看Spring Boot如何获取语言参数,以及如何根据语言参数拿到翻译内容
获取语言参数
从一个http请求中获取参数,无非有这么几种方式:header、url参数、body、cookie。
- 从Header获取当前请求的语言
spring默认使用AcceptHeaderLocaleResolver从http请求中解析语言,具体过程为:
public Locale resolveLocale(HttpServletRequest request) {
Locale defaultLocale = this.getDefaultLocale();
if (defaultLocale != null && request.getHeader("Accept-Language") == null) {
return defaultLocale;
} else {
Locale requestLocale = request.getLocale();
List<Locale> supportedLocales = this.getSupportedLocales();
if (!supportedLocales.isEmpty() && !supportedLocales.contains(requestLocale)) {
Locale supportedLocale = this.findSupportedLocale(request, supportedLocales);
if (supportedLocale != null) {
return supportedLocale;
} else {
return defaultLocale != null ? defaultLocale : requestLocale;
}
} else {
return requestLocale;
}
}
}
优先从http的header中获取Accept-Language的值作为当前语言。如果header中没有Accept-Language这个key,就取AcceptHeaderLocaleResolver实例中的默认语言(即defaultLocale,可以手动设置)。如果defaultLocale也为空,就用Locale.getDefault()从操作系统中获取当前语言。
AcceptHeaderLocaleResolver可以手动new一个,定制化默认语言和允许的语言。
@Configuration
public class Config{
@Bean
public LocaleResolver localeResolver() {
AcceptHeaderLocaleResolver acceptHeaderLocaleResolver = new AcceptHeaderLocaleResolver();
acceptHeaderLocaleResolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
//acceptHeaderLocaleResolver.setSupportedLocales();
return acceptHeaderLocaleResolver;
}
}
- 从Cookie获取当前请求的语言
spring提供了CookieLocaleResolver类,支持从Cookie中获取语言。Cookie的key可以自定义,使用方式如下:
@Configuration
public class Config{
@Bean
public LocaleResolver localeResolver() {
CookieLocaleResolver localeResolver= new CookieLocaleResolver();
localeResolver.setCookieName("language");
return localeResolver;
}
}
在请求中带上Cookie,如language=en_US,Spring就可以取到传入的语言参数了。
- 自定义获取当前请求的语言
如果不想用Header中的Accept-Language传递语言参数,也不想用Cookie传递语言参数,那么可以自定义传递方式,比如换一个header的key,或者把参数放在url,或者放在请求的Body中。示例如下:
@Configuration
public class Config{
@Bean
public LocaleResolver localeResolver() {
return new CustomLocaleResolver();
}
}
public class CustomLocaleResolver implements LocaleResolver {
@Override
public Locale resolveLocale(HttpServletRequest request) {
//从请求参数获取语言
Locale locale = Locale.forLanguageTag(request.getParameter("language"));
return locale == null ? Locale.SIMPLIFIED_CHINESE : locale;
}
@Override
public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {
}
}
根据语言参数获取翻译内容
无论使用以上哪种方式,Locale对象都会保存在LocaleContextHolder中的ThreadLocal里,贯穿当前整个http请求。在当前线程调用的任意方法内,用LocaleContextHolder.getLocale()可以获取Locale对象。
拿到Locale参数后,还需要配置每一种语言对应的翻译。在项目资源文件中建一个资源包
每种语言对应一个i18nMessage_语言.properties文件,内容为键值对。例如:
i18nMessae_zh_CN.properties文件中内容为
name=张三
i18nMessae_en_US.properties文件中内容为
name=zhangsan
然后在application.properties文件中指定资源包的路径(支持配置多个资源包,用逗号分隔):
spring.messages.basename:i18n/i18nMessage
spring.messages.encoding=UTF-8
最后,用MessageSource的getMessage方法就可以拿到资源包中配置的翻译内容了。示例如下:
@RestController
public class UserController {
@Resource
MessageSource messageSource;
@RequestMapping(value = "/getUserName",method = RequestMethod.GET)
public String getUserName(){
Locale locale= LocaleContextHolder.getLocale();
String userName= messageSource.getMessage("name",null,locale);
return userName;
}
}
结合参数验证和自定义业务异常实现错误信息国际化
有了上述的Spring Boot国际化基础功能和用法,就可以结合参数校验和自定义异常,来实现校验结果和异常信息的国际化了。
参数校验结果国际化
参数的校验使用hibernate-validator组件,在Spring Boot中可以直接添加starter依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
然后注入一个Validator的Bean,并配置它的国际化翻译源MessageSource,这是实现参数校验国际化的核心步骤。
@Configuration
public class Config{
@Autowired
private MessageSource messageSource;
@Bean
public Validator validator(){
LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
validator.setValidationMessageSource(messageSource);
return validator;
}
}
校验的字段不能硬编码错误信息,要写code,这个code也就是资源包中键值对的key
@Data
@Valid
public class BusinessModel {
@NotBlank(message="{name_blank_error}")
String name;
}
具体的校验方式可以有两种:
- 在Spring Boot请求的入口处做参数校验
@RequestMapping(value = "/getUserName",method = RequestMethod.POST)
public String getUserName(@Valid @RequestBody BusinessModel model){
}
在请求的入口处,对body中的数据做校验。校验结果不通过时会抛出BindException异常,因此还要在全局异常中处理异常。
@ControllerAdvice
public class GlobelExceptionHandler {
@ExceptionHandler({BindException.class})
@ResponseBody
public String handleBindException(BindException exception) {
List<FieldError> allErrors = exception.getFieldErrors();
StringBuilder sb = new StringBuilder();
for (FieldError errorMessage : allErrors) {
sb.append(errorMessage.getDefaultMessage()).append(", ");
}
return sb.toString();
}
在上面这个例子中,所有字段的校验错误保存在allErrors对象里,用FieldError的getDefaultMessage方法可以拿到每个字段的校验错误信息,这个错误信息是根据当前请求的语言翻译后的错误信息。实际项目中,还会对这些错误信息进一步封装,以前后端约定好的接口形式返回给前端。
- 在任意地方,对自定义的Model做参数校验
一般来说,接收到前端请求参数后,后端还会组装数据请求下游服务,可能是用http请求(Spring Cloud,或网关),也可能是RPC请求(Dubbo),还可能是MQ(RabbitMQ,Kafka,RocketMQ),假如是RPC请求,那么下游服务对请求参数的校验就无法直接用下面这样的写法来校验了:
public String getUserName(@Valid @RequestBody BusinessModel model){
}
对于这种情况,leo在项目中用手动调用校验接口的方式来校验参数。比如某个方法传入了BusinessModel类型的参数model,那么可以直接调用Validator的validate方法。
@Component
public class Service1 {
@Resource
Validator validator;
public String getUserName(@Valid BusinessModel model){
val validResult= validator.validate(model);
StringBuilder sb = new StringBuilder();
for (ConstraintViolation<?> errorMessage : validResult) {
sb.append(errorMessage.getMessage()).append(", ");
}
return sb.toString();
}
}
手动调用validate方法后拿到的结果,就是每个字段经翻译后的错误信息了。
异常信息国际化
异常信息国际化的思路是将自定义异常和MessageSource结合起来,返回的异常信息是经过MessageSource翻译后的内容,然后在全局异常处理环节将翻译后的错误信息返回给调用方。
例如,自定义的异常:
@Data
public class ApplicationException extends RuntimeException{
List<ErrorMsg> errors=new ArrayList<>();
public ApplicationException addError(String code, Object... args){
MessageSource messageSource= (MessageSource) SpringBeanUtils.getBean("messageSource");
String translatedMessage=messageSource.getMessage(code,args, LocaleContextHolder.getLocale());
this.errors.add(new ErrorMsg(code,translatedMessage));
return this;
}
@Override
public String getMessage(){
if(CollectionUtils.isEmpty(this.errors)){
return "";
}
StringBuilder sb = new StringBuilder();
for (ErrorMsg errorMsg : errors) {
sb.append(errorMsg.getMessage()).append(", ");
}
return sb.toString();
}
@Data
@AllArgsConstructor
public static class ErrorMsg{
String code;
String message;
}
}
在这个自定义异常中,有一个errors的集合,支持一个异常包含多个错误信息,errors中的元素是内部类ErrorMsg。
addError方法添加一个错误信息,code为错误码,要和资源包中的key对应,args为可选的错误信息中的参数。添加错误信息时,根据code翻译为具体的错误文本,放在ErrorMsg中。
getMessage方法重写父类的方法,返回errors集合中的所有错误信息。
业务校验代码示例:
@RequestMapping(value = "/getUserName",method = RequestMethod.POST)
public String getUserName(@Valid @RequestBody BusinessModel model){
//业务逻辑校验不通过抛出ApplicationException异常
ApplicationException exception=new ApplicationException();
exception.addError("5001",model.getName());
throw exception;
}
异常处理示例:
@ControllerAdvice
public class ExceptionHandler1 {
@ExceptionHandler({ApplicationException.class})
@ResponseBody
public String handleApplicationException(ApplicationException exception) {
return exception.getMessage();
}
}
总结
本文是leo之前的文章业务系统国际化方案落地实践的补充。
在本篇文章中,leo先总结了Spring Boot如何从http请求中获取语言参数,如何用MessageSource接口从资源包获取翻译内容,并结合常用的参数校验组件hibernate-validator给出了参数校验国际化的实现步骤,以及结合自定义异常实现了业务异常信息的国际化。