一、背景
开发中可能碰到各种各样的异常,有的是可自定义的业务异常,有的是运行中不可知的异常,这就需要有个统一的处理。另外如果给前端返回乱七八糟的错误信息,前端就很难告知用户有效的信息,这就需要跟前端约定一个统一的规范,照着这个规范给出响应。
二、自定义异常错误码
首先,得自定义一些异常,这个根据自己需要添加:
/**
* Description 异常错误码
*
* @author Bob
* @date 2020/8/17
**/
public final class ErrorCode {
/**
* 成功
*/
public static final String SUCCESS = "000000";
/**
* 参数验证异常
*/
public static final String INVALID_ARG_EXCEPTION = "000001";
/**
* 业务异常
*/
public static final String BIZ_EXCEPTION = "000002";
/**
* 数据库异常
*/
public static final String DAO_EXCEPTION = "000003";
/**
* 内部服务器错误
*/
public static final String SERVLET_EXCEPTION = "000004";
/**
* DATAFLOW异常
*/
public static final String DATAFLOW_EXCEPTION = "000005";
/**
* 空指针异常
*/
public static final String NULL_POINTER_EXCEPTION = "000006";
/**
* 系统异常
*/
public static final String EXCEPTION = "999999";
}
三、自定义异常
传统处理异常一般有以下烦恼:
- 是捕获异常(try…catch)还是抛出异常(throws)
- 是在controller层做处理还是在service层处理又或是在dao层做处理
- 处理异常的方式是啥也不做,还是返回特定数据,如果返回又返回什么数据
- 不是所有异常我们都能预先进行捕捉,如果发生了没有捕捉到的异常该怎么办?
以上这些问题都可以用全局异常处理来解决,全局异常处理也叫统一异常处理,全局和统一处理代表什么?代表规范!规范有了,很多问题就会迎刃而解!
抛异常的时候总不能每次都抛Exception,这里需要配置一些自定义的异常:
- 自定义异常可以携带更多的信息,不像这样只能携带一个字符串。
- 项目开发中经常是很多人负责不同的模块,使用自定义异常可以统一了对外异常展示的方式。
- 自定义异常语义更加清晰明了,一看就知道是项目中手动抛出的异常。
/**
* Description 默认异常
*
* @author Bob
* @date 2020/8/17
**/
public class DataFlowException extends RuntimeException {
private static final long serialVersionUID = 5688133194852743474L;
/**
* @description 错误码
* @author Bob
* @date 2020/8/17
*/
private String errCode = ErrorCode.DATAFLOW_EXCEPTION;
public String getErrCode() {
return errCode;
}
public void setErrCode(String errCode) {
this.errCode = errCode;
}
/**
* @description 构造方法
* @author Bob
* @date 2020/8/17
*/
public DataFlowException(String message) {
super(message);
}
/**
* @description 构造方法
* @author Bob
* @date 2020/8/17
*/
public DataFlowException(String errCode, String errMsg) {
super(errMsg);
this.setErrCode(errCode);
}
/**
* @description 构造方法
* @author Bob
* @date 2020/8/17
*/
public DataFlowException(String message, Throwable cause) {
super(message, cause);
}
/**
* @description 构造方法
* @author Bob
* @date 2020/8/17
*/
public DataFlowException(String errCode, String message, Throwable cause) {
super(message, cause);
this.setErrCode(errCode);
}
/**
* @description 构造方法
* @author Bob
* @date 2020/8/17
*/
public DataFlowException(Throwable cause) {
super(cause);
}
}
/**
* Description 业务异常
*
* @author Bob
* @date 2020/8/17
**/
public class BusinessException extends DataFlowException {
private static final long serialVersionUID = -8609214742345497948L;
/**
* @param message
* @description 构造方法
* @author Bob
* @date 2020/8/17
*/
public BusinessException(String message) {
super(message);
setErrCode(ErrorCode.BIZ_EXCEPTION);
}
/**
* @description 构造方法
* @author Bob
* @date 2020/8/17
*/
public BusinessException(String errCode, String errMsg) {
super(errMsg);
this.setErrCode(errCode);
}
/**
* @param message
* @param cause
* @description 构造方法
* @author Bob
* @date 2020/8/17
*/
public BusinessException(String message, Throwable cause) {
super(message, cause);
setErrCode(ErrorCode.BIZ_EXCEPTION);
}
/**
* @param errCode
* @param message
* @param cause
* @description 构造方法
* @author Bob
* @date 2020/8/17
*/
public BusinessException(String errCode, String message, Throwable cause) {
super(message, cause);
setErrCode(errCode);
}
/**
* @param cause
* @description 构造方法
* @author Bob
* @date 2020/8/17
*/
public BusinessException(Throwable cause) {
super(cause);
setErrCode(ErrorCode.BIZ_EXCEPTION);
}
}
四、统一响应
与前端约定,返回统一的格式:code:**,msg:**,data:**
import java.io.Serializable;
public class RestResponse<T> implements Serializable {
private static final long serialVersionUID = 4893280118017319089L;
public static final String SUCCESS_CODE = "0";
public static final String SUCCESS_MESSAGE = "操作成功";
public static final String ERROR_CODE = "9";
public static final String ERROR_MESSAGE = "系统异常";
private String code;
private String msg;
private T data;
public RestResponse() {
this("0", "操作成功");
}
public RestResponse(String code, String msg) {
this(code, msg, (Object)null);
}
public RestResponse(String code, String msg, T data) {
this.code(code).msg(msg).result(data);
}
private RestResponse<T> code(String code) {
this.setCode(code);
return this;
}
private RestResponse<T> msg(String msg) {
this.setMsg(msg);
return this;
}
public RestResponse<T> result(T data) {
this.setData(data);
return this;
}
public boolean success() {
return "0".equals(this.code);
}
public boolean fail() {
return !this.success();
}
public static <T> RestResponse<T> ok(T t) {
return new RestResponse("0", "操作成功", t);
}
public static <T> RestResponse<T> ok() {
return new RestResponse();
}
public static <E> RestResponse<E> error(String code, String msg) {
return new RestResponse(code, msg);
}
public static <T> RestResponse<T> error() {
return new RestResponse("9", "系统异常");
}
public static <T> RestResponse<T> error(String msg) {
return new RestResponse("9", msg);
}
public String getCode() {
return this.code;
}
public String getMsg() {
return this.msg;
}
public T getData() {
return this.data;
}
public void setCode(String code) {
this.code = code;
}
public void setMsg(String msg) {
this.msg = msg;
}
public void setData(T data) {
this.data = data;
}
public String toString() {
return "RestResponse(code=" + this.getCode() + ", msg=" + this.getMsg() + ", data=" + this.getData() + ")";
}
}
如果都按统一格式返回的话,就不用讨论下一步了。关键是代码不可能一个人开发,每个人都有自己的习惯,当然肯定也有不可预知的异常,比如:
校验请求参数是否为空,可以这么写:
Preconditions.checkNotNull(req.getPort(), "port不能为空");
也可以在请求体中添加@NotNull (推荐这个,代码简洁)
@NotNull(message = "port不能为空")
private String port;
可是,这么写,返回的时候就变成这样了:
{
"timestamp": "2020-09-27 14:03:22",
"status": 500,
"error": "Internal Server Error",
"message": "port不能为空",
"path": "/datasource/queryTableList"
}
这明显不是我们要的格式啊,我们要的是下面这样的:
{
"code": "000006",
"msg": "port不能为空",
"data": null
}
怎么办呢,Spring早就内置了这种处理方案,就是RestControllerAdvice(ControllerAdvice),结合ExceptionHandler
五、全局异常捕获统一处理 RestControllerAdvice(ControllerAdvice)
首先,我们需要新建一个类,在这个类上加上@ControllerAdvice或@RestControllerAdvice注解,这个类就配置成全局处理类了。(这个根据你的Controller层用的是@Controller还是@RestController来决定)
然后在类中新建方法,在方法上加上@ExceptionHandler注解并指定你想处理的异常类型,接着在方法内编写对该异常的操作逻辑,就完成了对该异常的全局处理!
先来看下RestControllerAdvice的源码:
package org.springframework.web.bind.annotation;
import java.lang.annotation.Annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@ControllerAdvice
@ResponseBody
public @interface RestControllerAdvice {
@AliasFor("basePackages")
String[] value() default {};
@AliasFor("value")
String[] basePackages() default {};
Class<?>[] basePackageClasses() default {};
Class<?>[] assignableTypes() default {};
Class<? extends Annotation>[] annotations() default {};
}
再看下ControllerAdvice的源码:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface ControllerAdvice {
@AliasFor("basePackages")
String[] value() default {};
@AliasFor("value")
String[] basePackages() default {};
Class<?>[] basePackageClasses() default {};
Class<?>[] assignableTypes() default {};
Class<? extends Annotation>[] annotations() default {};
}
发现RestControllerAdvice 其实就是ControllerAdvice和ResponseBody的结合体
算了,还是直接上自定义的代码吧
import com.***.common.exception.BusinessException;
import com.***.common.exception.DataFlowException;
import com.***.common.exception.ErrorCode;
import com.***.common.exception.InvalidArgumentException;
import com.***.framework.response.RestResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.ConversionNotSupportedException;
import org.springframework.beans.TypeMismatchException;
import org.springframework.dao.DataAccessException;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.validation.BindException;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingPathVariableException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.ServletRequestBindingException;
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.context.request.async.AsyncRequestTimeoutException;
import org.springframework.web.multipart.support.MissingServletRequestPartException;
import org.springframework.web.servlet.NoHandlerFoundException;
import javax.servlet.http.HttpServletRequest;
/**
* Description 统一异常处理
*
* @author Bob
* @date 2020/9/25
**/
@RestControllerAdvice
@Slf4j
public class UnifyExceptionHandler {
/**
* @description 业务异常处理
* @author Bob
* @date 2020/9/25
*/
@ExceptionHandler(value = BusinessException.class)
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public RestResponse businessException(BusinessException e, HttpServletRequest request) {
log.error("{} {}, 发生业务异常, 原因:{}", request.getMethod(), request.getRequestURI(), e.getMessage());
return RestResponse.error(e.getErrCode(), e.getMessage());
}
/**
* @description 参数异常处理
* @author Bob
* @date 2020/9/25
*/
@ExceptionHandler(value = InvalidArgumentException.class)
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public RestResponse invalidArgumentException(InvalidArgumentException e, HttpServletRequest request) {
log.error("{} {}, 发生参数异常, 原因:{}", request.getMethod(), request.getRequestURI(), e.getMessage());
return RestResponse.error(e.getErrCode(), e.getMessage());
}
/**
* @description 异常处理
* @author Bob
* @date 2020/9/25
*/
@ExceptionHandler(value = DataFlowException.class)
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public RestResponse dataFlowException(DataFlowException e, HttpServletRequest request) {
log.error("{} {}, 发生异常, 原因:{}", request.getMethod(), request.getRequestURI(), e.getMessage());
return RestResponse.error(e.getErrCode(), e.getMessage());
}
/**
* @description 数据库异常
* @author Bob
* @date 2020/9/27
*/
@ExceptionHandler(DataAccessException.class)
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public RestResponse dataAccessException(DataAccessException e, HttpServletRequest request) {
log.error("{} {}, 发生数据库异常, 原因:{}", request.getMethod(), request.getRequestURI(), e.getMessage());
return RestResponse.error(ErrorCode.DAO_EXCEPTION, e.getMessage());
}
/**
* @description 空指针异常
* @author Bob
* @date 2020/9/27
*/
@ExceptionHandler(NullPointerException.class)
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public RestResponse nullPointerException(NullPointerException e, HttpServletRequest request) {
log.error("{} {}, 发生空指针异常, 原因:{}", request.getMethod(), request.getRequestURI(), e.getMessage());
return RestResponse.error(ErrorCode.NULL_POINTER_EXCEPTION, e.getMessage());
}
/**
* @description 其它异常
* @author Bob
* @date 2020/9/27
*/
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public RestResponse handleException(Exception e, HttpServletRequest request) {
// 其他内部spring mvc异常
RestResponse restResponse = mvcExceptionResolve(e);
if (restResponse != null) {
if (HttpStatus.Series.valueOf(Integer.valueOf(restResponse.getCode()).intValue()) == HttpStatus.Series.CLIENT_ERROR) {
log.warn("{} {}, 发生异常, 原因:{}", request.getMethod(), request.getRequestURI(), e.getMessage());
} else {
log.error("{} {}, 发生异常, 原因:{}", request.getMethod(), request.getRequestURI(), e.getMessage(), e);
}
return restResponse;
}
log.error("{} {}, 发生异常, 原因:{}", request.getMethod(), request.getRequestURI(), e.getMessage(), e);
return RestResponse.error(ErrorCode.EXCEPTION, e.getMessage());
}
/**
* @description MVC异常处理
* @author Bob
* @date 2020/9/27
*/
protected RestResponse mvcExceptionResolve(Exception ex) {
if (ex instanceof NoHandlerFoundException) {
return RestResponse.error(HttpStatus.NOT_FOUND.toString(), HttpStatus.NOT_FOUND.getReasonPhrase());
} else if (ex instanceof HttpRequestMethodNotSupportedException) {
return RestResponse.error(HttpStatus.METHOD_NOT_ALLOWED.toString(), ex.getMessage());
} else if (ex instanceof HttpMediaTypeNotSupportedException) {
return RestResponse.error(HttpStatus.UNSUPPORTED_MEDIA_TYPE.toString(), ex.getMessage());
} else if (ex instanceof HttpMediaTypeNotAcceptableException) {
return RestResponse.error(HttpStatus.NOT_ACCEPTABLE.toString(), HttpStatus.NOT_ACCEPTABLE.getReasonPhrase());
} else if (ex instanceof MissingPathVariableException) {
return RestResponse.error(HttpStatus.BAD_REQUEST.toString(), "Missing path variable");
} else if (ex instanceof MissingServletRequestParameterException) {
return RestResponse.error(HttpStatus.BAD_REQUEST.toString(), HttpStatus.BAD_REQUEST.getReasonPhrase());
} else if (ex instanceof ServletRequestBindingException) {
return RestResponse.error(HttpStatus.BAD_REQUEST.toString(), HttpStatus.BAD_REQUEST.getReasonPhrase());
} else if (ex instanceof ConversionNotSupportedException) {
return RestResponse.error(HttpStatus.BAD_REQUEST.toString(), HttpStatus.BAD_REQUEST.getReasonPhrase());
} else if (ex instanceof TypeMismatchException) {
return RestResponse.error(HttpStatus.BAD_REQUEST.toString(), HttpStatus.BAD_REQUEST.getReasonPhrase());
} else if (ex instanceof HttpMessageNotReadableException) {
return RestResponse.error(HttpStatus.BAD_REQUEST.toString(), HttpStatus.BAD_REQUEST.getReasonPhrase());
} else if (ex instanceof HttpMessageNotWritableException) {
return RestResponse.error(HttpStatus.INTERNAL_SERVER_ERROR.toString(), HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase());
} else if (ex instanceof MethodArgumentNotValidException) {
return RestResponse.error(HttpStatus.BAD_REQUEST.toString(), HttpStatus.BAD_REQUEST.getReasonPhrase());
} else if (ex instanceof MissingServletRequestPartException) {
return RestResponse.error(HttpStatus.BAD_REQUEST.toString(), HttpStatus.BAD_REQUEST.getReasonPhrase());
} else if (ex instanceof BindException) {
return RestResponse.error(HttpStatus.BAD_REQUEST.toString(), HttpStatus.BAD_REQUEST.getReasonPhrase());
} else if (ex instanceof AsyncRequestTimeoutException) {
return RestResponse.error(HttpStatus.SERVICE_UNAVAILABLE.toString(), HttpStatus.SERVICE_UNAVAILABLE.getReasonPhrase());
}
return null;
}
}
参考:https://blog.youkuaiyun.com/qq_35098526/article/details/88949425
https://blog.youkuaiyun.com/devcloud/article/details/108598751