SpringBoot国际化和Validation融合
场景
在应用交互时,可能需要根据客户端得语言来返回不同的语言数据。
前端通过参数、请求头等往后端传入locale相关得参数,后端获取参数,根据不同得locale来获取不同得语言得文本信息返回给前端。
实现原理
SpringBoot支持国际化和Validation,主要通过MessageSource接口和Validator实现。
国际化配置
- 编写国际化配置文件,如
messages_en_US.properties
和messages_zh_CN.properties
,并置于resources/i18n
目录下。 - 配置
application.yml
或application.properties
以指定国际化文件的位置,例如spring.messages.basename=i18n/messages
。 - 配置
LocaleResolver
以解析当前请求的locale,常用的实现是AcceptHeaderLocaleResolver
,它通过请求头accept-language
获取当前的locale。
Validation配置
引入spring-boot-starter-validation
依赖以支持Validation功能
1 | org.springframework.bootspring-boot-starter-validation |
配置LocalValidatorFactoryBean
以使用国际化的校验消息,需注入MessageSource
示例
引入依赖
1 | org.springframework.bootspring-boot-starter-validationorg.springframework.bootspring-boot-starter-web |
国际化配置文件
在src/main/resources/i18n
目录下创建两个文件:messages_en_US.properties
和messages_zh_CN.properties
。
1 2 3 4 5 | #messages_en_US.properties welcome.message=Welcome to our website! #messages_zh_CN.properties welcome.message=欢迎来到我们的网站! |
配置MessageSource
在Spring Boot的配置文件中(application.properties
或application.yml
),配置MessageSource
以指定国际化文件的位置。
如果你打算使用Validation的默认国际化文件,你实际上不需要为Validation单独指定文件,因为LocalValidatorFactoryBean
会自动查找ValidationMessages.properties
。
但是,你可以配置自己的国际化文件,并让MessageSource
同时服务于你的应用消息和Validation消息。
1 2 3 | # 国际化文件被放置在src/main/resources/i18n目录下,并以messages为前缀 spring.messages.basename=i18n/messages,org.hibernate.validator.ValidationMessages spring.messages.encoding=utf- 8 |
注意:上面的配置假设你的自定义消息文件位于i18n/messages.properties
,而Validation的默认消息文件是org.hibernate.validator.ValidationMessages.properties
。
实际上,ValidationMessages.properties
文件位于Hibernate Validator的jar包中,所以你不需要显式地将它包含在你的资源目录中。Spring Boot会自动从classpath中加载它。
配置LocalValidatorFactoryBean
在你的配置类中,创建一个LocalValidatorFactoryBean
的bean,并将MessageSource
注入到它中。
这样,LocalValidatorFactoryBean
就会使用Spring的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 | import org.hibernate.validator.HibernateValidator; import org.springframework.context.MessageSource; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; import java.util.Properties; @Configuration public class ValidateConfig { @Bean public LocalValidatorFactoryBean validatorFactoryBean(MessageSource messageSource) { LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean(); factoryBean.setValidationMessageSource(messageSource); // 设置使用 HibernateValidator 校验器 factoryBean.setProviderClass(HibernateValidator. class ); // 设置 快速异常返回 只要有一个校验错误就立即返回失败,其他参数不在校验 Properties properties = new Properties(); properties.setProperty( "hibernate.validator.fail_fast" , "true" ); factoryBean.setValidationProperties(properties); // 加载配置 factoryBean.afterPropertiesSet(); return factoryBean; } } |
使用校验
1 2 3 4 5 6 7 8 9 | import javax.validation.constraints.NotNull; public class MyModel { @NotNull(message = "{not.null.message}") private String field; // getters and setters } |
messages.properties
文件中,你可以添加
1 | not. null .message=This field cannot be null . |
而在Hibernate Validator的ValidationMessages.properties
文件中,已经包含了默认的校验消息,如{javax.validation.constraints.NotNull.message}
的值。
自定义校验
-
定义约束注解:创建一个注解,用
@Constraint
标记,并定义message
、groups
和payload
属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | import javax.validation.Constraint; import javax.validation.Payload; import java.lang.annotation.*; @Documented @Target ({ElementType.PARAMETER, ElementType.FIELD, ElementType.TYPE}) @Retention (RetentionPolicy.RUNTIME) @Constraint (validatedBy = MyValidateContent. class ) public @interface MyValidate { String message() default "" ; Class>[] groups() default {}; Class extends Payload>[] payload() default {}; } |
- 实现约束验证器**:创建一个实现了
ConstraintValidator
接口的类,并重写isValid
方法 - 在
isValid
方法中使用ConstraintValidatorContext
**:如果验证失败,使用ConstraintValidatorContext
的buildConstraintViolationWithTemplate
方法来构建ConstraintViolation
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | import com.example.dto.ParamVo; import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; public class MyValidateContent implements ConstraintValidator { @Override public void initialize(MyConstraint constraintAnnotation) { // 初始化代码(如果需要的话) } @Override public boolean isValid(ParamVo paramVo, ConstraintValidatorContext constraintValidatorContext) { if ( "N" .equals(paramVo.getSex())) { if (paramVo.getAge() |
在这个例子中,如果sex
是N
并且age
小于18
,验证器将使用ConstraintValidatorContext
来构建一个带有错误消息的ConstraintViolation
。
消息模板"{template1}"
将会在验证失败时被解析,并替换为你在MyValidate
注解中定义的默认消息或你在messages.properties
文件中定义的国际化消息。
确保你的MyValidate
注解定义了一个message
属性,并且你在messages.properties
文件中有一个对应的条目例如:
1 2 | template1=男性要大于 18 template2=女性要大于 20 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | import com.example.validate.MyValidate; import lombok.Getter; import lombok.Setter; import org.hibernate.validator.constraints.Length; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; @Getter @Setter @MyValidate public class ParamVo { @NotBlank (message = "{javax.validation.constraints.NotNull.message}" ) private String sex; @NotNull (message = "age 不能为空" ) private Integer age; @NotBlank (message = "{name.not.null}" ) @Length (max = 3 ,message = "{name.length.max}" ) private String name; } |
Controller层异常处理
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 | import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.validation.BindingResult; import org.springframework.validation.FieldError; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import java.util.HashMap; import java.util.Map; @RestControllerAdvice public class GlobalException { @ExceptionHandler (MethodArgumentNotValidException. class ) public ResponseEntity<object data-origwidth= "" data-origheight= "" style= "width: 1264px;" > handleValidationExceptions(MethodArgumentNotValidException ex) { Map errors = new HashMap(); BindingResult result = ex.getBindingResult(); for (FieldError error : result.getFieldErrors()) { errors.put(error.getField(), error.getDefaultMessage()); } // 这里可以根据实际需求定制返回的错误信息结构 Map response = new HashMap(); response.put( "status" , HttpStatus.BAD_REQUEST.value()); response.put( "errors" , errors); return new ResponseEntity(response, HttpStatus.BAD_REQUEST); } } </object> |
内部方法校验
1 2 3 4 5 6 7 8 9 | import org.springframework.validation.annotation.Validated; import javax.validation.constraints.Min; import javax.validation.constraints.NotNull; //@Validated @Validated public interface ServiceIntface { //校验返回值,校验入参 @NotNull Object hello( @NotNull @Min ( 10 ) Integer id, @NotNull String name); } |
1 2 3 4 5 6 7 8 9 10 11 | import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @Slf4j @Service public class ServiceImpl implements ServiceIntface { @Override public Object hello(Integer id, String name) { return null ; } } |
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 | 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 javax.validation.ConstraintViolation; import javax.validation.ConstraintViolationException; import java.util.HashMap; import java.util.Map; import java.util.Set; @RestControllerAdvice public class GlobalException { @ExceptionHandler (ConstraintViolationException. class ) public ResponseEntity<object data-origwidth= "" data-origheight= "" style= "width: 1264px;" > handleValidationExceptions(ConstraintViolationException ex) { Map errors = new HashMap(); Set> constraintViolations = ex.getConstraintViolations(); for (ConstraintViolation> constraintViolation : constraintViolations) { String key = constraintViolation.getPropertyPath().toString(); String message = constraintViolation.getMessage(); errors.put(key, message); } // 这里可以根据实际需求定制返回的错误信息结构 Map response = new HashMap(); response.put( "status" , HttpStatus.BAD_REQUEST.value()); response.put( "errors" , errors); return new ResponseEntity(response, HttpStatus.BAD_REQUEST); } } </object> |
- 校验写在接口上的,抛出异常
javax.validation.ConstraintViolationException
- 校验写在具体实现,抛出异常
javax.validation.ConstraintDeclarationException
注意点
代码中国际化使用
代码里响应,手动获取使用MessageSource的getMessage方法即可,也就是spring容器中的getMessage()
1 2 3 4 5 6 7 8 | # messages_en_US.properties welcome.message=Welcome to our website! # messages_zh_CN.properties welcome.message=欢迎来到我们的网站! #定义消息,并使用占位符{ 0 }、{ 1 }等表示参数位置 #welcome.message=欢迎{ 0 }来到{ 1 } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | //创建一个配置类来配置LocaleResolver,以便根据请求解析当前的语言环境: 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.WebMvcConfigurer; import org.springframework.web.servlet.i18n.SessionLocaleResolver; import java.util.Locale; @Configuration public class WebConfig implements WebMvcConfigurer { @Bean public LocaleResolver localeResolver() { SessionLocaleResolver sessionLocaleResolver = new SessionLocaleResolver(); sessionLocaleResolver.setDefaultLocale(Locale.US); // 设置默认语言 return sessionLocaleResolver; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | //创建一个控制器来使用国际化的消息 import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.MessageSource; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletRequest; import java.util.Locale; @RestController @RequestMapping ( "/hello" ) public class HelloController { @Autowired private MessageSource messageSource; @GetMapping public String hello(HttpServletRequest request) { Locale locale = (Locale) request.getAttribute(org.springframework.web.servlet.LocaleResolver.LOCALE_RESOLVER_ATTRIBUTE); //messageSource.getMessage("welcome.message", new Object[]{"张三", "中国"}, Locale.CHINA)。 return messageSource.getMessage( "welcome.message" , null , locale); } } |
Locale获取
默认情况下spring注册的messageSource对象为ResourceBundleMessageSource,会读取spring.message
配置。
请求中Locale的获取是通过LocaleResolver
进行处理,默认是AcceptHeaderLocaleResolver
,通过WebMvcAutoConfiguration
注入,从Accept-Language
请求头中获取locale信息。
此时前端可以在不同语言环境时传入不同的请求头Accept-Language即可达到切换语言的效果
1 2 | Accept-Language: en-Us Accept-Language: zh-CN |
默认情况下前端请求中的不用处理,如果约定其他信息传递Local,使用自定义的I18nLocaleResolver替换默认的AcceptHeaderLocaleResolver
,重写resolveLocale
方法就可以自定义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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | import cn.hutool.core.util.StrUtil; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.LocaleResolver; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.Locale; /** * */ @Configuration public class I18nConfig { @Bean public LocaleResolver localeResolver() { return new I18nLocaleResolver(); } /** * 获取请求头国际化信息 * 使用自定义的I18nLocaleResolver替换默认的AcceptHeaderLocaleResolver,重写resolveLocale方法就可以自定义Locale的解析逻辑。 * * 自定义后使用content-language传Locale信息,使用_划分语言个地区。 * content-language: en_US * content-language: zh_CN */ static class I18nLocaleResolver implements LocaleResolver { @Override public Locale resolveLocale(HttpServletRequest httpServletRequest) { String language = httpServletRequest.getHeader( "content-language" ); Locale locale = Locale.getDefault(); if (StrUtil.isNotBlank(language)) { String[] split = language.split( "_" ); locale = new Locale(split[ 0 ], split[ 1 ]); } return locale; } @Override public void setLocale(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Locale locale) { } } } |
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持IT俱乐部。