SpringBoot + Hibernate Validator + I18N

在日常开发中的国际化是比较常见的,对于springboot的web项目结合Hibernate Validator验证框架的国际化配置也是经常使用到的,本文就是这种场景的一种实现,下面是具体实现代码

本项目使用 jdk-1.8 + springboot-2.5.2 + hutool-5.7.5 + lombok-1.18.20

具体实现

  1. 项目依赖
<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>
  1. 参数实体类
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;
}
  1. 测试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;
    }

}
  1. 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;
    }
}
  1. 系统配置
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());
    }
}
  1. 配置国际化信息配置文件,在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
  1. 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
  1. 启动程序,请求接口
    参数