参数校验 + 全局异常拦截

x33g5p2x  于2022-07-26 转载在 其他  
字(13.2k)|赞(0)|评价(0)|浏览(529)

案例引入

需求是写一个 新增用户 的controller接口
首先,提供一下 UserVo类:

import lombok.Data;
/**
 * todo 用户vo
 *
 * @author coderzpw.zhang
 * @version 1.0
 * @date 2022/7/23 12:36
 */
@Data
public class UserVo {
    private String name;
    private Integer age;
    private String addr;
}

然后在controller中写对应的addUser接口,参数要求:name 和 age不能为空

/**
 * todo 用户web接口
 *
 * @author coderzpw.zhang
 * @version 1.0
 * @date 2022/7/23 12:54
 */
@RequestMapping("/user")
@RestController
public class UserController {

    @PostMapping("/save")
    public Object paramTest(@RequestBody UserVo userVo) {
        // 返回结果, 当然在实际开发中肯定是会有自己的返回结果类的, 这里我就用map来替代
        HashMap<String, Object> result = new HashMap<>();
        if (StringUtils.isEmpty(userVo.getName())) {
            result.put("code","400");
            result.put("msg","name参数为空!");
            return result;
        }
        if (Objects.isNull(userVo.getAge())) {
            result.put("code","400");
            result.put("msg","age参数为空!");
            return result;
        }
        result.put("code","200");
        result.put("msg","success");
        return result;
    }
}

使用 postman 自测:
缺少name

缺少age

可以看到,返回结果是没有问题的,可以实现对 nameage 的校验。

但我们在编写代码是就会感觉有点繁琐,主要是两个问题:

  • 无效业务代码过多:一个参数就要写 4、5行代码校验 ,那如果一个Vo中有20个参数需要校验呢?难度写100行校验代码?(太麻烦了,感觉能写吐)
  • 不够优雅:大多数是一些 if else 判断,而且风格都一样(不够美观)

参数校验

引入依赖

@Valid@NotBlank@NotNull这些注解是 Validation Starter 依赖下的注解,在 Spring Boot 2.3 之前内部包括这个依赖包,但是2.3更新之后就不再包括了,如果想要使用它们就需要额外引入spring-boot-starter-validation这个依赖。
springboot2.3更新说明,有图有真相:

看一下我的pom依赖:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.3.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!--参数校验(SpringBoot2.3后需要自行引入)-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>

</dependencies>

其实spring-boot-starter-validation为我们提供了很多校验注解:

注解作用
@AssertFalse被注解的元素必须为false
@AssertTrue被注解的元素必须为True
@DecimalMax(value)被注解的元素必须为一个数字,其值必须小于等于指定的最小值
@DecimalMin(Value)被注解的元素必须为一个数字,其值必须大于等于指定的最小值
@Digits(integer=, fraction=)被注解的元素必须为一个数字,其值必须在可接受的范围内
@Email被注释的元素必须是电子邮箱地址
@Future被注解的元素必须是日期,检查给定的日期是否比现在晚
@Max(value)被注解的元素必须为一个数字,其值必须小于等于指定的最小值,检查该值是否小于或等于约束条件中指定的最大值. 会给对应的数据库表字段添加一个 check的约束条件
@MinBigDecimal,BigInteger, byte,short, int, long,等任何Number或CharSequence(存储的是数字)子类型 验证注解的元素值大于等于@Min指定的value值
@NotEmpty被注释的对象必须为空(数据:String,Collection,Map,arrays)
@NotBlank不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的首位空格
@NotNull被注解的元素必须不为null
@Null被注解的元素必须为null
@Past(java.util.Date/Calendar)被注解的元素必须过去的日期,检查标注对象中的值表示的日期比当前早
@Pattern(regex=, flag=)被注解的元素必须符合正则表达式,检查该字符串是否能够在match指定的情况下被regex定义的正则表达式匹配
@Size(min=, max=)被注解的元素必须在制定的范围(数据类型:String, Collection, Map and arrays)
@Valid递归的对关联对象进行校验, 如果关联对象是个集合或者数组, 那么对其中的元素进行递归校验,如果是一个map,则对其中的值部分进行校验

没有列举完,还有很多,有兴趣自己去研究哈

@Valid 详解

接下来我们在UserVo 类上打上如下注解:

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

