优雅哥 SpringBoot 2.7 实战基础 - 08 - 全局异常处理及参数校验

前后端分离开发非常普遍,后端处理业务,为前端提供接口。服务中总会出现很多运行时异常和业务异常,本文主要讲解在 SpringBoot 实战中如何进行异常统一处理和请求参数的校验。

1 异常统一处理

所有异常处理相关的类,咱们都放到 com.yygnb.demo.common包中。

当后端发生异常时,需要按照一个约定的规则(结构)返回给前端,所以先定义一个发生异常时固定的结构。

1.1 错误响应结构

发生异常时的响应结构约定两个字段:code——错误编码;msg——错误消息。创建类:

com.yygnb.demo.common.domain.ErrorResult

  1. @Data
  2. @Builder
  3. @NoArgsConstructor
  4. @AllArgsConstructor
  5. public class ErrorResult implements Serializable {
  6. private static final long serialVersionUID = -8738363760223125457L;
  7. /**
  8. * 错误码
  9. */
  10. private String code;
  11. /**
  12. * 错误消息
  13. */
  14. private String msg;
  15. public static ErrorResult build(ErrorResult commonErrorResult, String msg) {
  16. return new ErrorResult(commonErrorResult.getCode(), commonErrorResult.getMsg() + " " + msg);
  17. }
  18. }

1.2 通用错误响应常量

有些异常返回的 ErrorResult 是一样的,如参数校验错误、未查询到对象、系统异常等,可以定义一些错误响应的常量:

com.yygnb.demo.common.exception.DefaultErrorResult

  1. public interface DefaultErrorResult {
  2. ErrorResult SYSTEM_ERROR = new ErrorResult("C00001", "系统异常");
  3. ErrorResult CUSTOM_ERROR = new ErrorResult("C99999", "自定义异常");
  4. ErrorResult PARAM_BIND_ERROR = new ErrorResult("C00003", "参数绑定错误:");
  5. ErrorResult PARAM_VALID_ERROR = new ErrorResult("S00004", "参数校验错误:");
  6. ErrorResult JSON_PARSE_ERROR = new ErrorResult("S00005", "JSON转换异常");
  7. ErrorResult CODE_NOT_FOUND = new ErrorResult("S00006", "根据编码没有查询到对象");
  8. ErrorResult ID_NOT_FOUND = new ErrorResult("S00007", "根据ID没有查询到对象");
  9. }

1.3 通用异常类定义

定义一个通用的异常类 CommonException,继承自 RuntimeException,当程序中捕获到编译时异常或业务异常时,就抛出这个通用异常,交给全局来处理。(随着业务复杂度的增加,可以细分自定义异常,如 AuthExceptionUserExceptionCouponException 等,让这些细分异常都继承自 CommonException。)

com.yygnb.demo.common.exception.CommonException

  1. @EqualsAndHashCode(callSuper = true)
  2. @Data
  3. public class CommonException extends RuntimeException {
  4. protected ErrorResult errorResult;
  5. public CommonException(String message) {
  6. super(message);
  7. }
  8. public CommonException(String message, Throwable cause) {
  9. super(message, cause);
  10. }
  11. public CommonException(Throwable cause) {
  12. super(cause);
  13. }
  14. protected CommonException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
  15. super(message, cause, enableSuppression, writableStackTrace);
  16. }
  17. public CommonException(String code, String msg) {
  18. super(msg + "(" + code + ")");
  19. this.errorResult = new ErrorResult(code, msg);
  20. }
  21. public CommonException(ErrorResult errorResult) {
  22. super(errorResult.getMsg() + "(" + errorResult.getCode() + ")");
  23. this.errorResult = errorResult;
  24. }
  25. }

这个自定义异常类复写了父类的构造函数,同时定义了一个成员变量 ErrorResult,便于在同一异常处理时快速构造返回信息。

1.4 全局异常处理

Spring MVC 中提供了全局异常处理的注解:@ControllerAdvice@RestControllerAdvice。由于前后端分离开发 RESTful 接口,我们这里就使用 @RestControllerAdvice

com.yygnb.demo.common.exception.CommonExceptionHandler

  1. @Slf4j
  2. @RestControllerAdvice
  3. public class CommonExceptionHandler {
  4. /**
  5. * 通用业务异常
  6. *
  7. * @param e
  8. * @return
  9. */
  10. @ExceptionHandler(value = CommonException.class)
  11. @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
  12. public ErrorResult handleCommonException(CommonException e) {
  13. log.error("{}, {}", e.getMessage(), e);
  14. if (e.getErrorResult() != null) {
  15. return e.getErrorResult();
  16. }
  17. return new ErrorResult(DefaultErrorResult.CUSTOM_ERROR.getCode(), e.getMessage());
  18. }
  19. /**
  20. * 其他运行时异常
  21. * @param e
  22. * @return
  23. */
  24. @ExceptionHandler(value = Exception.class)
  25. @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
  26. public ErrorResult handleDefaultException(Exception e) {
  27. log.error("{}, {}", e.getMessage(), e);
  28. return DefaultErrorResult.SYSTEM_ERROR;
  29. }
  30. }

