写在前面 在REST接口的实现方案中,后端可以仅仅返回一个code
,让前端根据code的内容做自定义的消息提醒。当然,也有直接显示后端返回消息的方案。在后端直接返回消息的方案中,如果要提供多个不同语种国家使用,则需要做国际化消息的实现。
1 2 3 4 5 400 BAD_REQUEST{ "code" : "user.email.token" , "message" : "The email is token." }
实现的目标:
validation的error code可以按照不同语言响应;
自定义error code可以按照不同语言响应;
指定默认的语言;
版本说明
1 2 3 spring-boot: 2.1.6.RELEASE sping: 5.1.8.RELEASE java: openjdk 11.0.13
初始化项目 1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency > java
添加如下的Controller类和User实体类,其中的ServiceException
是一个自定义的异常类,该异常会带上业务处理过程中的异常码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Validated @RestController @RequestMapping ("/user" )public class UserController { private static final String TOKEN_EMAIL = "token@example.com" ; @PostMapping public User createUser (@RequestBody @Valid User user) { if (TOKEN_EMAIL.equalsIgnoreCase(user.getEmail())) { String message = String.format("The email %s is token." , user.getEmail()); throw new ServiceException("user.email.token" , message); } return user; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Data public class User { @NotNull @Length (min = 5 , max = 12 ) @Pattern (regexp = "^r.*" , message = "{validation.user.username}" ) private String username; @NotNull @Email private String email; @Range (min = 12 , message = "{validation.user.age}" ) private int age; }
Validation中实现国际化信息 基本实现 在默认的SpringBoot配置中,只要在请求中增加Accept-Language
的Header即可实现Validation错误信息的国际化。如下的请求会使用返回中文的错误信息。
1 2 3 4 5 6 7 8 curl --location --request POST 'http://localhost:8080/user' \ --header 'Accept-Language: zh-CN' \ --header 'Content-Type: application/json' \ --data-raw '{ "username": "xx", "email": "token@example.com", "age": 24 }
对于自定义的message,如validation.user.username
,则只需要在resource
目录下创建一个basename为ValidationMessages
的国际化信息文件即可实现。
1 2 3 4 └── resources ├── ValidationMessages.properties ├── ValidationMessages_zh_CN.properties └── application.yml
1 2 validation.user.age=用户的年龄应该大于{min}. validation.user.username=用户名应该以r字母开始
国家化信息文件文件名定义规则:
1 2 3 basename_<language>_<country_or_region>.properties ValidationMessages_en.properties ValidationMessages_zh_CN.properties
更多的文件,可以到hibernate-validator的github仓库查看(文末会给出链接)。
LocaleResolver解析locale信息 在spring-framework的spring-webmvc/.../i18n
中,可以找到如下三种不同的LocaleResolver
的实现:
默认使用AcceptHeaderLocaleResolver
。
设置指定固定的locale 通过指定spring.mvc.locale-resolver=fixed
可以使用固定的locale。如果项目只有一种语言可以做该指定,以免客户端没有配置Accept-Language
的header而出现多种不同的语言。
1 2 3 4 spring: mvc: locale-resolver: fixed locale: en
在spring-boot的spring-boot-autoconfig/.../web/serlet
中可以找到如下的配置,该配置指定了localeResolver的配置方式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package org.springframework.boot.autoconfigure.web.servlet;public class WebMvcAutoConfiguration { @Bean @ConditionalOnMissingBean @ConditionalOnProperty (prefix = "spring.mvc" , name = "locale" ) public LocaleResolver localeResolver () { if (this .mvcProperties.getLocaleResolver() == WebMvcProperties.LocaleResolver.FIXED) { return new FixedLocaleResolver(this .mvcProperties.getLocale()); } AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver(); localeResolver.setDefaultLocale(this .mvcProperties.getLocale()); return localeResolver; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package org.springframework.boot.autoconfigure.web.servlet;@ConfigurationProperties (prefix = "spring.mvc" )public class WebMvcProperties { ... private Locale locale; private LocaleResolver localeResolver = LocaleResolver.ACCEPT_HEADER; ... }
通过Url指定Locale 通过LocaleChangeInterceptor
获取请求中的lang
参数来设定语言,并将locale保存到Cookie中,请求了一次之后,相同的请求就无需再带上该lang参数,即可使用原本已经设定的locale。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @Configuration @ConditionalOnProperty (prefix = "spring.mvc" , name = "customer-locale-resolver" , havingValue = "cookie" )public class MvcConfigurer implements WebMvcConfigurer { @Bean public LocaleResolver localeResolver (@Value("${spring.mvc.locale:null}" ) Locale locale) { CookieLocaleResolver resolver = new CookieLocaleResolver(); if (locale != null ) { resolver.setDefaultLocale(locale); } return resolver; } @Bean public LocaleChangeInterceptor localeInterceptor () { LocaleChangeInterceptor localeInterceptor = new LocaleChangeInterceptor(); localeInterceptor.setParamName("lang" ); return localeInterceptor; } @Override public void addInterceptors (InterceptorRegistry registry) { registry.addInterceptor(localeInterceptor()); } }
1 2 3 4 5 6 7 curl --location --request POST 'http://localhost:8080/user?lang=en' \ --header 'Content-Type: application/json' \ --data-raw '{ "username": "rxdxxxx", "email": "token@example.com", "age": 24 }'
之后的请求只要带上含有设置的cookie即可。
1 2 3 4 5 6 7 8 curl --location --request POST 'http://localhost:8080/user' \ --header 'Content-Type: application/json' \ --header 'Cookie: org.springframework.web.servlet.i18n.CookieLocaleResolver.LOCALE=zh-CN' \ --data-raw '{ "username": "rxdxxxx", "email": "token@example.com", "age": 24 }'
自定义Error Code实现国际化信息 在前文给出的代码中,当email为`token@example.com 时,抛出邮箱已被占用的异常。自定义异常中的code不是通过validator校验,所以不能通过validator自动处理。需要通过
MessageSource`来将定义的code转化为message,可以将MessageSource理解为一个key-value结构的类。
在resources
目录下创建目录i18n
,然后创建以messages
为basename的国际化信息文件。如下面的目录结构。
1 2 3 4 5 6 7 8 └── resources ├── ValidationMessages.properties ├── ValidationMessages_zh_CN.properties ├── application.yml └── i18n ├── messages.properties ├── messages_en.properties └── messages_zh_CN.properties
message_zh_CN.properties
在application.yml
中添加如下配置,即可让springboot生成含有i18n/messages
中的properties的配置了。默认情况下,spring.messages.basename=messages
,也就是可以直接在resources
目录下创建需要的messages
国际化信息文件。这里为了把i18n配置都放到一个目录下才做了修改,也可以不修改。
1 2 3 spring: messages: basename: i18n/messages
定义ApiExceptionHandler
捕获指定的异常,并在其中的code转化为message。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 package io.github.donespeak.springbootsamples.i18n.support;import io.github.donespeak.springbootsamples.i18n.core.ServiceException;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.context.MessageSource;import org.springframework.http.HttpHeaders;import org.springframework.http.HttpStatus;import org.springframework.http.ResponseEntity;import org.springframework.web.bind.annotation.ExceptionHandler;import org.springframework.web.bind.annotation.RestControllerAdvice;import org.springframework.web.context.request.WebRequest;import org.springframework.web.servlet.LocaleResolver;import javax.servlet.http.HttpServletRequest;import java.util.Locale;@Slf 4j@RestControllerAdvice public class ApiExceptionHandler { private LocaleResolver localeResolver; private MessageSource messageSource; public ApiExceptionHandler (LocaleResolver localeResolver, MessageSource messageSource) { this .localeResolver = localeResolver; this .messageSource = messageSource; } @ExceptionHandler (ServiceException.class) public ResponseEntity<Object> handleServiceException (ServiceException ex, HttpServletRequest request, WebRequest webRequest) { HttpHeaders headers = new HttpHeaders(); HttpStatus status = HttpStatus.BAD_REQUEST; Locale locale = localeResolver.resolveLocale(request); log.info("the local for request is {} and the default is {}" , locale, Locale.getDefault()); String message = messageSource.getMessage(ex.getCode(), null , locale); ApiError apiError = new ApiError(ex.getCode(), message); log.info("The result error of request {} is {}" , request.getServletPath(), ex.getMessage(), ex); return new ResponseEntity(apiError, headers, status); } }
code转化为国际化信息的message也就配置完成了。如果对于其他的在ResponseEntityExceptionHandler
中定义的Exception也需要做国际化信息转化的话,也可以按照上面处理ServiceException
的方法进行定义即可。
源码探究 按照上面的代码操作,已经可以解决需要实现的功能了。这部分将会讲解国际化信息的功能涉及到的源码部分。
Springboot如何获取messages文件生成MessageSource 在Springboot的spring-boot-autoconfig
中配置MessageSource的Properties类为MessageSourceProperties
,该类默认指定了basename为messages。
1 2 3 4 5 6 7 8 package org.springframework.boot.autoconfigure.context;public class MessageSourceProperties { ... private String basename = "messages" ; ... }
下面的MessageSourceAutoConfiguration
为配置MessageSource的实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package org.springframework.boot.autoconfigure.context;public class MessageSourceAutoConfiguration { ... @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()); } messageSource.setAlwaysUseMessageFormat(properties.isAlwaysUseMessageFormat()); messageSource.setUseCodeAsDefaultMessage(properties.isUseCodeAsDefaultMessage()); return messageSource; } ... }
Validation如何使用到ValidationMessages springboot的spring-boot-autoconfigure/.../validation/ValidationAutoConfiguration
为Validator的配置类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 package org.springframework.boot.autoconfigure.validation;@Configuration @ConditionalOnClass (ExecutableValidator.class)@ConditionalOnResource (resources = "classpath:META-INF/services/javax.validation.spi.ValidationProvider" )@Import (PrimaryDefaultValidatorPostProcessor.class)public class ValidationAutoConfiguration { @Bean @Role (BeanDefinition.ROLE_INFRASTRUCTURE) @ConditionalOnMissingBean (Validator.class) public static LocalValidatorFactoryBean defaultValidator () { LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean(); MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory(); factoryBean.setMessageInterpolator(interpolatorFactory.getObject()); return factoryBean; } @Bean @ConditionalOnMissingBean public static MethodValidationPostProcessor methodValidationPostProcessor (Environment environment, @Lazy Validator validator) { MethodValidationPostProcessor processor = new MethodValidationPostProcessor(); boolean proxyTargetClass = environment.getProperty("spring.aop.proxy-target-class" , Boolean.class, true ); processor.setProxyTargetClass(proxyTargetClass); processor.setValidator(validator); return processor; } }
这里重点关注一下MessageInterpolatorFactory
类,该类最后会创建一个org.hibernate.validator.messageinterpolatio.ResourceBundleMessageInterpolator
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 package org.springframework.boot.validation;public class MessageInterpolatorFactory implements ObjectFactory <MessageInterpolator > { private static final Set<String> FALLBACKS; static { Set<String> fallbacks = new LinkedHashSet<>(); fallbacks.add("org.hibernate.validator.messageinterpolation" + ".ParameterMessageInterpolator" ); FALLBACKS = Collections.unmodifiableSet(fallbacks); } @Override public MessageInterpolator getObject () throws BeansException { try { return Validation.byDefaultProvider().configure().getDefaultMessageInterpolator(); } catch (ValidationException ex) { MessageInterpolator fallback = getFallback(); if (fallback != null ) { return fallback; } throw ex; } } ... }
ResourceBundleMessageInterpolator
是AbstractMessageInterpolator
的一个子类,该类定义了validation messages文件的路径,其中org.hibernate.validator.ValidationMessages
为hibernate提供的,而用户自定义的为ValidationMessages
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package org.hibernate.validator.messageinterpolation;public abstract class AbstractMessageInterpolator implements MessageInterpolator { public static final String DEFAULT_VALIDATION_MESSAGES = "org.hibernate.validator.ValidationMessages" ; public static final String USER_VALIDATION_MESSAGES = "ValidationMessages" ; public static final String CONTRIBUTOR_VALIDATION_MESSAGES = "ContributorValidationMessages" ; ... }
在Springboot 2.6.0及之后,对validation进行了修改。如果需要使用,则需要按照如下的方式引入:
1 2 3 4 5 6 7 8 9 10 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > <version > 2.6.2</version > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-validation</artifactId > <version > 2.6.2</version > </dependency >
此外,还对MessageInterpolatorFactory
进行了修改,允许设置外部的MessageSource。还增加了一个MessageSourceMessageInterpolator
来整合messageSource和MessageInterpolator。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 public class MessageInterpolatorFactory implements ObjectFactory <MessageInterpolator > { ... private final MessageSource messageSource; public MessageInterpolatorFactory () { this (null ); } public MessageInterpolatorFactory (MessageSource messageSource) { this .messageSource = messageSource; } @Override public MessageInterpolator getObject () throws BeansException { MessageInterpolator messageInterpolator = getMessageInterpolator(); if (this .messageSource != null ) { return new MessageSourceMessageInterpolator(this .messageSource, messageInterpolator); } return messageInterpolator; } private MessageInterpolator getMessageInterpolator () { try { return Validation.byDefaultProvider().configure().getDefaultMessageInterpolator(); } catch (ValidationException ex) { MessageInterpolator fallback = getFallback(); if (fallback != null ) { return fallback; } throw ex; } } ... }
MessageSourceMessageInterpolator
会优先使用messageSource处理,再通过messageInterpolator处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package org.springframework.boot.validation;class MessageSourceMessageInterpolator implements MessageInterpolator { ... @Override public String interpolate (String messageTemplate, Context context) { return interpolate(messageTemplate, context, LocaleContextHolder.getLocale()); } @Override public String interpolate (String messageTemplate, Context context, Locale locale) { String message = replaceParameters(messageTemplate, locale); return this .messageInterpolator.interpolate(message, context, locale); } ... }
如需了解更多的信息,可以去查看org.springframework.validation.beanvalidation。LocalValidatorFactoryBean
的源码。
参考