/**
 * todo 用户vo
 *
 * @author coderzpw.zhang
 * @version 1.0
 * @date 2022/7/22 23:40
 */
@Data
public class UserVo {

    @NotBlank(message = "name参数不能为空!")
    private String name;
    @NotNull(message = "age参数不能为空!")
    private Integer age;
    private String addr;
}

然后再 Controller 对应方法上,对这个userVo标上 @Valid 注解,表示我们对这个对象属性需要进行验证:

有验证就会有结果,想要拿到验证结果还需要在参数中添加BindingResult参数,然后利用该参数来判断是否抛出了异常

import com.coderzpw.exception.vo.UserVo;
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;

import javax.validation.Valid;
import java.util.HashMap;

/**
 * todo 用户web接口
 *
 * @author coderzpw.zhang
 * @version 1.0
 */
@RequestMapping("/user")
@RestController
public class UserController {

    @PostMapping("/save")
    public Object paramTest(@RequestBody @Valid UserVo userVo, BindingResult bindingResult) {
        // 返回结果, 当然在实际开发中肯定是会有自己的返回结果类的, 这里我就用map来替代
        HashMap<String, Object> result = new HashMap<>();
        // 若校验UserVo中的参数不通过,就会有错误信息
        if (bindingResult.hasErrors()) {
            // 获取第一个错误信息
            String errorMessage = bindingResult.getAllErrors().get(0).getDefaultMessage();
            result.put("code","400");
            result.put("msg",errorMessage);
            return result;
        }

        result.put("code","200");
        result.put("msg","success");
        return result;
    }
}

请求测试:

没有问题吧

@Validated 详解

现在我们来说说 @Validated 这个注解,该注解 是在 @Valid 基础上做的一个升级版。
我们在使用@Valid时,还需要自己用一个BindingResult 对象来接收校验的结果,而且判断逻辑还需要自己写,好像有点鸡肋

现在我们使用@Validated这个注解,就不需要在引入BindingResult 参数了,也不需要自己写逻辑处理

import com.coderzpw.exception.vo.UserVo;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
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;

import javax.validation.Valid;
import java.util.HashMap;

/**
 * todo 用户web接口
 *
 * @author coderzpw.zhang
 * @version 1.0
 */
@RequestMapping("/user")
@RestController
public class UserController {

    @PostMapping("/save")
    public Object paramTest(@RequestBody @Validated UserVo userVo) {
        // 返回结果, 当然在实际开发中肯定是会有自己的返回结果类的, 这里我就用map来替代
        HashMap<String, Object> result = new HashMap<>();
        result.put("code","200");
        result.put("msg","success");
        return result;
    }
}

请求测试:

后端控制台:

注意点:

  • 其实@Validated在校验Json参数不通过时,会抛出一个MethodArgumentNotValidException的异常。控制台为什么没有抛出异常信息呢?因为被springMvc的DefaultHandlerExceptionResolver捕获并处理了
  • 返回的数据、格式很显然都不是我们自己定义的,为什么会返回这样的数据呢?其实是因为 上面那个异常
    被springMvc的DefaultHandlerExceptionResolver捕获并处理了,并设置了返回的数据

不信的话,我们可以dubug一下:
ctrl+shift+alt+n 定位 DefaultHandlerExceptionResolver这个类

打断点:

再次请求测试,查看dubug信息:

我就不再深入探究了,感兴趣的话自己可以研究哈!

这里有篇DefaultHandlerExceptionResolver的博客,感兴趣可以看一下:【Spring MVC : 工具 DefaultHandlerExceptionResolver】

异常捕获

关键注解

实际开发中我们一般不会返回如下格式的数据:

我们常常会自己去定制化返回结果,那如何定制呢?
我们可以自定义一个 异常捕获的类 MyExceptionHandler,最关键的是加上@ControllerAdvice这个注解,然后使用@ExceptionHandler(xxx.class)来捕获指定的异常

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 java.util.HashMap;
import java.util.Map;

/**
 * todo 全局异常捕获
 *
 * @author coderzpw.zhang
 * @version 1.0
 */