上面捕获了 CommonException 和 Exception,匹配顺序为从上往下。假设 handleDefaultException 在前面,当发生一个异常 CommonException 时,一来就会被 handleDefaultException 捕获,因为无论什么异常,都属于 Exception 的实例,不会执行 handleCommonException。所以越具体的异常处理,越要写在前面。

1.5 测试统一异常处理

在 DemoController 中抛出一个 CommonException:

  1. @GetMapping("hello")
  2. public String hello(String msg) {
  3. String result = "Hello Spring Boot ! " + msg;
  4. if ("demo".equals(msg)) {
  5. throw new CommonException("发生错误----这是自定义异常");
  6. }
  7. return result;
  8. }

启动服务,访问:

  1. http://localhost:9099/demo/hello?msg=demo

结果返回:

  1. {
  2. "code": "C99999",
  3. "msg": "发生错误----这是自定义异常"
  4. }

可以看出全局统一异常处理已经生效了。

2 参数校验

传统参数校验方式是通过多个 if/else 来进行,代码量大,很没有意义。Spring Boot 中有个 starter spring-boot-starter-validation 可以帮助咱们很方便的实现参数校验。

2.1 添加依赖

有些文章中说 spring boot 2.3 还是多少版本以后不用手动加入这个 starter,我试了以后不行,需要手动引入该依赖才行。

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-validation</artifactId>
  4. </dependency>

这个 starter 定义 Validator 以及 SmartValidator 接口,提供 @Validated,支持 spring 环境,支持验证组的规范, 提供了一系列的工厂类以及适配器。底层依赖 hibernate-validator 包。

2.2 完善异常处理类

在 1.4 中只捕获了 CommonException 和 Exception,此处要完善参数绑定、校验等异常。补充后 CommonExceptionHandler 如下:

  1. @Slf4j
  2. @RestControllerAdvice
  3. public class CommonExceptionHandler {
  4. @ExceptionHandler(value = BindException.class)
  5. @ResponseStatus(HttpStatus.BAD_REQUEST)
  6. public ErrorResult handleBindException(BindException e) {
  7. log.error("{}", e.getMessage(), e);
  8. List<String> defaultMsg = e.getBindingResult().getAllErrors()
  9. .stream()
  10. .map(ObjectError::getDefaultMessage)
  11. .collect(Collectors.toList());
  12. return ErrorResult.build(DefaultErrorResult.PARAM_BIND_ERROR, defaultMsg.get(0));
  13. }
  14. @ExceptionHandler(value = MethodArgumentNotValidException.class)
  15. @ResponseStatus(HttpStatus.BAD_REQUEST)
  16. public ErrorResult handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
  17. log.error("{}", e.getMessage(), e);
  18. List<String> defaultMsg = e.getBindingResult().getFieldErrors()
  19. .stream()
  20. .map(fieldError -> "【" + fieldError.getField() + "】" + fieldError.getDefaultMessage())
  21. .collect(Collectors.toList());
  22. return ErrorResult.build(DefaultErrorResult.PARAM_VALID_ERROR, defaultMsg.get(0));
  23. }
  24. @ExceptionHandler(value = MissingServletRequestParameterException.class)
  25. @ResponseStatus(HttpStatus.BAD_REQUEST)
  26. public ErrorResult handleMissingServletRequestParameterException(MissingServletRequestParameterException e) {
  27. log.error("{}", e.getMessage(), e);
  28. log.error("ParameterName: {}", e.getParameterName());
  29. log.error("ParameterType: {}", e.getParameterType());
  30. return ErrorResult.build(DefaultErrorResult.PARAM_VALID_ERROR, e.getMessage());
  31. }
  32. @ExceptionHandler(value = ConstraintViolationException.class)
  33. @ResponseStatus(HttpStatus.BAD_REQUEST)
  34. public ErrorResult handleBindGetException(ConstraintViolationException e) {
  35. log.error("{}", e.getMessage(), e);
  36. List<String> defaultMsg = e.getConstraintViolations()
  37. .stream()
  38. .map(ConstraintViolation::getMessage)
  39. .collect(Collectors.toList());
  40. return ErrorResult.build(DefaultErrorResult.PARAM_VALID_ERROR, defaultMsg.get(0));
  41. }
  42. @ExceptionHandler(HttpMessageNotReadableException.class)
  43. @ResponseStatus(HttpStatus.BAD_REQUEST)
  44. public ErrorResult error(HttpMessageNotReadableException e){
  45. log.error("{}", e.getMessage(), e);
  46. return DefaultErrorResult.JSON_PARSE_ERROR;
  47. }
  48. /**
  49. * 通用业务异常
  50. *
  51. * @param e
  52. * @return
  53. */
  54. @ExceptionHandler(value = CommonException.class)
  55. @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
  56. public ErrorResult handleCommonException(CommonException e) {
  57. log.error("{}, {}", e.getMessage(), e);
  58. if (e.getErrorResult() != null) {
  59. return e.getErrorResult();
  60. }
  61. return new ErrorResult(DefaultErrorResult.CUSTOM_ERROR.getCode(), e.getMessage());
  62. }
  63. /**
  64. * 其他运行时异常
  65. * @param e
  66. * @return
  67. */
  68. @ExceptionHandler(value = Exception.class)
  69. @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
  70. public ErrorResult handleDefaultException(Exception e) {
  71. log.error("{}, {}", e.getMessage(), e);
  72. return DefaultErrorResult.SYSTEM_ERROR;
  73. }
  74. }

