项目中经常会出现一些异常,比如在新增项目的时候必要的字段没有填写。在springboot项目中,遇到异常会往上抛出给调用方,DAO层遇到异常抛给Service层,Service层遇到异常抛给Controller层,Controller层遇到异常就抛给了SpringMVC框架。对于前端来说是无法感知的,并不知道具体发生了什么错误。如果使用try-catch的话代码又过于冗余,所以需要把异常处理的逻辑统一进行管理。
上图是设想的解决方案,通过SpringMVC提供的控制器增强类统一由一个类完成异常的捕获,背后是AOP机制。
实现过程:
枚举类CommonError用于定义常见的错误类型
package com.gavin.base.exception;
/**
* @author Gavin
* @description 通用错误信息
* @date 2024/10/15
**/
public enum CommonError {
UNKOWN_ERROR("执行过程异常,请重试。"),
PARAMS_ERROR("非法参数"),
OBJECT_NULL("对象为空"),
QUERY_NULL("查询结果为空"),
REQUEST_NULL("请求参数为空");
private String errMessage;
public String getErrMessage() {
return errMessage;
}
private CommonError( String errMessage) {
this.errMessage = errMessage;
}
}
自定义异常类EffecttiveStudyException用来封装项目中特定异常信息
package com.gavin.base.exception;
/**
* @author Gavin
* @description 自定义高效学习在线类项目异常类
* @date 2024/10/15
**/
public class EffectiveStudyException extends RuntimeException{
private String errMessage;
public EffectiveStudyException() {
super();
}
public EffectiveStudyException(String errMessage) {
super(errMessage);
this.errMessage = errMessage;
}
public String getErrMessage() {
return errMessage;
}
public static void cast(CommonError commonError){
throw new EffectiveStudyException(commonError.getErrMessage());
}
public static void cast(String errMessage){
throw new EffectiveStudyException(errMessage);
}
}
错误响应类RestErrorResponse用于统一封装并返回给前端的错误信息
package com.gavin.base.exception;
import java.io.Serializable;
/**
* @author Gavin
* @description 和前端约定返回的异常信息
* @date 2024/10/15
**/
public class RestErrorResponse implements Serializable {
private String errMessage;
public RestErrorResponse(String errMessage){
this.errMessage= errMessage;
}
public String getErrMessage() {
return errMessage;
}
public void setErrMessage(String errMessage) {
this.errMessage = errMessage;
}
}
定义全局异常处理器GlobalExceptionHandler类,这个类可以认为就是AOP机制中的一个切面,在系统各种专门处理所有控制器方法抛出的异常。以下是两个比较重要的注解:
@ControllerAdvice:用来标识一个全局异常处理类,所有被@Controller注解的控制器中的异常都会被它捕获。
@ExceptionHandler:用于捕获特定类型的异常。在这里,EffectiveStudyException 和其他普通异常都分别有处理方法。
package com.gavin.base.exception;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
/**
* @author Gavin
* @description 全局异常处理器
* @date 2024/10/15
**/
@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {
@ResponseBody
@ExceptionHandler(EffectiveStudyException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public RestErrorResponse customException(EffectiveStudyException e) {
log.error("【系统异常】{}",e.getErrMessage(),e);
return new RestErrorResponse(e.getErrMessage());
}
@ResponseBody
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public RestErrorResponse exception(Exception e) {
log.error("【系统异常】{}",e.getMessage(),e);
return new RestErrorResponse(CommonError.UNKOWN_ERROR.getErrMessage());
}
}
通过上述全局异常处理器,任何抛出的EffectiveStudyException或未被预期的异常都会被这个类统一处理,并返回一个标准的错误信息。
上述自定义后,对于抛出异常的地方可以进行替换,例如
throw new RuntimeException("课程的价格不能为空且必须大于0");
可以替换为
throw new EffectiveStudyException("课程的价格不能为空且必须大于0");
或者
EffectiveStudyException.cast("课程的价格不能为空且必须大于0");
两种形式是等价的,但后者更为简洁。如果捕获了上述异常,前端返回的就是对应内容的json数据信息,如下:
当存在其他异常时,如controller中存在一个除数为0的情形,如果在Controller中没有进行显示的try-catch捕获,异常会被直接抛出,由SpringMVC框架捕获并向上传递,SpringMVC会将这个异常传递给全局异常处理器类(GlobalExceptionHandler类),由于不是EffectiveStudyException类型的错误,所以交由兜底Exception处理,返回了UNKOWN_ERROR对应的错误信息(这里对于常见错误可定义特定信息返回)。如下:
这里可能会存在疑问,为什么不直接返回e.getMessage()的具体信息而是笼统的执行过程异常请重试?考虑的因素有:异常具体信息中可能包含敏感的系统内部信息,如数据库查询语句,服务器路径及框架细节等信息,有风险。另外,复杂的信息用户体验不好,提示信息应该简洁友好。但实际中开发环境有时候是有必要放出具体异常信息的,生产环境不适合放具体异常信息。