在做web相关的应用时,经常需要提供接口与用户交互(获取数据、上传数据等),由于这个过程需要用户进行相关的操作,为了避免出现一些错误的数据等,一般需要对数据进行校验,随着接口的增多,校验逻辑的冗余度也越来越大,虽然可以通过抽象出校验的方法来处理,但还是需要每次手动调用校验逻辑,相对来说还是不方便。
为了解决这个问题,Java中提供了Bean Validation的标准,该标准规定了校验的具体内容,通过简单的注解就能完成必要的校验逻辑了,相对来说就方便了很多,而该规范其实只是规范,并没有具体的实现,Hibernate提供了具体的实现,也即Hibernate Validator,这个也是目前使用得比较多的验证器了。
Bean Validation 1.0(JSR-303)
http://jcp.org/en/jsr/detail?id=303
Bean Validation 1.1(JSR-349)
http://jcp.org/en/jsr/detail?id=349.
@Valid是使用hibernate validation的时候使用
java的jsr303声明了这类接口,hibernate-validator对其进行了实现
@Validated 是只用spring Validator 校验机制使用
配置ValidatorConfiguration
package com.example.validation.config;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import org.hibernate.validator.HibernateValidator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;
@Configuration
public class ValidatorConfiguration {
/**
* Method: 开启快速返回
* Description: 如果参数校验有异常,直接抛异常,不会进入到 controller,使用全局异常拦截进行拦截
*
* @param
* @return org.springframework.validation.beanvalidation.MethodValidationPostProcessor
*/
@Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
MethodValidationPostProcessor postProcessor = new MethodValidationPostProcessor();
postProcessor.setValidator(validator());
return postProcessor;
}
@Bean
public Validator validator(){
ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
.configure()
.failFast( true )
.buildValidatorFactory();
Validator validator = validatorFactory.getValidator();
return validator;
}
}
package com.example.validation.web.advice;
import java.util.List;
import java.util.stream.Collectors;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
@Component
@ControllerAdvice
public class GlobalExceptionHandlerAdvice {
@ResponseBody
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResultInfo<?> validationErrorHandler(MethodArgumentNotValidException ex) {
// 同样是获取BindingResult对象,然后获取其中的错误信息
// 如果前面开启了fail_fast,事实上这里只会有一个信息
//如果没有,则可能又多个
List<String> errorInformation = ex.getBindingResult().getAllErrors()
.stream()
.map(ObjectError::getDefaultMessage)
.collect(Collectors.toList());
return new ResultInfo<>(400, errorInformation.toString(), null);
}
@ResponseBody
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(ConstraintViolationException.class)
public ResultInfo<?> validationErrorHandler(ConstraintViolationException ex) {
List<String> errorInformation = ex.getConstraintViolations()
.stream()
.map(ConstraintViolation::getMessage)
.collect(Collectors.toList());
return new ResultInfo<>(400, errorInformation.toString(), null);
}
}
package com.example.validation.web.advice;
public class ResultInfo<T> {
private int code;
private String message;
private T body;
public ResultInfo(int code, String message, T body) {
this.code = code;
this.message = message;
this.body = body;
}
public ResultInfo(int code, String message) {
this(code, message, null);
}
public ResultInfo(String message) {
this(200, message, null);
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public T getBody() {
return body;
}
public void setBody(T body) {
this.body = body;
}
}
GET参数校验(@RequestParam参数校验)
RequestParam参数的校验需要使用 @Validated 注解,进行支持,该注解可以注解在类、方法、参数上
package com.example.validation.web;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.example.validation.web.dto.User;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
@RestController("testController")
@Api(tags = "用户管理")
@RequestMapping("/api/user/")
@Validated //使用spring的validation
public class UserController {
@GetMapping("/id/{userId}")
@ApiOperation(value = "用户管理-查询用户-根据userId查询", code = 200)
public User getUserByUserId(@PathVariable String userId) {
User user = new User();
user.setUserId(userId);
return user;
}
/**
* PathVariable启用注解
* RequestParam启用注解
* @param name
* @param params
* @return
*/
@GetMapping("/name/{name}")
@ApiOperation(value = "用户管理-查询用户-根据userName查询", code = 200)
public User getUserByName(
@NotNull
@Size(min = 1, max = 20, message = "用户名格式有误")
@PathVariable String name,
@NotNull
@Size(min = 1, max = 20, message = "params用户名格式有误")
@RequestParam String params) {
User user = new User();
user.setUserId("userName," + params);
// user.setName(name);
return user;
}
@PostMapping()
@ApiOperation(value = "用户管理-添加用户", code = 200)
public User save(@RequestBody @Valid User user) {
return user;
}
}
效果
model校验
package com.example.validation.web.dto;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import com.example.validation.web.validator.CaseMode;
import com.example.validation.web.validator.CheckCase;
public class User {
@Size(min = 6,max = 16,message = "userId长度")
@NotNull(message = "userId长度不能为空")
private String userId;
@CheckCase(value = CaseMode.LOWER,message = "userName必须是小写")
private String userNameLower;
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getUserNameLower() {
return userNameLower;
}
public void setUserNameLower(String userNameLower) {
this.userNameLower = userNameLower;
}
}
package com.example.validation.web;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.example.validation.web.dto.User;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
@RestController("testController")
@Api(tags = "用户管理")
@RequestMapping("/api/user/")
@Validated //使用spring的validation
public class UserController {
@GetMapping("/id/{userId}")
@ApiOperation(value = "用户管理-查询用户-根据userId查询", code = 200)
public User getUserByUserId(@PathVariable String userId) {
User user = new User();
user.setUserId(userId);
return user;
}
/**
* PathVariable启用注解
* RequestParam启用注解
* @param name
* @param params
* @return
*/
@GetMapping("/name/{name}")
@ApiOperation(value = "用户管理-查询用户-根据userName查询", code = 200)
public User getUserByName(
@NotNull
@Size(min = 1, max = 20, message = "用户名格式有误")
@PathVariable String name,
@NotNull
@Size(min = 1, max = 20, message = "params用户名格式有误")
@RequestParam String params) {
User user = new User();
user.setUserId("userName," + params);
// user.setName(name);
return user;
}
@PostMapping()
@ApiOperation(value = "用户管理-添加用户", code = 200)
public User save(@RequestBody @Valid User user) {
return user;
}
}
自定义校验器
package com.example.validation.web.validator;
public enum CaseMode {
UPPER,
LOWER;
}
package com.example.validation.web.validator;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
@Target( { ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = CheckCaseValidator.class)
@Documented
public @interface CheckCase {
String message() default "";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
CaseMode value();
}
package com.example.validation.web.validator;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class CheckCaseValidator implements ConstraintValidator<CheckCase, String> {
private CaseMode caseMode;
public void initialize(CheckCase checkCase) {
this.caseMode = checkCase.value();
}
public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
if (s == null) {
return true;
}
if (caseMode == CaseMode.UPPER) {
return s.equals(s.toUpperCase());
} else {
return s.equals(s.toLowerCase());
}
}
}
效果
使用资源文件配置错误提示 和 国际化
- 国际化配置
package com.example.validation.config;
import java.util.Locale;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
import org.springframework.web.servlet.i18n.SessionLocaleResolver;
@Configuration
@EnableAutoConfiguration
@ComponentScan
public class MessageSourceAndLocaleConfiguration extends WebMvcConfigurerAdapter{
@Value("i18n/messages_validate")
private String i18nMessages;
@Bean(name = "messageSource")
public ResourceBundleMessageSource getMessageSource() throws Exception {
ResourceBundleMessageSource resourceBundleMessageSource = new ResourceBundleMessageSource();
resourceBundleMessageSource.setDefaultEncoding("UTF-8");
// resourceBundleMessageSource.setBasenames("i18n/messages_validate");
resourceBundleMessageSource.setBasenames(i18nMessages);
return resourceBundleMessageSource;
}
@Bean
public LocaleResolver localeResolver() {
SessionLocaleResolver slr = new SessionLocaleResolver();
// 默认语言
slr.setDefaultLocale(Locale.CHINA);
return slr;
}
@Bean
public LocaleChangeInterceptor localeChangeInterceptor() {
LocaleChangeInterceptor lci = new LocaleChangeInterceptor();
// 参数名
lci.setParamName("lang");
return lci;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(localeChangeInterceptor());
}
}
- 配置validation处理拦截器和资源文件地址
package com.example.validation.config;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import javax.validation.Validator;
import org.hibernate.validator.messageinterpolation.ResourceBundleMessageInterpolator;
import org.hibernate.validator.resourceloading.PlatformResourceBundleLocator;
import org.hibernate.validator.spi.resourceloading.ResourceBundleLocator;
import org.springframework.beans.factory.annotation.Autowired;
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.validation.beanvalidation.MethodValidationPostProcessor;
@Configuration
public class ValidatorConfiguration {
@Autowired
ResourceBundleMessageSource messageSource;
/**
* Method: 开启快速返回
* Description: 如果参数校验有异常,直接抛异常,不会进入到 controller,使用全局异常拦截进行拦截
*
* @param
* @return org.springframework.validation.beanvalidation.MethodValidationPostProcessor
*/
@Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
MethodValidationPostProcessor postProcessor = new MethodValidationPostProcessor();
postProcessor.setValidator(validator());
return postProcessor;
}
@Bean
public Validator validator(){
/*ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
.configure()
.messageInterpolator(new ResourceBundleMessageInterpolator(new PlatformResourceBundleLocator("i18n/messages_validate" )))
// 将fail_fast设置为true即可,如果想验证全部,则设置为false或者取消配置即可
.failFast( true )
.buildValidatorFactory();
Validator validator = validatorFactory.getValidator();
return validator;*/
LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
// 方式一
// try {
// validator.setValidationMessageSource(messageSource);
// } catch (Exception e) {
// e.printStackTrace();
// }
validator.setMessageInterpolator(new MessageInterpolator(new PlatformResourceBundleLocator("i18n/messages_validate" )));
// 方式二
return validator;
}
private class MessageInterpolator extends ResourceBundleMessageInterpolator {
@SuppressWarnings("unused")
MessageInterpolator(){
}
MessageInterpolator(ResourceBundleLocator resourceBundleLocator){
super(resourceBundleLocator);
}
@Override
public String interpolate(String message, Context context, Locale locale) {
// 获取注解类型
String annotationTypeName = context.getConstraintDescriptor().getAnnotation().annotationType().getSimpleName();
// 根据注解类型获取自定义的消息Code
String annotationDefaultMessageCode = VALIDATION_ANNATATION_DEFAULT_MESSAGES.get(annotationTypeName);
if (null != annotationDefaultMessageCode && !message.startsWith("javax.validation")
&& !message.startsWith("org.hibernate.validator.constraints")) {
// 如果注解上指定的message不是默认的javax.validation或者org.hibernate.validator等开头的情况,
// 则需要将自定义的消息Code拼装到原message的后面;
message += "{" + annotationDefaultMessageCode + "}";
}
return super.interpolate(message, context, locale);
}
}
private static final Map<String, String> VALIDATION_ANNATATION_DEFAULT_MESSAGES =
new HashMap<String, String>(20) {{
put("CheckCase", "demo.validation.constraints.CheckCase.LOWER.message");
put("NotNull", "demo.validation.constraints.NotNull.message");
}};
}
- 使用
package com.example.validation.web.dto;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import com.example.validation.web.validator.CaseMode;
import com.example.validation.web.validator.CheckCase;
public class User2 {
@Size(min = 6,max = 16)
@NotNull(message = "{userId}")
private String userId;
@CheckCase(value = CaseMode.LOWER,message = "{userName}")
private String userNameLower;
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getUserNameLower() {
return userNameLower;
}
public void setUserNameLower(String userNameLower) {
this.userNameLower = userNameLower;
}
}
- 在UserController配置一个语言切换的端点
@GetMapping("/i18n")
public String changeSessionLanauage(HttpServletRequest request, String lang){
System.out.println(lang);
if("zh_CN".equals(lang)){
//代码中即可通过以下方法进行语言设置
request.getSession().setAttribute(SessionLocaleResolver.LOCALE_SESSION_ATTRIBUTE_NAME,new Locale("zh","CN"));
}else if("en_US".equals(lang)){
request.getSession().setAttribute(SessionLocaleResolver.LOCALE_SESSION_ATTRIBUTE_NAME,new Locale("en","US"));
}
return "hello";
}
- 测试
切换语言
再次验证
进一步通过反射优化使用时的配置
现在使用检测时,字段名称的国际化仍然需要指定国际化属性文件的key值
@CheckCase(value = CaseMode.LOWER,message = "{userName}")
private String userNameLower;
可以通过一定的规则(如字段名去属性文件中查找),进一步简化成
@CheckCase(value = CaseMode.LOWER)
private String userNameLower;
待进一步补充
JSR303定义的校验类型
空检查
@Null 验证对象是否为null
@NotNull 验证对象是否不为null, 无法查检长度为0的字符串
@NotBlank 检查约束字符串是不是Null还有被Trim的长度是否大于0,只对字符串,且会去掉前后空格.
@NotEmpty 检查约束元素是否为NULL或者是EMPTY.
Booelan检查
@AssertTrue 验证 Boolean 对象是否为 true
@AssertFalse 验证 Boolean 对象是否为 false
长度检查
@Size(min=, max=) 验证对象(Array,Collection,Map,String)长度是否在给定的范围之内
@Length(min=, max=) Validates that the annotated string is between min and max included.
日期检查
@Past 验证 Date 和 Calendar 对象是否在当前时间之前
@Future 验证 Date 和 Calendar 对象是否在当前时间之后
@Pattern 验证 String 对象是否符合正则表达式的规则
数值检查,建议使用在Stirng,Integer类型,不建议使用在int类型上,因为表单值为“”时无法转换为int,但可以转换为Stirng为"",Integer为null
@Min 验证 Number 和 String 对象是否大等于指定的值
@Max 验证 Number 和 String 对象是否小等于指定的值
@DecimalMax 被标注的值必须不大于约束中指定的最大值. 这个约束的参数是一个通过BigDecimal定义的最大值的字符串表示.小数存在精度
@DecimalMin 被标注的值必须不小于约束中指定的最小值. 这个约束的参数是一个通过BigDecimal定义的最小值的字符串表示.小数存在精度
@Digits 验证 Number 和 String 的构成是否合法
@Digits(integer=,fraction=) 验证字符串是否是符合指定格式的数字,interger指定整数精度,fraction指定小数精度。
@Range(min=, max=) Checks whether the annotated value lies between (inclusive) the specified minimum and maximum.
@Range(min=10000,max=50000,message="range.bean.wage")
private BigDecimal wage;
@Valid 递归的对关联对象进行校验, 如果关联对象是个集合或者数组,那么对其中的元素进行递归校验,如果是一个map,则对其中的值部分进行校验.(是否进行递归验证)
@CreditCardNumber信用卡验证
@Email 验证是否是邮件地址,如果为null,不进行验证,算通过验证。
@ScriptAssert(lang= ,script=, alias=)
@URL(protocol=,host=, port=,regexp=, flags=)
常用验证注解
@Null,标注的属性值必须为空
@NotNull,标注的属性值不能为空
@AssertTrue,标注的属性值必须为true
@AssertFalse,标注的属性值必须为false
@Min,标注的属性值不能小于min中指定的值
@Max,标注的属性值不能大于max中指定的值
@DecimalMin,小数值,同上
@DecimalMax,小数值,同上
@Negative,负数
@NegativeOrZero,0或者负数
@Positive,整数
@PositiveOrZero,0或者整数
@Size,指定字符串长度,注意是长度,有两个值,min以及max,用于指定最小以及最大长度
@Digits,内容必须是数字
@Past,时间必须是过去的时间
@PastOrPresent,过去或者现在的时间
@Future,将来的时间
@FutureOrPresent,将来或者现在的时间
@Pattern,用于指定一个正则表达式
@NotEmpty,字符串内容非空
@NotBlank,字符串内容非空且长度大于0
@Email,邮箱
@Range,用于指定数字,注意是数字的范围,有两个值,min以及max