2.3 自定义校验注解

javax.validation 中提供了很多用于校验的注解,常见的如:@NotNull、@Min、@Max 等等,但可能这些注解不够,需要自定义注解。例如咱们自定义一个注解 @OneOf,该注解对应字段的值只能从 value 中选择:使用方式为:

  1. @OneOf(value = {"MacOS", "Windows", "Linux"})

首先定义一个注解

com.yygnb.demo.common.validator.OneOf

  1. @Target(ElementType.FIELD)
  2. @Retention(RetentionPolicy.RUNTIME)
  3. @Documented
  4. @Constraint(validatedBy = OneOfValidator.class)
  5. public @interface OneOf {
  6. String message() default "只能从备选值中选择";
  7. Class<?>[] groups() default {};
  8. Class<? extends Payload>[] payload() default {};
  9. String[] value();
  10. }

定义注解时,@Constraint(validatedBy = OneOfValidator.class)表示校验使用 OneOfValidator 类进行校验,我们需要编写这个类。

com.yygnb.demo.common.validator.OneOfValidator

  1. public class OneOfValidator implements ConstraintValidator<OneOf, String> {
  2. private List<String> list;
  3. @Override
  4. public void initialize(OneOf constraintAnnotation) {
  5. list = Arrays.asList(constraintAnnotation.value());
  6. }
  7. @Override
  8. public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
  9. if (s == null) {
  10. return true;
  11. }
  12. return list.contains(s);
  13. }
  14. }

这样便实现一个自定义校验注解了。

2.4 RequestBody 参数校验

新增电脑接口为例,首先需要在方法的参数前面使用注解 @Valid@Validated 修饰。在此处使用两者之一都可以,前者是 javax.validation 中提供的,后者是 springframework 中提供的:

  1. @Operation(summary = "新增电脑")
  2. @PostMapping()
  3. public Computer save(@RequestBody @Validated Computer computer) {
  4. computer.setId(null);
  5. this.computerService.save(computer);
  6. return computer;
  7. }

在 RequestBody 参数对应的 DTO / 实体中,对需要校验的字段加上校验注解。例如 操作系统operation 只能从 "MacOS", "Windows", "Linux" 中选择;年份year不能为空且长度为4:

  1. @OneOf(value = {"MacOS", "Windows", "Linux"})
  2. @Schema(title = "操作系统")
  3. private String operation;
  4. @NotNull(message = "不能为空")
  5. @Length(min = 4, max = 4)
  6. @Schema(title = "年份")
  7. private String year;

此时重启服务,调用新增电脑接口时,就会进行校验。

2.5 路径参数和RequestParam校验

路径参数和没有封装为实体的 RequestParam 参数,首先需要在参数前面加上校验注解,然后需要在 Controller 类上面加上注解 @Validated 才会生效。如在分页列表接口中,要求参数当前页 page 大于 0:

  1. public Page<Computer> findPage(@PathVariable @Min(1) Integer page, @PathVariable @Max(10) Integer size) {
  2. ...
  3. }

本文简单介绍了统一异常处理和参数校验,本节的代码还有很多优化空间,在后面的实战部分逐一完善。



\/ 程序员优雅哥(youyacoder),今日学习到此结束~~~