@ControllerAdvice   // 用来捕获异常的关键注解
public class MyExceptionHandler {
    /**
     * json参数 - 后端用对象接收
     * @param exception
     * @return
     */
    @ResponseBody	// 表明最终返回json格式数据
    @ExceptionHandler(MethodArgumentNotValidException.class)	// 指定要捕获的异常类型
    public Map<String, Object> handleCustomException(MethodArgumentNotValidException exception) {
        // 定制化返回结果
        HashMap<String, Object> hashMap = new HashMap<>();
        hashMap.put("code","400");
        hashMap.put("msg", exception.getBindingResult().getAllErrors().get(0).getDefaultMessage());
        return hashMap;
    }
}

请求测试:

简单介绍一下@ControllerAdvice@ExceptionHandler两个注解

  • @ControllerAdvice注解作用是给Controller控制器添加统一的操作或处理。
    对于@ControllerAdvice,我们比较熟知的用法是结合@ExceptionHandler用于全局异常的处理,但其作用不止于此。ControllerAdvice拆开来就是Controller Advice,关于Advice,在Spring的AOP中,是用来封装一个切面所有属性的,包括切入点和需要织入的切面逻辑。这里ControllerAdvice也可以这么理解,其抽象级别应该是用于对Controller进行切面环绕的,而具体的业务织入方式则是通过结合其他的注解来实现的。@ControllerAdvice是在类上声明的注解,其用法主要有三点:

  • @ExceptionHandler:用于捕获Controller中抛出的指定类型的异常,从而达到不同类型的异常区别处理的目的

  • @InitBinder:用于request中自定义参数解析方式进行注册,从而达到自定义指定格式参数的目的

  • @ModelAttribute:表示其注解的方法将会在目标Controller方法执行之前执行

常见参数异常类型

常见的参数异常有三种

  • MethodArgumentNotValidException:前端传来的是Json数据,后端用对象接收(就是上面我们演示的那种情况)
  • BindException:前端传来的是form表单参数 或者 url参数,后端用对象接收
  • ConstraintViolationException:前端传来的是form表单参数 或者 url参数,后端用多个参数接收,在参数前加@NotBlank等注解

接下来我们再写另外两种类型的接口:

@RequestMapping("/user")
@RestController
@Validated
public class UserController {

    // @RequestBody用json接收参数。校验失败会抛MethodArgumentNotValidException异常
    @PostMapping("/save")
    public Object paramTest(@RequestBody @Validated UserVo userVo) {
        HashMap<String, Object> result = new HashMap<>();
        result.put("code","200");
        result.put("msg","success");
        return result;
    }

    // 去掉@RequestBody,不再用json接收。校验失败会抛BindException异常
    @PostMapping("/save2")
    public Object paramTest2(@Validated UserVo userVo) {
        HashMap<String, Object> result = new HashMap<>();
        result.put("code","200");
        result.put("msg","success");
        return result;
    }

    // 不使用对象而用多个参数接收,在每个参数前加@NotBlank等注解,这个需要在UserController类上加@Validated。校验失败会抛ConstraintViolationException异常
    @PostMapping("/save3")
    public Object paramTest2(@NotBlank(message = "name参数为空!") String name,
                             @NotNull Integer age,
                             String addr) {
        HashMap<String, Object> result = new HashMap<>();
        result.put("code","200");
        result.put("msg","success");
        return result;
    }
}

再添加上对应的异常捕获方法:

/**
 * todo 全局异常捕获
 *
 * @author coderzpw.zhang
 * @version 1.0
 */
@ControllerAdvice   // 用来捕获异常的关键注解
public class MyExceptionHandler {

    /**
     * json参数 - 后端用对象接收
     * @param exception
     * @return
     */
    @ResponseBody
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Map<String, Object> handleCustomException(MethodArgumentNotValidException exception) {
        System.out.println("MethodArgumentNotValidException异常拦截!");
        HashMap<String, Object> hashMap = new HashMap<>();
        hashMap.put("code","400");
        hashMap.put("msg", exception.getBindingResult().getAllErrors().get(0).getDefaultMessage());
        return hashMap;
    }

    /**
     * form表单 或者 url参数 - 后端用对象接收
     * @param exception
     * @return
     */
    @ResponseBody
    @ExceptionHandler(BindException.class)
    public Map<String, Object> handleCustomException(BindException exception) {
        System.out.println("BindException异常拦截!");
        HashMap<String, Object> hashMap = new HashMap<>();
        hashMap.put("code","400");
        hashMap.put("msg", exception.getBindingResult().getAllErrors().get(0).getDefaultMessage());
        return hashMap;
    }

