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

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

1 异常统一处理

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

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

1.1 错误响应结构

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

com.yygnb.demo.common.domain.ErrorResult

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ErrorResult implements Serializable { private static final long serialVersionUID = -8738363760223125457L; /**
* 错误码
*/
private String code; /**
* 错误消息
*/
private String msg; public static ErrorResult build(ErrorResult commonErrorResult, String msg) {
return new ErrorResult(commonErrorResult.getCode(), commonErrorResult.getMsg() + " " + msg);
}
}

1.2 通用错误响应常量

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

com.yygnb.demo.common.exception.DefaultErrorResult

public interface DefaultErrorResult {

    ErrorResult SYSTEM_ERROR = new ErrorResult("C00001", "系统异常");
ErrorResult CUSTOM_ERROR = new ErrorResult("C99999", "自定义异常");
ErrorResult PARAM_BIND_ERROR = new ErrorResult("C00003", "参数绑定错误:");
ErrorResult PARAM_VALID_ERROR = new ErrorResult("S00004", "参数校验错误:");
ErrorResult JSON_PARSE_ERROR = new ErrorResult("S00005", "JSON转换异常");
ErrorResult CODE_NOT_FOUND = new ErrorResult("S00006", "根据编码没有查询到对象");
ErrorResult ID_NOT_FOUND = new ErrorResult("S00007", "根据ID没有查询到对象");
}

1.3 通用异常类定义

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

com.yygnb.demo.common.exception.CommonException

@EqualsAndHashCode(callSuper = true)
@Data
public class CommonException extends RuntimeException { protected ErrorResult errorResult; public CommonException(String message) {
super(message);
} public CommonException(String message, Throwable cause) {
super(message, cause);
} public CommonException(Throwable cause) {
super(cause);
} protected CommonException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
} public CommonException(String code, String msg) {
super(msg + "(" + code + ")");
this.errorResult = new ErrorResult(code, msg);
} public CommonException(ErrorResult errorResult) {
super(errorResult.getMsg() + "(" + errorResult.getCode() + ")");
this.errorResult = errorResult;
}
}

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

1.4 全局异常处理

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

com.yygnb.demo.common.exception.CommonExceptionHandler

