统一异常捕获
使用注解的方式统一异常处理时,可以使用@RestControllerAdvice配合@ExceptionHandler拦截控制器的异常。(@RestControllerAdvice = @ControllerAdvice + @ResponseBody)
@RestControllerAdvice注解是Spring框架中用于定义全局异常处理类的注解,它结合了@ControllerAdvice和@ResponseBody的功能,用于RESTful风格的应用程序。
当SpringBoot应用启动时,Spring容器会自动扫描并加载带有@RestControllerAdvice注解的类,将其实例化并加载到容器中进行管理。一旦控制层在处理请求时抛出异常,Spring MVC的异常处理机制就会被出发,并且@RestControllerAdvice中定义的异常处理方法将被调用。
在标注了的@RestControllerAdvice类中,我们可以定义多个@ExceptionHandler方法,这些方法会根据其参数类型与抛出的异常类型进行匹配,实现对特定异常的捕获和处理。
使用@RestControllerAdvice可以避免在每个Controller中重复编写异常处理代码,实现代码的复用和集中管理。异常处理方法可以返回ResponseEntity对象,通过这种方法可以自定义响应的状态码和响应体。
常见可被捕获的异常类型
- Exception:普通的Java异常,是大多数其他异常的基类。
- RuntimeException:运行时异常,包括NullPointerException、IllegalArgumentException等。
- HTTP状态码异常:例如,HttpStatus.NOT_FOUND表示资源未找到,HttpStatus.BAD_REQUEST表示请求错误等。
- ValidationException:数据验证异常,例如使用JSR-303或Hibernate Validator进行的数据验证失败。
- MethodArgumentNotValidException:请求方法参数验证失败时抛出的异常。
- HttpMessageNotReadableException:当请求的HTTP消息无法读取或解析时抛出的异常,例如请求体格式错误。
- HttpRequestMethodNotSupportedException:当请求的HTTP方法不受支持时抛出的异常。
- MissingServletRequestParameterException:当请求缺少必需的参数时抛出的异常。
- BindException:数据绑定失败时抛出的异常,通常与表单提交相关。
- ConstraintViolationException:当使用Bean验证(如Hibernate Validator)时,违反约束条件时抛出的异常。
- NoHandlerFoundException:当找不到适合处理当前请求的处理程序时抛出的异常。
- 自定义的异常类。
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 全局异常捕捉处理
*/
@ExceptionHandler(value = Exception.class)
public ResultEntity<Void> errorHandler(Exception ex) {
log.error("errorHandler----> 全局异常 ex = ", ex);
return ResultEntity.createResponseEntity(ResultEntity.CODE_ERROR, ex.getMessage());
}
/**
* 拦截捕捉自定义异常 GlobalException.class
*/
@ExceptionHandler(value = GlobalException.class)
public ResultEntity<Void> globalExceptionHandler(GlobalException ex) {
log.error("myErrorHandler----> 全局异常 ex = ", ex);
return ResultEntity.createResponseEntity(ex.getCode(), ex.getMsg());
}
/**
* 拦截参数校验异常 MethodArgumentNotValidException.class
*/
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public ResultEntity<String> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
List<ObjectError> allErrors = e.getBindingResult().getAllErrors();
String message = allErrors.stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining(";"));
log.error("ArgumentNotValidException ----> 接口请求参数异常 ex = {}", message);
return ResultEntity.paramError(message);
}
}
如何精准捕获SQLException
统一异常处理时,如果有SQL执行的错误,会被Exception.class捕获,此时如果直接返回ex.getMessage()的话,会提示一长串错误信息,更严重的是会暴露数据库表结构,增加数据库的漏洞风险。直接把ex.getMessage()替换为“XXX异常”又比较笼统,所以最好是能够处理SQL操作抛出的异常。
通过查看日志发现,我当前抛出的异常的是DuplicateKeyException:
org.springframework.dao.DuplicateKeyException:
### Error updating database. Cause: java.sql.SQLIntegrityConstraintViolationException: Duplicate entry '0-MY_CUSTOMER-0' for key 'tb_process_model.uk_corporation_num_model_key_enable_status'
### The error may exist in com/huayu/processflow/dao/mapper/ProcessModelMapper.java (best guess)
### The error may involve com.huayu.processflow.dao.mapper.ProcessModelMapper.insert-Inline
### The error occurred while setting parameters
### SQL: INSERT INTO tb_process_model ( corporation_num, model_name, model_content, model_key, remark, enable_status, use_scope, total_point, recheck_type, updater_num, updater_name, creator_num, creator_name, create_time, update_time ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )
### Cause: java.sql.SQLIntegrityConstraintViolationException: Duplicate entry '0-MY_CUSTOMER-0' for key 'tb_process_model.uk_corporation_num_model_key_enable_status'
; Duplicate entry '0-MY_CUSTOMER-0' for key 'tb_process_model.uk_corporation_num_model_key_enable_status'
这个异常是由Spring框架内部定义的,继承自Exception,所以是被Exception捕获,继承图如下:
如果想要捕获此异常,最匹配的还是使用DuplicateKeyException.class。加入以下代码,可以正常捕获唯一索引冲突异常。
@ExceptionHandler(value = DuplicateKeyException.class)
public ResultEntity<Void> duplicateKeyExceptionHandler(DuplicateKeyException ex) {
log.error("duplicateKeyExceptionHandler: {}", ex.toString());
return ResultEntity.createResponseEntity(ResultEntity.CODE_ERROR, "数据库操作出现错误");
}
但这个异常它的底层还是JDBC的SQLIntegrityConstraintViolationException,就想知道它是怎么转换为DuplicateKeyException的。
通过查看源码发现,可以通过org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator来将SQLException转换为自身定义的异常,方便外部捕获使用。sqlErrorCodes,即同一包下的SQLErrorCodes。
在SQLErrorCodes中,定义了一系列的特定异常错误码。
在同一包下,存在一个配置文件,定义了Spring配置的JDBC异常码,涵盖好几种数据库,其中MySQL的定义如下:
所以,我们可以知道MySQL唯一索引或主键冲突时,返回的就是1062的错误码,并通过Spring中对SQL异常进行转换得到DuplicateKeyException类型的异常。但是这个异常的内部cause还是属于SQL异常。
RestControllerAdvice的拦截顺序
除了上述的方法,还可以通过RestControllerAdvice的拦截顺序来精准捕获SQLException。在出现多个处理器时,RestControllerAdvice的拦截顺序会有下面两种情况。
1、存在一个类中
调试源码可见匹配顺序为:异常层级高的优先调用,即子类异常处理器优先。
2、存在不同的类中
源码可见:
通过以上代码可以看到与多个异常处理类放入LinkedHashMap的顺序有关。继续看源码:
此处对异常类进行了排序:
此处看到可以利用Order指定顺序,如果没有,则默认最小顺序;
那么,如果都没有指定顺序的话,那就是list中的顺序,源码:
获取所有的beanDefinitionNames,再遍历寻找标注了异常处理注解的类,放入list中(存在父容器的合并后再遍历寻找)。
所以,通过加一个新的处理器,并赋予更高的Order级别,也可以捕获SQLException。
@Order(value = Ordered.HIGHEST_PRECEDENCE)
@RestControllerAdvice
@Slf4j
public class SQLExceptionHandler {
@ExceptionHandler(value = SQLException.class)
public ResultEntity<Void> sqlErrorHandler(SQLException ex) {
log.error("sqlErrorHandler: {}", ex.toString());
return ResultEntity.createResponseEntity(ResultEntity.CODE_ERROR, "数据库操作出现错误");
}
}
当多个 @RestControllerAdvice
类存在时,它们的拦截顺序可以通过几种方式来确定:
- @Order 注解:可以使用
@Order
注解来指定类的加载顺序,值越小优先级越高 。 - @Priority 注解:与
@Order
类似,@Priority
注解来自 JSR 250 标准,也用于指定优先级,且优先级比@Order
更高 。 - @Primary 注解:当存在多个候选 Bean 时,标记为
@Primary
的 Bean 将被优先加载 。 - Ordered 接口:实现
Ordered
接口的类可以通过实现getOrder()
方法来指定顺序,值越小优先级越高 。