    /**
     * form表单 或者 url参数 - 后端用多个参数接收
     * @param exception
     * @return
     */
    @ResponseBody
    @ExceptionHandler(ConstraintViolationException.class)
    public Map<String, Object> handleCustomException2(ConstraintViolationException exception) {
        System.out.println("ConstraintViolationException异常拦截!");
        HashMap<String, Object> hashMap = new HashMap<>();
        hashMap.put("code","400");
        hashMap.put("msg", exception.getMessage());
        return hashMap;
    }

}

启动项目,并请求测试:

先测一下 /save2 接口

控制台输出:

再来测一下 /save3 接口

控制台输出:

自定义异常

自定义异常枚举类:

import lombok.Getter;

/**
 * todo 异常枚举类
 *
 * @author coderzpw.zhang
 * @version 1.0
 */
@Getter
public enum ResultEnum {

    SUCCESS(200, "成功!"),
    UN_EXPECTED(500, "系统发生错误,请联系管理员!"),
    UN_AUTHORIZED(401, "未认证!"),
    NO_PERMISSION(403, "无权限!");


    private Integer code;
    private String message;

    ResultEnum(Integer code, String message) {
        this.code = code;
        this.message = message;
    }
}

自定义异常类:

import com.coderzpw.exception.enums.ResultEnum;
import lombok.Data;

/**
 * todo 自定义异常类
 *
 * @author coderzpw.zhang
 * @version 1.0
 */
@Data
public class MyException extends RuntimeException {
    private Integer code;

    public MyException(Integer code, String message) {
        super(message);
        this.code = code;
    }

    public MyException(ResultEnum resultEnum) {
        super(resultEnum.getMessage());
        this.code = resultEnum.getCode();
    }
}

为了返回统一的数据,我们还可以自定义一个返回类ResultVO

import lombok.Data;

import java.io.Serializable;

/**
 *todo 统一返回类型
 * 
 * @author coderzpw.zhang
 * @version 1.0
 */
@Data
public class ResultVO<T> implements Serializable {

    // 状态码
    private Integer code;
    // 提示信息
    private String msg;
    // 具体内容
    private T data;
}

然后再写一个生成ResultVo对象的工具类

import com.coderzpw.exception.vo.ResultVO;
import com.coderzpw.exception.enums.ResultEnum;

/**
 * todo 生成ResultVo对象
 *
 * @author coderzpw.zhang
 * @version 1.0
 */
public class ResultVOUtil {
    /**
     * 返回成功信息(带返回数据)
     * @param object
     * @return
     */
    public static ResultVO success(Object object) {
        ResultVO resultVO = new ResultVO();
        resultVO.setData(object);
        resultVO.setCode(ResultEnum.SUCCESS.getCode());
        resultVO.setMsg(ResultEnum.SUCCESS.getMessage());
        return resultVO;
    }

    /**
     * 返回成功信息(不带数据)
     * @return
     */
    public static ResultVO success() {
        return success(null);
    }

    /**
     * 返回错误数据
     * @param code
     * @param msg
     * @return
     */
    public static ResultVO error(Integer code, String msg) {
        ResultVO resultVO = new ResultVO();
        resultVO.setCode(code);
        resultVO.setMsg(msg);
        return resultVO;
    }

    /**
     * 返回错误数据(枚举类型入参)
     * @param resultEnum
     * @return
     */
    public static ResultVO error(ResultEnum resultEnum) {
        ResultVO resultVO = new ResultVO();
        resultVO.setCode(resultEnum.getCode());
        resultVO.setMsg(resultEnum.getMessage());
        return resultVO;
    }
}

接下来我们在异常拦截类MyExceptionHandler中对MyException类型进行拦截

@ResponseBody
@ExceptionHandler(MyException.class)
public ResultVO handleCustomException2(MyException exception) {
    System.out.println("customException拦截!");
    return ResultVOUtil.error(exception.getCode(), exception.getMessage());
}

最后在Controller层写个接口测试一下:

/**
 * 抛出自定义异常
 * @return
 */
@GetMapping("/custom-exception")
public ResultVO customException() {
    System.out.println("例如用户没权限访问");
    throw new MyException(ResultEnum.NO_PERMISSION);
}

请求测试:

完结,撒花!!!

相关文章