全局异常处理及参数校验-SpringBoot 2.7 实战基础 (建议收藏)的更多相关文章

  1. springboot+aop+自定义注解,打造通用的全局异常处理和参数校验切面(通用版)

    一.引入相应的maven依赖 <dependency> <groupId>org.springframework.boot</groupId> <artifa ...

  2. springboot项目--传入参数校验-----SpringBoot开发详解(五)--Controller接收参数以及参数校验----https://blog.csdn.net/qq_31001665/article/details/71075743

    https://blog.csdn.net/qq_31001665/article/details/71075743 springboot项目--传入参数校验-----SpringBoot开发详解(五 ...

  3. 配置 Druid 数据源及密码加密-SpringBoot 2.7 实战基础

    在SpringBoot中配置 Druid 数据源及密码加密的方法 前文集成 MyBatis Plus,实现了一组增删改查接口.在启动服务时,从控制台中可以看出 Spring Boot 默认使用 Hik ...

  4. springboot 全局异常处理

    springboot 全局异常处理 研究了半天springboot的全局异常处理,虽然还是需要再多整理一下,但是对于常见的404和500足以可以区分开,能够根据这两个异常分别处理 首先配置视图解析路径 ...

  5. 【全网最全】springboot整合JSR303参数校验与全局异常处理

    一.前言 我们在日常开发中,避不开的就是参数校验,有人说前端不是会在表单中进行校验的吗?在后端中,我们可以直接不管前端怎么样判断过滤,我们后端都需要进行再次判断,为了安全.因为前端很容易拜托,当测试使 ...

  6. SpringBoot Validation优雅的全局参数校验

    前言 我们都知道在平时写controller时候,都需要对请求参数进行后端校验,一般我们可能会这样写 public String add(UserVO userVO) { if(userVO.getA ...

  7. 补习系列(4)-springboot 参数校验详解

    目录 目标 一.PathVariable 校验 二.方法参数校验 三.表单对象校验 四.RequestBody 校验 五.自定义校验规则 六.异常拦截器 参考文档 目标 对于几种常见的入参方式,了解如 ...

  8. SpringBoot Validation参数校验 详解自定义注解规则和分组校验

    前言 Hibernate Validator 是 Bean Validation 的参考实现 .Hibernate Validator 提供了 JSR 303 规范中所有内置 constraint 的 ...

  9. 【springboot】@Valid参数校验

    转自: https://blog.csdn.net/cp026la/article/details/86495659 扯淡: 刚开始写代码的时候对参数的校验要么不做.要么写很多类似 if( xx == ...

随机推荐

  1. CentOS7及以下版本安装禅道

    由于是CentOS7以及以下系统,禅道已经集成了 Apache Nginx Mysql 服务,不需要我们再次安装搭建,我们只进行解压使用就好: 一.进行下载安装 1.在终端命令中输入以下命令确认系统是 ...

  2. pytorch基础常识

     

  3. Dev C++编写C/C++程序 出现[Error] ld returned 1 exit status报错分析及解决

    debug系列第一弹,不知道大家写程序的时候是不是都遇到过如题的报错. 我本人是经常遇到这行熟悉的令人不知所措的报错,可能是我太笨了 有时候百度无果也差不到原因,那就汇总一下目前我遇到的情况吧--持续 ...

  4. Redis初启(一)

    1.数据库存存储性能优化 在mysql的文章专题中我写过了关于传统关系型数据库的一些优化思路,整体来说,通过优化之后能够提升程序访问数据库的计算性能.但是还是有一些情况,即便是优化之后,使用传统关系型 ...

  5. 【python基础】第04回 变量常量

    本章内容概要 1. python 语法注释 2. python 语法之变量常量 3. python 基本数据类型(整型(int),浮点型(float),字符串(str)) 本章内容详解 1. pyth ...

  6. [LINUX] 像电影里的黑客一样用 terminal 作为日常开发

    目录 1.效果预览 2.具体实现 2.1 定位鼠标位置 2.2 获取屏幕位置 2.3 计算鼠标在哪个窗口 2.4 1920x1080 平铺效果设计 2.5 1280x1024 平铺效果设计 3 代码 ...

  7. 腾讯云数据库公有云市场稳居TOP 2!

    7月4日,国际权威机构IDC发布的<2021年下半年中国关系型数据库软件市场跟踪报告>显示,腾讯云数据库在关系型数据库软件市场(公有云模式)中,位列第二. IDC报告显示,2021下半年中 ...

  8. js导入excel&导出excel

    Excel导入 html代码 <button style={{ color: '#1890ff', fontSize: '14px', cursor: 'pointer' }} onClick= ...

  9. RapidEye快鸟、SPOT卫星遥感影像数据

    ​目前地理遥感生态网平台已发布高分辨率卫星遥感影像数据. 数据样例:百度云下载链接:https://pan.baidu.com/s/17ofPwpDM3OCHnE-LuhvUp 提取码:i0m4   ...

  10. 集合—collection、iterator遍历集合

    一.collection接口 1.collection常用方法 点击查看代码 @Test public void test(){ //contains() Collection coll = new ...