@Slf4j
@RestControllerAdvice
public class CommonExceptionHandler { /**
* 通用业务异常
*
* @param e
* @return
*/
@ExceptionHandler(value = CommonException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ErrorResult handleCommonException(CommonException e) {
log.error("{}, {}", e.getMessage(), e);
if (e.getErrorResult() != null) {
return e.getErrorResult();
}
return new ErrorResult(DefaultErrorResult.CUSTOM_ERROR.getCode(), e.getMessage());
} /**
* 其他运行时异常
* @param e
* @return
*/
@ExceptionHandler(value = Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ErrorResult handleDefaultException(Exception e) {
log.error("{}, {}", e.getMessage(), e);
return DefaultErrorResult.SYSTEM_ERROR;
}
}

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

1.5 测试统一异常处理

在 DemoController 中抛出一个 CommonException:

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

启动服务,访问:

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

结果返回:

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

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

2 参数校验

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

2.1 添加依赖

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

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

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

2.2 完善异常处理类

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

@Slf4j
@RestControllerAdvice
public class CommonExceptionHandler { @ExceptionHandler(value = BindException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResult handleBindException(BindException e) {
log.error("{}", e.getMessage(), e);
List<String> defaultMsg = e.getBindingResult().getAllErrors()
.stream()
.map(ObjectError::getDefaultMessage)
.collect(Collectors.toList());
return ErrorResult.build(DefaultErrorResult.PARAM_BIND_ERROR, defaultMsg.get(0));
} @ExceptionHandler(value = MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResult handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
log.error("{}", e.getMessage(), e);
List<String> defaultMsg = e.getBindingResult().getFieldErrors()
.stream()
.map(fieldError -> "【" + fieldError.getField() + "】" + fieldError.getDefaultMessage())
.collect(Collectors.toList());
return ErrorResult.build(DefaultErrorResult.PARAM_VALID_ERROR, defaultMsg.get(0));
} @ExceptionHandler(value = MissingServletRequestParameterException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResult handleMissingServletRequestParameterException(MissingServletRequestParameterException e) {
log.error("{}", e.getMessage(), e);
log.error("ParameterName: {}", e.getParameterName());
log.error("ParameterType: {}", e.getParameterType());
return ErrorResult.build(DefaultErrorResult.PARAM_VALID_ERROR, e.getMessage());
} @ExceptionHandler(value = ConstraintViolationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResult handleBindGetException(ConstraintViolationException e) {
log.error("{}", e.getMessage(), e);
List<String> defaultMsg = e.getConstraintViolations()
.stream()
.map(ConstraintViolation::getMessage)
.collect(Collectors.toList());
return ErrorResult.build(DefaultErrorResult.PARAM_VALID_ERROR, defaultMsg.get(0));
} @ExceptionHandler(HttpMessageNotReadableException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResult error(HttpMessageNotReadableException e){
log.error("{}", e.getMessage(), e);
return DefaultErrorResult.JSON_PARSE_ERROR;
} /**
* 通用业务异常
*
* @param e
* @return
*/
@ExceptionHandler(value = CommonException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ErrorResult handleCommonException(CommonException e) {
log.error("{}, {}", e.getMessage(), e);
if (e.getErrorResult() != null) {
return e.getErrorResult();
}
return new ErrorResult(DefaultErrorResult.CUSTOM_ERROR.getCode(), e.getMessage());
} /**
* 其他运行时异常
* @param e
* @return
*/
@ExceptionHandler(value = Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ErrorResult handleDefaultException(Exception e) {
log.error("{}, {}", e.getMessage(), e);
return DefaultErrorResult.SYSTEM_ERROR;
}
}

2.3 自定义校验注解

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

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

首先定义一个注解

com.yygnb.demo.common.validator.OneOf

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

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

com.yygnb.demo.common.validator.OneOfValidator

public class OneOfValidator implements ConstraintValidator<OneOf, String> {

    private List<String> list;

    @Override
public void initialize(OneOf constraintAnnotation) {
list = Arrays.asList(constraintAnnotation.value());
} @Override
public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
if (s == null) {
return true;
}
return list.contains(s);
}
}

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

2.4 RequestBody 参数校验

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

@Operation(summary = "新增电脑")
@PostMapping()
public Computer save(@RequestBody @Validated Computer computer) {
computer.setId(null);
this.computerService.save(computer);
return computer;
}

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

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

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

2.5 路径参数和RequestParam校验

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

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

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



\/ 程序员优雅哥(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. java和.net 双语言开发框架,开源的PaaS平台

    当下,我国国内的PaaS平台正在蓬勃发展,各式各样的PaaS平台层出不穷,但万变不离其宗,一个优秀的PaaS平台总有自己独树一帜或与众不同的地方.那么,首先我们要了解下什么是PaaS平台?PaaS是( ...

  2. BI 如何让SaaS产品具有 “安全感”和“敏锐感”(上)

    SaaS模式一经推出,凭借自身的高性价比.低维护成本,无需软硬件维护.无需运维等明晃晃的优点,得到了爆发式的增长,甚至全面改变了软件的开发模式.各位老总的问候语,不知从什么时候开始,都变成了:&quo ...

  3. python+selenium 自动化测试——显式等待详解

    1.前言 之前有提到过等待函数,等待函数分为:强制等待(sleep).隐式等待(implicitly_wait),显示等待(WebDriverWait),这次以显示等待方式专门做一次总结,因为我个人是 ...

  4. Egg上层框架CabloyJS是如何输出SQL语句日志的?

    背景 在Egg开发实践中,经常会遇到一个问题:如何查看刚刚执行过的Egg组装的原生SQL语句呢? 1. 现有方案 可以直接在项目的config配置文件中添加MySQL配置debug: true.这会启 ...

  5. ESP8266 系统环境搭建

    1. 前言 因为ESP8266/ESP32这个开发环境没少折腾,是真没见过这么难搞又不清晰的环境. 简单开发可以使用Arduino IDE ,这个平台还是挺好的.开发使用Arduino的函数库,很高效 ...

  6. Chrome自带功能实现网页截图

    更新记录 本文迁移自Panda666原博客,原发布时间:2021年6月28日. 很简单,按下Ctrl+Shift+P,打开命令行窗口,如下图所示. 输入命令. Capture full size sc ...

  7. nginx https证书配置

    1. Nginx配置 server { listen 443; #指定ssl监听端口 server_name www.example.com; ssl on; #开启ssl支持 ssl_certifi ...

  8. JavaScript写放大镜效果

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  9. SAP 查看在线用户

    SM04 可查看服务器全部客户端(Client)的用户的在线状态,并可以结束指定用户的会话状态,也就是强制踢出用户.

  10. Android multiple back stacks导航的几种实现

    Android multiple back stacks导航 谈谈android中多栈导航的几种实现. 什么是multiple stacks 当用户在app里切换页面时, 会需要向后回退到上一个页面, ...