(5)SprintBoot 2.X JSR303 参数校验
1.JSR303参数校验
1.1 JSR303简介
- JSR-303 是 JAVA EE 6 中的一项子规范,叫做 Bean Validation,官方参考实现是Hibernate Validator。
- 此实现与 Hibernate ORM 没有任何关系。 JSR 303 用于对 Java Bean 中的字段的值进行验证。
Spring MVC 3.x 之中也大力支持 JSR-303,可以在控制器中对表单提交的数据方便地验证。 - 注:可以使用注解的方式进行验证
1.2 为什么使用JSR303
- JSR 303 用于对Java Bean 中的字段的值进行验证,使得验证逻辑从业务代码中脱离出来。是一个运行时的数据验证框架,在验证之后验证的错误信息会被马上返回。
1.3 常用注解
Constraint | 详细信息 |
---|---|
@Null | 被注释的元素必须为 null |
@NotNull | 被注释的元素必须不为 null |
@AssertTrue | 被注释的元素必须为 true |
@AssertFalse | 被注释的元素必须为 false |
@Min(value) | 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 |
@Max(value) | 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 |
@DecimalMin(value) | 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 |
@DecimalMax(value) | 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 |
@Size(max, min) | 被注释的元素的大小必须在指定的范围内 |
@Digits (integer, fraction) | 被注释的元素必须是一个数字,其值必须在可接受的范围内 |
@Past | 被注释的元素必须是一个过去的日期 |
@Future | 被注释的元素必须是一个将来的日期 |
@Pattern(value) | 被注释的元素必须符合指定的正则表达式 |
1.4 代码实现
1.pom.xml中引入依赖
<!--用于定义参数校验器 + 全局异常处理器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
2.自定义参数校验器@IsMobile
- 除了使用自带的注解之外,JSR303还允许自定义校验注解
- 步骤1: 实现校验器类IsMobileValidator类,需要继承ConstraintValidator校验器,需要重写方法initialize(),指定注解接口 IsMobile,然后查看值是否为必须的,如果是必须的,那么isValid会进行验证逻辑。
public class IsMobileValidator implements ConstraintValidator<IsMobile, String> {
private boolean required = false;
//初始化指定注解接口
@Override
public void initialize(IsMobile isMobile) {
required = isMobile.required();
}
@Override
public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
if (required) {
return ValidatorUtil.isMobile(value);
} else {
if (StringUtils.isEmpty(value)) {
return true;
} else {
return ValidatorUtil.isMobile(value);
}
}
}
}
- 步骤2: 自定义注解@IsMobile,并引入相关配置。@Constraint(validatedBy = { IsMobileValidator.class })。指定自定义的一个校验器。
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint( validatedBy = {IsMobileValidator.class})//引进校验器
public @interface IsMobile {
boolean required() default true;//默认不能为空
//假如手机号码格式错误将抛出绑定异常
String message() default "手机号码格式错误";//校验不通过输出信息
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
- 步骤3: 步骤1中使用的自定义的手机验证逻辑工具类ValidatorUtil的实现
public class ValidatorUtil {
private static final Pattern mobile_pattern = Pattern.compile("1\\d{10}");
public static boolean isMobile(String src){
if(StringUtils.isEmpty(src)){
return false;
}
Matcher m = mobile_pattern.matcher(src);
return m.matches();
}
}
- 步骤4: 登陆需要用的LoginVo,需要使用自带的检验注解
public class LoginVo {
@NotNull
@IsMobile
private String mobile;//@IsMobile 假如手机号码格式错误将抛出绑定异常
@NotNull
private String password;
public String getMobile() {
return mobile;
}
public void setMobile(String mobile) {
this.mobile = mobile;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "LoginVo{" +
"mobile='" + mobile + '\'' +
", password='" + password + '\'' +
'}';
}
}
3.具体用法
- 在需要验证的参数前面打上标签注解@Valid,那么此注解就会自动对该Bean 进行参数校验。具体校验规则在该Bean内部实现。
@RequestMapping("/do_login")
@ResponseBody
public Result<String> doLogin(HttpServletResponse response , @Valid LoginVo loginVo) {
log.info(loginVo.toString());
String token = userService.login(response,loginVo);
return Result.success(token);
}
2.全局异常处理
2.1 为什么引入全局异常处理?
- 当定义了JSR303校验器后,校验不通过都会产生一个BindException( org.springframework.validation.BindException)和一大串错误信息(其中就包括校验的处理信息)。若要对异常处理,我们可以定义一个全局异常处理的拦截器。
- 优点1: 可以实现对项目中所有产生的异常进行拦截,在同一个类中实现统一处理。避免异常漏处理的情况。
- 优点2: 当Service 出现业务逻辑错误的时候,这个时候我们可以直接抛出异常,让拦截器来捕捉,捕捉之后,就不需要冗余的代码来return 一个不符合业务逻辑的返回值来作为输出。
- 优点3: 当参数校验不通过的时候,输出也是Result(CodeMsg),传给前端用于前端显示获取处理
2.2 代码实现全局异常处理
2.2.1自定义全局异常类GlobalException,该类需要继承RunTimeException
public class GlobalException extends RuntimeException {
private static final long servialVersionUID = 1L;
private CodeMsg codeMsg;
public GlobalException(CodeMsg codeMsg) {
super(codeMsg.toString());
this.codeMsg = codeMsg;
}
public CodeMsg getCodeMsg() {
return codeMsg;
}
}
2.2.2 自定义全局异常拦截器
-
主要用到的是:@ControllerAdvice + @ExceptionHandler (Spring 框架)
-
@ControllerAdvice是一个@Component,用于定义@ExceptionHandler,@InitBinder和@ModelAttribute方法,适用于所有使用@RequestMapping方法。
-
@ResponseBody
为了方便输出,使得这个GlobalExceptionHandler类里面的方法跟我们Controller类一样是输出的信息,返回值Result类型,可以携带信息。当参数校验不通过的时候,输出也是Result(CodeMsg),传给前端用于前端显示获取处理。 -
@ExceptionHandler(value = Exception.class)用于指定需要拦截的异常及其子类异常
@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {
@ExceptionHandler(value = Exception.class)//拦截所有异常
public Result<String> exceptionHandler(HttpServletRequest request, Exception e){
e.printStackTrace();
if(e instanceof GlobalException) {
GlobalException ex = (GlobalException)e;
return Result.error(ex.getCodeMsg());
}else if(e instanceof BindException) {
/*注意:此处的BindException 是 Spring 框架抛出的Validation异常*/
BindException ex = (BindException)e;
List<ObjectError> errors = ex.getAllErrors();//绑定错误返回很多错误,是一个错误列表,只需要第一个错误
ObjectError error = errors.get(0);
String msg = error.getDefaultMessage();
return Result.error(CodeMsg.BIND_ERROR.fillArgs(msg));//给状态码填充参数
}else {
return Result.error(CodeMsg.SERVER_ERROR);
}
}
}
2.2.3 业务逻辑代码使用全局异常
- 全局异常处理场景:先检查异常类型,若是我们业务异常,返回即可。业务中发现异常直接抛出我们自定义的异常即可。
public String login(HttpServletResponse response,LoginVo loginVo){
if (loginVo == null) {
throw new GlobalException(CodeMsg.SERVER_ERROR);
}
String mobile = loginVo.getMobile();
String formPass = loginVo.getPassword();
//判断手机号是否存在
MiaoshaUser user = getById(Long.parseLong(mobile));
if (user == null) {
throw new GlobalException(CodeMsg.MOBILE_NOT_EXIST);
}
//验证密码
String dbPass = user.getPassword();
String saltDB = user.getSalt();
String calcPass = MD5Util.formPassToDBPass(formPass, saltDB);
if (!calcPass.equals(dbPass)) {
throw new GlobalException(CodeMsg.PASSWORD_ERROR);
}
String token = UUIDUtil.uuid();
addCookie(response, token, user);
return token;
}