Spring Boot Validator以及国际化(i18n)

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/choimroc/article/details/101880225

最近在研究Spring Boot 的Validator以及国际化,在墙里墙外找了很多很久,可能是因为版本的更新迭代,找到的资料基本都用不了了。自己折腾了半天,终于琢磨出来了,特此记录。觉得过程啰嗦的可以直奔demo代码

本文代码地址:https://github.com/choimroc/SpringBoot_i18nDemo

本文实验的软件环境如下:

  • Spring Boot 2.1.8.RELEASE
  • JDK 1.8
  • IDEA 2019.2.3
  • Gradle 5.6.2

1 Validator的使用

Validator的具体用法不是本文的重点,网上也已经有大量的相关文章,所以就不展开说了。这里要提一下的是hibernate-validator包下的注解,在5.0版本之后就已经标记过时(@deprecated)了,hibernate-validator也是建议使用javax.validation 包下的注解来替换,用法都差不多。


2 Validator的配置

在很多文章中都出现过下面的这两种配置(来自hibernate-validator官方文档

import org.hibernate.validator.HibernateValidator;
import org.hibernate.validator.cfg.ConstraintMapping;

import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;

//这样
ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
            .configure()
            .failFast(true)
            .addMapping((ConstraintMapping) null)
            .buildValidatorFactory();
Validator validator = validatorFactory.getValidator();

//或者这样
ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
            .configure()
            .addProperty("hibernate.validator.fail_fast", "true")
            .buildValidatorFactory();
Validator validator = validatorFactory.getValidator();

这里说一下failFast的作用,在Validator进行校验时,会校验所有的参数,然后把错误一起返回,如下:

{
    "timestamp": 1570029617189,
    "status": 500,
    "error": "Internal Server Error",
    "message": "test.param1: 参数1不能为空, test.param2: 参数2不能为空",
    "path": "/test"
}

而设置了failFast之后,如果校验时发现了错误就会停止后续参数的校验。

我们可以看到上面的配置使用的javax.validation包下的Validator,但是在我所使用的Spring Boot版本中,实际使用的是org.springframework.validation包下的Validator,具体可以查看源码里org.springframework.web.servlet.config.annotation.WebMvcConfigurer这个接口的代码。所以上面所用的配置已经不适用了(PS:具体是什么时候替换的我也不清楚,了解情况的同学可以在留言区告知一声,感谢~)。

查看了org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration,发现defaultValidator用的是org.springframework.validation.beanvalidation.LocalValidatorFactoryBean,但是LocalValidatorFactoryBean下并没有configure和failFast。难道新版本之后就没办法设置了?对比了LocalValidatorFactoryBeanjavax.validation后发现LocalValidatorFactoryBean有一个很可疑的属性validationPropertyMap,于是有了下面的配置

@Configuration
@EnableWebMvc
public class WebConfiguration implements WebMvcConfigurer {

	@Bean
	public LocalValidatorFactoryBean mvcValidator() {
	    LocalValidatorFactoryBean localValidatorFactoryBean = new LocalValidatorFactoryBean();
	    localValidatorFactoryBean.getValidationPropertyMap().put("hibernate.validator.fail_fast", "true");
	    return localValidatorFactoryBean;
	}
}

运行之后,发起请求,发现真的起作用了,nice!

注意,如果直接运行会抛出异常信息Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overriding=true,还需要在application.yml或者application.properties中加入如下配置

# yml
spring:
  main:
    allow-bean-definition-overriding: true

# properties
spring.main.allow-bean-definition-overriding=true

3 Validator的全局异常捕获

上面的返回格式是Spring Boot自带的,而且HttpStatus是500,如果我们想要返回自定义的格式,就得做全局异常捕获。废话不多说,直接上代码:

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ResponseStatus(HttpStatus.OK)
    @ExceptionHandler(BindException.class)
    public Result bindExceptionHandler(final BindException e) {
        String message = e.getBindingResult().getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining());
        return new Result(500, message);
    }


    @ResponseStatus(HttpStatus.OK)
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result handler(final MethodArgumentNotValidException e) {
        String message = e.getBindingResult().getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining());
        return new Result(500, message);
    }

    @ResponseStatus(HttpStatus.OK)
    @ExceptionHandler(ConstraintViolationException.class)
    public Result handler(final ConstraintViolationException e) {
        String message = e.getConstraintViolations().stream().map(ConstraintViolation::getMessage).collect(Collectors.joining());
        return new Result(500, message);
    }

}

返回的数据如下:

{
    "code": 500,
    "msg": "参数1不能为空"
}

4 Validator和国际化

直奔主题,常规的配置如下:

@Configuration
@EnableWebMvc
public class WebConfiguration implements WebMvcConfigurer {
    public WebConfiguration() {
    }

    /*国际化 start*/
    @Bean
    public LocaleChangeInterceptor localeChangeInterceptor() {
        LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();
        //自定义参数
        localeChangeInterceptor.setParamName("language");
        return localeChangeInterceptor;
    }

    @Bean
    public LocaleResolver localeResolver() {
        SessionLocaleResolver localeResolver = new SessionLocaleResolver();
        //指定默认语言为中文
        localeResolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
        return localeResolver;
    }

