springboot中使用@Valid以及@Validated优雅的进行数据校验

在实际开发中常常要对传入参数做校验,往往会冗余大量的if/else来进行数据校验显得既不美观也不优雅,使用springboot中@Valid和@Valided结合jsr303数据校验可以很好的解决这个问题

这里处理的思路:

  1. 使用@Valided注解标识在需要校验的方法参数上并根据分组校验规则校验,配合BindResult对象封装校验的错误信息(BindResult对象会对校验结果自动进行封装)

  2. 使用spring的aop,配置需要进行校验的方法的切点对BindResult对象进行统一处理,如果有异常则抛出自定义的异常

  3. 使用@RestControllerAdvice配置处理自定义校验的通知并返回通用结果集BaseResult到前端显示

@Valid和@Validated的区别:

  1. @Validated:用在方法入参上无法单独提供嵌套验证功能。不能用在成员属性(字段)上,也无法提示框架进行嵌套验证。能配合嵌套验证注解@Valid进行嵌套验证。
  2. @Valid:用在方法入参上无法单独提供嵌套验证功能。能够用在成员属性(字段)上,提示验证框架进行嵌套验证。能配合嵌套验证注解@Valid进行嵌套验证。

参考:

https://blog.csdn.net/qq_27680317/article/details/79970590

话不多说直接上代码:

BaseResult

/**
 * Web接口统一返回结果
 * @author
 * @since
 */

@Data
@ToString
@Accessors(chain = true)
public class BaseResult implements Serializable {

    private static final long serialVersionUID = 1L;

    public static final String ERROR_MSG = "操作失败";

    /**
     * 状态码
     */
    private int code;

    /**
     * 消息信息
     */
    private String message;

    private Object data;

    private Map<String,Object> row;

    public BaseResult() {
    }

    public BaseResult(int code, String message) {
        this(code, message, null);
    }

    public BaseResult(int code, String message, Object data) {
        this.code = code;
        this.message = message;
        this.data = data;
    }

    /**
     * 获取成功的对象
     * @return
     */
    public static BaseResult successResult(){
        BaseResult baseResult = new BaseResult();
        baseResult.setCode(ResultEnum.SUCCESS.getCode());
        baseResult.setMessage(ResultEnum.SUCCESS.getValue());
        return baseResult;
    }

    /**
     * 设置为操作成功
     * @return
     */
    public BaseResult success(){
        this.setCode(ResultEnum.SUCCESS.getCode());
        this.setMessage(ResultEnum.SUCCESS.getValue());
        return this;
    }

    /**
     * 设置为失败
     * @return
     */
    public BaseResult error(){
        this.setCode(ResultEnum.ERR_PARAM.getCode());
        this.setMessage(ResultEnum.ERR_PARAM.getValue());
        return this;
    }

    /**
     * 设置为失败
     * @param code
     * @param msg
     * @return
     */
    public BaseResult error(int code, String msg){
        this.setCode(ResultEnum.ERR_PARAM.getCode());
        this.setMessage(ResultEnum.ERR_PARAM.getValue());
        return this;
    }

    /**
     * 是否成功
     * @return
     */
    public boolean isSuccess(){
      return this.code == 200? true:false;
    }
}

ValidType:
定义几个空的接口用于@Valided注解中group属性对校验进行分组以满足不同的校验业务场景

public class ValidType {

    public interface Add{};

    public interface Delete{};

    public interface Select{};

    public interface Update{};
}

User实体:
可以使用校验注解的group属性对校验进行分类,用来区分不同场景下是否需要进行校验,比如add时需要对id进行校验,而update操作时不需要对id进行校验
我这里定义了CRUD四个场景用来分组

@Data
@Accessors(chain = true)
public class User extends BaseModel{

    @NotNull(message="id不能为空",groups = {ValidType.Update.class,ValidType.Delete.class})
    private Long id;

    @NotNull(message="username不能为空",groups = {ValidType.Add.class,ValidType.Update.class})
    @NotBlank(message="username不能为空",groups = {ValidType.Add.class,ValidType.Update.class})
    private String username;

    @Email(message = "邮箱格式不正确",groups = {ValidType.Add.class,ValidType.Update.class})
    @NotNull(message="邮箱不能为空",groups = {ValidType.Add.class,ValidType.Update.class})
    @NotBlank(message="邮箱不能为空",groups = {ValidType.Add.class,ValidType.Update.class})
    private String email;
}

@null 验证对象是否为空
@notnull 验证对象是否为非空
@asserttrue 验证 boolean 对象是否为 true
@assertfalse 验证 boolean 对象是否为 false
@min 验证 number 和 string 对象是否大等于指定的值
@max 验证 number 和 string 对象是否小等于指定的值
@decimalmin 验证 number 和 string 对象是否大等于指定的值,小数存在精度
@decimalmax 验证 number 和 string 对象是否小等于指定的值,小数存在精度
@size 验证对象(array,collection,map,string)长度是否在给定的范围之内
@digits 验证 number 和 string 的构成是否合法
@past 验证 date 和 calendar 对象是否在当前时间之前
@future 验证 date 和 calendar 对象是否在当前时间之后
@pattern 验证 string 对象是否符合正则表达式的规则
@Email 验证邮箱

