26.1.从零开始学springboot-全局异常处理
前言
无论什么项目,异常处理和数据校验都显得尤其重要。作为一个开发,我们不应该不对数据检验就直接入库,我们也不应该傻乎乎的把乱糟糟的报错信息直接返回给用户。本章,我们就讲讲sprinboot的异常和数据校验处理。
异常处理流程
- 自定义异常类型
- 自定义错误代码及错误信息
- 对于可预知的异常由程序员在代码中主动抛出,有springboot统一捕获,可预知异常是程序员在代码中手动抛出本系统定义的特定业务异常类型,由于是程序员抛出的异常,通常异常信息比较齐全,程序员在抛出时会指定错误代码及错误信息,获取异常信息也比较方便
- 对于不可预知的异常(运行时异常)由springboot统一捕获Exception类型的异常,不可预知的异常通常是由于系统出现bug、或一些不可抗拒的错误(比如网络中断、服务器宕机等),异常类型为RuntimeException类型(运行时异常)
- 可预知异常及不可预知异常最终都会采用统一的信息格式(错误代码+错误信息)来表示,最终也会随请求响应给客户端
默认异常处理
默认情况下,sprinboot为两种情况提供了不同的响应方式。
-
浏览器异常处理
浏览器客户端请求一个不存在的页面或服务端处理发生异常时,一般情况下浏览器默认发送的请求头中Accept: text/html,sprinboot默认会响应一个html文档内容,称作“Whitelabel Error Page”。
1.png -
非浏览器异常处理
如postman等调试工具发送请求一个不存在的url或服务端处理发生异常时,sprinboot会返回类似如下的Json格式字符串信息
2.png
默认异常处理原理简介
sprinboot默认提供了程序出错的结果映射路径“/error”。这个“/error”请求会在BasicErrorController类中处理
我们找到该类,简单看些核心实现
3.png
不难看出其内部是通过判断请求头中的Accept的内容是否为text/html来区分请求是来自浏览器,还是其它接口的调用,以此来决定返回页面视图还是 json 消息内容。
全局异常处理
以上介绍了springboot默认的异常处理机制,那么,问题来了,我们实际业务中肯定是需要统一的消息体的。所以,我们就需要实现ErrorController这个接口,来统一返回的消息体。
(PS:也可以直接继承BasicErrorController,该中已经有一个默认处理text/html的方法。如果你想添加一个新内容类型(如JSON)的处理程序,你需要添加一个具有@RequestMapping属性的方法,然后返回你自定义的实体类)。
package com.mrcoder.sbexceptionvalidator.controller;
import com.mrcoder.sbexceptionvalidator.common.model.ResponseInfo;
import com.mrcoder.sbexceptionvalidator.model.Person;
import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
@RestController
public class ExceptionValidatorController implements ErrorController {
@RequestMapping("/error")
public ResponseInfo handleError(HttpServletRequest request) {
//获取statusCode:401,404,500
Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code");
return ResponseInfo.fail(statusCode);
}
@Override
public String getErrorPath() {
return "/error";
}
}
此时,再访问不存在的url你会发现返回的消息体已经是我们定义的了。
浏览器:
5.png
非浏览器:
4.png
自定义全局异常处理
上面我们提到ErrorController可对全局错误进行处理,但是有如下几点问题:
- 获取不到异常的具体信息
- 无法根据异常类型进行不同的响应,例如对自定义异常的处理。(PS:实际开发中我们常常会定义各种业务的自定义异常)。
实际上,当出现错误,如获取值为空或出现异常时,我们并不希望用户看到异常的具体信息,而是希望对对应的错误和异常做相应提示
在MVC框架中很多时候会出现执行异常,那我们就需要加try/catch进行捕获,如果service层和controller层都加上,那就会造成代码冗余。
而@ControllerAdvice可对全局异常进行捕获,包括自定义异常(也就是我们自定定义的业务异常)
需要注意的是,@ControllerAdvice是应用于对springmvc中的控制器抛出的异常进行处理,而对于404/500这类的异常不会进入控制器处理的异常不起作用,所以404这类的还是要依靠实现ErrorController接口来处理。
我们定义一个错误码枚举类
package com.mrcoder.sbexceptionvalidator.common.status;
import com.mrcoder.sbexceptionvalidator.common.model.ResponseInfo;
/**
* @Description: 系统错误类型枚举类
*/
public enum ErrorCodeEnum {
SYSTEM_ERROR(ResponseInfo.FAIL_CODE, "系统异常,请稍后重试"),
NO_AUTHORITY(ResponseInfo.FAIL_CODE, "无权访问"),
PARAM_ERROR(ResponseInfo.FAIL_CODE, "参数非法"),
NOT_LOG(ResponseInfo.FAIL_CODE, "当前用户未登录");
private Integer errCode;
private String errMsg;
private ErrorCodeEnum(Integer errCode, String errMsg) {
this.errCode = errCode;
this.errMsg = errMsg;
}
public Integer getErrCode() {
return errCode;
}
public String getErrMsg() {
return errMsg;
}
}
再定义一个业务异常类型
package com.mrcoder.sbexceptionvalidator.common.exception;
import com.mrcoder.sbexceptionvalidator.common.model.ResponseInfo;
import com.mrcoder.sbexceptionvalidator.common.status.ErrorCodeEnum;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
/**
* @Description: 全局业务异常类型
*/
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class BusinessException extends RuntimeException {
private static final long serialVersionUID = -6842004487143726249L;
private Integer errCode;
private String errMsg;
public BusinessException(String errMsg) {
super();
this.errCode = ResponseInfo.FAIL_CODE;
this.errMsg = errMsg;
}
public BusinessException(ErrorCodeEnum errorCodeEnum) {
super();
this.errCode = errorCodeEnum.getErrCode();
this.errMsg = errorCodeEnum.getErrMsg();
}
}
最后全局异常处理器
package com.mrcoder.sbexceptionvalidator.common.exception;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.validation.ConstraintViolationException;
import com.mrcoder.sbexceptionvalidator.common.model.ResponseInfo;
import org.springframework.validation.ObjectError;
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;
/**
* @Description: 全局异常捕获处理器
*/
@ControllerAdvice
public class GlobalExpectionHandler {
/**
* 异常捕获处理
*/
@ExceptionHandler(value = Exception.class)
@ResponseBody
public ResponseInfo expectionHandler(HttpServletRequest request, Exception e) {
if (e instanceof MethodArgumentNotValidException) { // JavaBean参数校验异常
MethodArgumentNotValidException exception = (MethodArgumentNotValidException) e;
List<ObjectError> allErrors = exception.getBindingResult().getAllErrors(); // 取出错误信息
ObjectError error = allErrors.get(0); // 只返回第一个错误信息即可
return ResponseInfo.fail(error.getDefaultMessage());
} else if (e instanceof ConstraintViolationException) { // Controller方法参数校验异常
// 错误异常
String message = ((ConstraintViolationException) e).getConstraintViolations().iterator().next().getMessage();
return ResponseInfo.fail(message);
} else if (e instanceof BusinessException) {
// 自定义业务异常
BusinessException exception = (BusinessException) e;
return ResponseInfo.fail(exception.getErrMsg());
} else {
// 系统异常,打印错误日志
e.printStackTrace();
return ResponseInfo.fail("系统异常,请稍后重试");
}
}
}
项目地址
https://github.com/MrCoderStack/SpringBootDemo/tree/master/sb-exception-validator