    @Bean
    public LocalValidatorFactoryBean mvcValidator() {
        LocalValidatorFactoryBean localValidatorFactoryBean = new LocalValidatorFactoryBean();
        localValidatorFactoryBean.getValidationPropertyMap().put("hibernate.validator.fail_fast", "true");
        //为Validator配置国际化
        localValidatorFactoryBean.setValidationMessageSource(resourceBundleMessageSource());
        return localValidatorFactoryBean;
    }


    @Bean(name = "messageSource")
    public ResourceBundleMessageSource resourceBundleMessageSource() {
        ResourceBundleMessageSource resourceBundleMessageSource = new ResourceBundleMessageSource();
        //指定国际化的Resource Bundle地址
        resourceBundleMessageSource.setBasename("i18n/messages");
        //指定国际化的默认编码
        resourceBundleMessageSource.setDefaultEncoding("UTF-8");
        return resourceBundleMessageSource;
    }


    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(localeChangeInterceptor());
    }

    /*国际化 end*/
}

这样就能完成国际化了,是不是 so easy?!但是这样写有一个问题,就是language参数必须是url参数,如http://localhost:8080/test?language=en_US,一般对于这种公共的参数,我们都希望放在Headers中的,那有没有办法呢?
我们首先来查看一下org.springframework.web.servlet.i18n.LocaleChangeInterceptor这个类的源码,这里面有一个关键的方法preHandle

//这里只贴出关键代码,有兴趣的可以自己去研究
public class LocaleChangeInterceptor extends HandlerInterceptorAdapter {
	……
	
	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws ServletException {
		//正是这一行代码,导致只能通过url参数传递language
		String newLocale = request.getParameter(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()) {
						if (logger.isDebugEnabled()) {
							logger.debug("Ignoring invalid locale value [" + newLocale + "]: " + ex.getMessage());
						}
					}
					else {
						throw ex;
					}
				}
			}
		}
		// Proceed in any case.
		return true;
	}
	……
	
}

既然找到了源头那就好办了,我们只需要继承源码中的这个类,依然画葫芦,重写关键方法就行了;在源码中,有个checkHttpMethod方法是用private标识的,所以我们同样要复制一份:

public class LocaleInterceptor 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()) {
                        if (logger.isDebugEnabled()) {
                            logger.debug("Ignoring invalid locale value [" + newLocale + "]: " + ex.getMessage());
                        }
                    } else {
                        throw ex;
                    }
                }
            }
        }
        // Proceed in any case.
        return true;
    }

	//复制自源码中
    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;
    }
}

//修改WebConfiguration

@Configuration
@EnableWebMvc
public class WebConfiguration implements WebMvcConfigurer {

    @Bean
    public LocaleChangeInterceptor localeChangeInterceptor() {
        //改为我们自定义的LocaleInterceptor
        LocaleChangeInterceptor localeChangeInterceptor = new LocaleInterceptor();
        //指定参数
        localeChangeInterceptor.setParamName("language");
        return localeChangeInterceptor;
    }
}

这才是真正的so easy!同学们,这说明什么问题呀?这说明只要能够充分利用源码,我们就可以只做出些许修改就能达到目的,可谓是四两拨千斤呀!

然后我们再封装一个获取国际化资源的工具类:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.stereotype.Component;

import java.util.Locale;

@Component
public class LocaleMessage {
    private final MessageSource messageSource;

    @Autowired
    public LocaleMessage(MessageSource messageSource) {
        this.messageSource = messageSource;
    }

    public String getMessage(String code) {
        return getMessage(code, new Object[]{});
    }

    public String getMessage(String code, String defaultMessage) {
        return getMessage(code,  new Object[]{}, defaultMessage);
    }

    public String getMessage(String code, String defaultMessage, Locale locale) {
        return getMessage(code,  new Object[]{}, defaultMessage, locale);
    }

    public String getMessage(String code, Locale locale) {
        return getMessage(code,  new Object[]{}, "", locale);
    }

    public String getMessage(String code, Object[] args) {
        return getMessage(code, args, "");
    }

    public String getMessage(String code, Object[] args, Locale locale) {
        return getMessage(code, args, "", locale);
    }

    public String getMessage(String code, Object[] args, String defaultMessage) {
        Locale locale = LocaleContextHolder.getLocale();
        return getMessage(code, args, defaultMessage, locale);
    }

    public String getMessage(String code, Object[] args, String defaultMessage, Locale locale) {
        return messageSource.getMessage(code, args, defaultMessage, locale);
    }

}

使用:

1.在src\main\resources下新建文件夹i18n,在i18n新建Resource Bundle,命名为messages
新建Resource Bundle
2.添加中文和英文



在这里插入图片描述
3.添加一些数据

4.在Controller中使用

@Validated
@RestController
@RequestMapping("test")
public class TestController {
    private final LocaleMessage localeMessage;

    @Autowired
    public TestController(LocaleMessage localeMessage) {
        this.localeMessage = localeMessage;
    }

    @PostMapping
    public Result test(
            @NotBlank(message = "{test.msg1}") String param1,
            @NotBlank(message = "{test.msg2}") String param2
    ) {
        return new Result(200, localeMessage.getMessage("test.msg3"));
    }
}

记录,分享,交流。

猜你喜欢

转载自blog.csdn.net/choimroc/article/details/101880225