CheckParamException:自定义异常

package lf.ssm.exception;

import com.sun.tools.javac.jvm.Code;
import lf.ssm.enums.ResultEnum;
import lombok.Data;
import lombok.Getter;
import org.springframework.validation.BindingResult;

/**
 * @Classname CheckParamException
 * @Date 2020/4/10 8:25
 * @Created by liufeng
 */

public class CheckParamException extends Exception{

    @Getter
    private String msg;

    @Getter
    private int code;

    @Getter
    private BindingResult bindingResult;

    public CheckParamException(ResultEnum resultEnum,BindingResult bindingResult) {
        this.msg = resultEnum.value;
        this.code = resultEnum.code;
        this.bindingResult = bindingResult;
    }

}


Contoller:

package lf.ssm.controller.test;

@Api(description = "用户控制器",tags = "BaseController")
@RestController
@RequestMapping("/user")
public class UserController extends BaseController {

    @Autowired
    private IUserService service;

    @PostMapping("/add")
    @ApiOperation("添加用户")
    public BaseResult userAdd(@RequestBody @Validated({ValidType.Add.class}) User user, BindingResult bindingResult) {
        BaseResult baseResult = BaseResult.successResult();
        service.insert(user);
        return baseResult;
    }

    @PostMapping("/update")
    @ApiOperation("更新用户")
    public BaseResult userUpdate(@RequestBody @Validated({ValidType.Update.class}) User user, BindingResult bindingResult){
        BaseResult baseResult = BaseResult.successResult();
        service.updateById(user);
        return baseResult;
    }
    
  }

配置aop切面:
当请求controller(切点)时,会进入所配置的前置通知中,这样我们就可以对校验的结果进行判断并处理

@Slf4j
@Aspect
@Component
public class CheckParamAop {
    //execution(* lf.ssm.controller.*.*(..)):controller包下所有控制器的方法
    //execution(* lf.ssm.controller..*.*(..)):controller包及其子包下controller的所有方法
    //execution(* lf.ssm.controller.UserController.*(..)): UserController的所有方法
    private final String expCheckPoint="execution(* lf.ssm.controller..*.*(..))";

    @Pointcut(expCheckPoint) //配置切点
    public void expCheckPoint(){}

    /**
     *配置前置通知并织入
     * @author liufeng
     * @date 2020/4/13 8:05
     * @return void
     */
    @Before("expCheckPoint()")
    public void doBefore(JoinPoint joinPoint) throws CheckParamException {
        log.info("这是前置通知参数校验");
        BindingResult bindingResult=null;

        for (Object arg:joinPoint.getArgs()){
            if(arg instanceof BindingResult){   //取出校验结果
                bindingResult= (BindingResult) arg;
            }
        }
        //当校验不通过时抛出自定义异常
        if(bindingResult != null && bindingResult.hasErrors()){
            throw new CheckParamException(ResultEnum.PARAM_ERROR,bindingResult);
        }
    }

}

ExceptionControllerAdvice
使用spring的@RestControllerAdvice注解配置通知处理自定义异常,封装异常信息返回给前端:

/**
 * 校验异常处理通知,异常统一处理
 * @Classname ExceptionControllerAdvice
 * @Date 2020/4/13 10:10
 * @Created by liufeng
 */
@Slf4j
@RestControllerAdvice
public class ExceptionControllerAdvice {

    @ExceptionHandler(CheckParamException.class)
    public BaseResult bindException(CheckParamException e){
        BaseResult baseResult = new BaseResult();
        BindingResult bindingResult = e.getBindingResult();
        log.warn("CheckParamException:>>>");

        if(bindingResult!=null && bindingResult.hasErrors()){
            List<ObjectError> allErrors = bindingResult.getAllErrors();
            baseResult.setCode(e.getCode());
            String message="参数错误:%s";

            for (ObjectError err:allErrors){
                message = String.format(message, err.getDefaultMessage());
               // log.warn(err.getDefaultMessage());
                break;
            }

            baseResult.setMessage(message);
        }else{
            baseResult.setCode(e.getCode());
            baseResult.setMessage(e.getMessage());
        }
        return baseResult;
    }
}

测试请求校验:
add
springboot中使用@Valid以及@Validated优雅的进行数据校验_第1张图片
update:
springboot中使用@Valid以及@Validated优雅的进行数据校验_第2张图片
springboot中使用@Valid以及@Validated优雅的进行数据校验_第3张图片

总结:使用@Valided的方式对数据进行校验有效减少大量if/else的校验代码,对于复杂的校验@valided可以与if/else配合一起使用以满足不同的业务场景

你可能感兴趣的:(springboot中使用@Valid以及@Validated优雅的进行数据校验)