SpringBoot + Hibernate Validator + I18N
在日常开发中的国际化是比较常见的,对于springboot的web项目结合Hibernate Validator验证框架的国际化配置也是经常使用到的,本文就是这种场景的一种实现,下面是具体实现代码
本项目使用 jdk-1.8 + springboot-2.5.2 + hutool-5.7.5 + lombok-1.18.20
具体实现
- 项目依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.2</version>
<relativePath/>
</parent>
<groupId>com.butioy.test</groupId>
<artifactId>i18n-test</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>i18n-test</name>
<description>i18n test</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>
<version>5.7.5</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
- 参数实体类
package com.butioy.test.request;
import cn.hutool.core.lang.RegexPool;
import java.io.Serializable;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.Accessors;
@Setter
@Getter
@ToString
@Accessors(chain = true)
public class ValidateI18NTestRequest implements Serializable {
private static final long serialVersionUID = 1393188957993676667L;
@NotBlank(message = "{valid.test.text.blank}")
@Size(min = 2, message = "{valid.test.text.min_limit}")
private String text;
@NotBlank(message = "{valid.test.email.blank}")
@Email(regexp = RegexPool.EMAIL, message = "{valid.test.email.illegal_format}")
private String email;
}
- 测试Controller
package com.butioy.test.controller;
import com.butioy.test.request.ValidateI18NTestRequest;
import com.butioy.test.user.dto.UserDto;
import com.butioy.test.user.request.UserRequest;
import com.butioy.test.user.service.IUserService;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
import javax.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
@RequestMapping("/i18n")
@RequiredArgsConstructor
public class I18NTestController {
@PostMapping("/test")
public Map<String, Object> createUser(@Valid @RequestBody ValidateI18NTestRequest req, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
String errorMsg = bindingResult.getAllErrors().stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.collect(Collectors.joining(","));
return wrapper(20000, errorMsg, null);
}
return wrapper(10000, "ok", req);
}
private Map<String, Object> wrapper(int code, String msg, Object data) {
Map<String, Object> result = new HashMap<>(4);
result.put("code", code);
result.put("msg", msg);
result.put("data", data);
return result;
}
}
- Spring Interceptor,因为Spring Web自带的 LocaleChangeInterceptor 是从Request中获取国际化参数,实际应用中一般是从Header中获取国际化参数,所以需要自定义Interceptor,继承 Spring Web 自带的 LocaleChangeInterceptor。
package com.butioy.test.config;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
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;
@Slf4j
public class I18NInterceptor extends LocaleChangeInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException {
//getParameter 改为 getHeader
String newLocale = request.getHeader(getParamName());
if (newLocale != null) {
if (checkHttpMethod(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(newLocale));
} catch (IllegalArgumentException ex) {
if (isIgnoreInvalidLocale()) {
log.debug("Ignoring invalid locale value [" + newLocale + "]: " + ex.getMessage());
} else {
throw ex;
}
}
}
return true;
} else {
// 如果从Header中未获取到国际化参数值,则调用Spring的默认实现获取国际化参数值
return super.preHandle(request, response, handler);
}
}
private boolean checkHttpMethod(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.butioy.test.config;
import java.util.Locale;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.validation.MessageInterpolatorFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
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;
@Configuration
@RequiredArgsConstructor
public class SystemConfig implements WebMvcConfigurer {
/**
* 国际化的参数名
*/
private static final String LANGUAGE_PARAM_NAME = "lang";
private final ResourceBundleMessageSource resourceBundleMessageSource;
@Bean
public LocaleResolver localeResolver() {
SessionLocaleResolver localeResolver = new SessionLocaleResolver();
//指定默认语言为中文
localeResolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
return localeResolver;
}
@Bean
public LocalValidatorFactoryBean localValidatorFactoryBean() {
LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory();
factoryBean.setMessageInterpolator(interpolatorFactory.getObject());
// 设置快速失败,Hibernate 验证框架默认验证所有字段设置的所有规则,并返回错误集合。
// 快速失败则是只要验证时出现一个错误,立马返回,不执行后面的验证规则
factoryBean.getValidationPropertyMap().put("hibernate.validator.fail_fast", "true");
//为Validator配置国际化
factoryBean.setValidationMessageSource(resourceBundleMessageSource);
return factoryBean;
}
@Bean
public LocaleChangeInterceptor i18nInterceptor() {
LocaleChangeInterceptor interceptor = new I18NInterceptor();
interceptor.setParamName(LANGUAGE_PARAM_NAME);
return interceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(i18nInterceptor());
}
}
- 配置国际化信息配置文件,在resources目录下新建一个i18n目录,在目录中创建3个properties配置文件,分别是messages.properties, messages_zh_CN.properties、messages_en_US.properties,其中的messages.properties是默认的国际化信息,其它两个分别对ing中文简体和美国英语两个语种
# 这里默认国际化为中文简体,所以 messages.properties 和 messages_zh_CN.properties 的内容一致
valid.test.text.blank=文本内容不能为空
valid.test.text.min_limit=文本内容字符长度不能小于2
valid.test.email.blank=邮箱不能为空
valid.test.email.illegal_format=邮箱格式不合法
# messages_en_US.properties 的内容
valid.test.text.blank=text cannot be not blank
valid.test.text.min_limit=text must contain at least two characters
valid.test.email.blank=email cannot be not blank
valid.test.email.illegal_format=email format is invalid
- SpringBoot 配置文件 application.properties
# 端口号
server.port=8081
# 必须设置为true,让自定义配置的Bean覆盖SpringBoot自动配置的Bean
spring.main.allow-bean-definition-overriding=true
# 应用名称
spring.application.name=i18n-test
# 指定国际化的Resource Bundle地址。
# spring 默认的国际化信息配置文件是在classpath下,这里是放在classpath下的i18n目录中,所以需要配置该项
spring.messages.basename=i18n/messages
- 启动程序,请求接口
参数