前言
上一章对统一返回值进行封装,但是都是基于正常情况下的返回,系统难免会出现异常的情况,我们不可能在每一个地方都try...catch
,但是异常有分为很多种类,我们需要对异常进行统一的拦截,希望友好的返回给客户端信息,而不是报一堆错误代码…
Spring中提供了一个@RestControllerAdvice
它是 @ControllerAdvice
和 @ResponseBody
的结合体,意味着使用该注解的类可以为所有的@RequestMapping
处理方法提供通用的异常处理和数据绑定等增强功能,并且方法的返回值将被直接写入 HTTP 响应体中,这里用来做全局异常处理正合适不过。
我们需要配合@ExceptionHandler
和@ResponseStatus
一起结合着使用
-
@ExceptionHandler
该注解用于标注在异常处理方法,该方法需要@RestControllerAdvice
写到类中,其作用是当控制器抛出异常时候,Spring
会根据异常的类型调用相应的异常处理方法,我们可以利用该注解实现各种不同类型的异常处理逻辑 -
@ResponseStatus
该注解也是注解到异常处理方法上的,主要是为了设置返回的http状态码。如果我们希望在后端异常的时候,客户端的http
状态码也跟随变化,则需要用到此注解
一、先定义一个业务异常类
在com.light.common
中创建一个exception
包,并在exception
中创建一个ServiceException
代码如下:
import com.light.common.result.IResultCode;
import java.io.Serial;
/**
* 自定义业务异常类
*/
public class ServiceException extends RuntimeException {
@Serial
private static final long serialVersionUID = -2184933109131865121L;
private IResultCode resultCode;
public ServiceException(IResultCode resultCode) {
super(resultCode.getMessage());
this.resultCode = resultCode;
}
public ServiceException(String message) {
super(message);
}
public ServiceException(Throwable cause) {
super(cause);
}
public ServiceException(String message, Throwable cause) {
super(message, cause);
}
public IResultCode getErrorCode() {
return resultCode;
}
}
二、定义全局异常处理器
在exception
中创建一个GlobalExceptionHandler
代码如下:
import cn.hutool.core.convert.Convert;
import cn.hutool.http.HTMLFilter;
import com.light.common.result.BaseResult;
import com.light.common.result.ResultCode;
import io.micrometer.common.util.StringUtils;
import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingPathVariableException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
/**
* 全局异常处理器
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
/**
* 自定义业务异常
*/
@ExceptionHandler(ServiceException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ResponseBody
public Object handleServiceException(ServiceException e, HttpServletRequest request) {
log.error("自定义业务异常,'{}'", e.getMessage(), e);
if (e.getErrorCode() != null) {
return BaseResult.error(e.getErrorCode());
}
return BaseResult.error(e.getMessage());
}
/**
* 自定义验证异常
*/
@ExceptionHandler(BindException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ResponseBody
public BaseResult<?> handleBindException(BindException e) {
log.error(e.getMessage(), e);
BindingResult bindingResult = e.getBindingResult();
String message = null;
if (bindingResult.hasErrors()) {
FieldError fieldError = bindingResult.getFieldError();
if (fieldError != null) {
message = fieldError.getField() + fieldError.getDefaultMessage();
}
}
return BaseResult.error(message);
}
/**
* 请求参数类型不匹配
*/
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ResponseBody
public BaseResult<?> handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e,
HttpServletRequest request) {
String requestURI = request.getRequestURI();
String value = Convert.toStr(e.getValue());
if (StringUtils.isNotEmpty(value)) {
value = new HTMLFilter().filter(value);
}
log.error("请求参数类型不匹配'{}',发生系统异常.", requestURI, e);
return BaseResult.error(String.format("请求参数类型不匹配,参数[%s]要求类型为:'%s',输入值为:'%s'", e.getName(), e.getRequiredType().getName(), value));
}
/**
* 请求路径中缺少必需的路径变量
*/
@ExceptionHandler(MissingPathVariableException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ResponseBody
public BaseResult<?> handleMissingPathVariableException(MissingPathVariableException e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
log.error("请求路径中缺少必需的路径变量'{}',发生系统异常.", requestURI, e);
return BaseResult.error(String.format("请求路径中缺少必需的路径变量[%s]", e.getVariableName()));
}
/**
* 参数验证异常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ResponseBody
public BaseResult<?> handleMethodArgumentNotValidException(MethodArgumentNotValidException e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
BindingResult bindingResult = e.getBindingResult();
String message = null;
if (bindingResult.hasErrors()) {
FieldError fieldError = bindingResult.getFieldError();
if (fieldError != null) {
message = fieldError.getField() + fieldError.getDefaultMessage();
}
}
log.error("请求地址'{}',参数验证失败", requestURI);
return BaseResult.error(ResultCode.VALIDATE_FAILED, message);
}
/**
* 请求方式异常
*/
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
@ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
@ResponseBody
public BaseResult<?> handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException e,
HttpServletRequest request) {
String requestURI = request.getRequestURI();
log.error("请求地址'{}',不支持'{}'请求", requestURI, e.getMethod());
return BaseResult.error(ResultCode.METHOD_FAILED, "请求方式不支持:{}" + e.getMethod());
}
/**
* 拦截未知的运行时异常
*/
@ExceptionHandler(RuntimeException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ResponseBody
public BaseResult exceptionHandler(RuntimeException e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
log.error("请求地址'{}',发生未知异常.", requestURI, e);
return BaseResult.error("未知异常:"+e.getMessage());
}
/**
* 拦截系统异常
*/
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ResponseBody
public BaseResult handleException(Exception e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
log.error("请求地址'{}',发生系统异常.", requestURI, e);
return BaseResult.error("系统异常:"+e.getMessage());
}
}
二、测试
通过上述代码处理后,我们在写业务逻辑的时候,只需要关心自己的主要业务,无需花费大量精力去写try...catch
小插曲
直接测试会发现无效,返回的信息并不是我们定义的消息类结构,原因是默认情况下,Spring Boot会扫描启动类所在包及其子包,而我们这里的Controller
和GlobalExceptionHandler
分别在不同的子模块中,所以没有被关联上,我们只需要在启动类上添加手动扫描包路径即可
@SpringBootApplication
@ComponentScan(basePackages = {"com.light.api", "com.light.common"})
public class LightApiApplication {
public static void main(String[] args) {
SpringApplication.run(LightApiApplication.class, args);
}
}
抛出异常:
- 我们在控制器中人为制造一个异常比如,除数为0
@GetMapping("/getMsg")
public BaseResult<?> getTest() {
int s=1/0;
// throw new ServiceException("自定义异常");
return BaseResult.success("成功");
}
测试请求,返回结果如下
抛出自定义异常:
如果需要对自己的业务代码进行自定义抛出异常可采用此方式
@GetMapping("/getMsg")
public BaseResult<?> getTest() {
throw new ServiceException("自定义异常");
}
测试请求,返回结果如下
总结
至此,我们对整个系统进行的全局异常的统一处理,写代码的时候不再花费大量精力考虑try...catch
的事情了,这也是后端需要做到的统一返回标准
下一章写统一日志处理