实际业务中的异常处理(全局异常拦截器)

实际业务中的异常处理(全局异常拦截器)

之前在写问卷系统的时候我们也会偶尔遇到异常,比如说要查询一个数据库,但是数据为空的情况。

我们在Controller层中还要进行逻辑判断

image-20240315100144557比如说这里,我们要根据Service接口返回的数据来判断用户名是否被使用即是否存在于数据库,这样就是在Controller层中进行的逻辑判断

其实绝大多数情况我们都可以直接返回成功的结果,若出现问题,应该在Service中抛出异常并返回问题数据

这里就需要用到自定异常全局异常拦截器

自定异常

我们可以将一个系统的异常比较笼统地分为三种异常:客户端异常ClientException服务端异常ServiceException远程调用异常RemoteException

image-20240315002459696

他们都继承于一个抽象异常类AbstractException,我们先编写一个AbstractException

/**
 * 自定抽象异常
 * @see ClientException
 * @see ServiceException
 * @see RemoteException
 */
@Getter
public abstract class AbstractException extends RuntimeException{

    public final String errorCode;

    public final String errorMessage;

    //这里的设计:我们的自定异常都会去实现IErrorCode,所以我们这里在实现的时候直接传入一个自定Exception类,从里面获取错误码
    public AbstractException(String message, Throwable cause, IErrorCode errorCode) {
        //先调用RuntimeException的初始化方法
        super(message, cause);
        //再设置异常码
        this.errorCode = errorCode.code();
        //若异常message不为空,就赋值给errorMessage
        this.errorMessage = Optional.ofNullable(StringUtils.hasLength(message) ? message : null).orElse(errorCode.message());
    }
}

然后逐个配置三种自定抽象异常

/**
 * 客户端异常
 */
public class ClientException extends AbstractException{

    public ClientException(IErrorCode errorCode){
        this(null, null, errorCode);
    }

    public ClientException(String message){
        this(message, null, BaseErrorCode.CLIENT_ERROR);
    }

    public ClientException(String message,IErrorCode errorCode){
        this(message, null, errorCode);
    }

    public ClientException(String message, Throwable cause, IErrorCode errorCode) {
        super(message, cause, errorCode);
    }

    @Override
    public String toString() {
        return "ClientException{" +
                "errorCode='" + errorCode + '\'' +
                ", errorMessage='" + errorMessage + '\'' +
                '}';
    }
}

/**
 * 远程服务调用异常
 */
public class RemoteException extends AbstractException{

    public RemoteException(String message) {
        super(message, null, BaseErrorCode.REMOTE_ERROR);
    }

    public RemoteException(IErrorCode errorCode) {
        super(errorCode.message(), null, errorCode);
    }

    public RemoteException(String message,IErrorCode errorCode) {
        super(message, null, errorCode);
    }

    public RemoteException(String message, Throwable cause, IErrorCode errorCode) {
        super(message, cause, errorCode);
    }

    @Override
    public String toString() {
        return "RemoteException{" +
                "errorCode='" + errorCode + '\'' +
                ", errorMessage='" + errorMessage + '\'' +
                '}';
    }
}
/**
 * 服务端异常
 */
public class ServiceException extends AbstractException{

    public ServiceException(String message) {
        this(message, null, BaseErrorCode.SERVICE_ERROR);
    }

    public ServiceException(IErrorCode errorCode) {
        this(errorCode.message(), null, errorCode);
    }

    public ServiceException(String message,IErrorCode errorCode) {
        this(message, null, errorCode);
    }

    public ServiceException(String message, Throwable cause, IErrorCode errorCode) {
        super(message, cause, errorCode);
    }

    @Override
    public String toString() {
        return "ServiceException{" +
                "errorCode='" + errorCode + '\'' +
                ", errorMessage='" + errorMessage + '\'' +
                '}';
    }
}

全局返回对象Result

我们相应前端的请求最好有一个统一的格式,所以这里需要定义一个全局返回对象,它必须是可序列化的

/**
 * 全局返回对象
 */
@Data
@Accessors(chain = true)
public class Result<T> implements Serializable {

    @Serial
    private static final long serialVersionUID = 5679018624309023727L;

    /**
     * 正确的返回码
     */
    public static final String SUCCESS_CODE = "0";

    /**
     * 返回码
     */
    private String code;

    /**
     * 返回消息
     */
    private String message;

    /**
     * 相应数据
     */
    private T data;

    /**
     * 请求id
     */
    private String requestId;

    /**
     * 判断是否是成功请求
     * @return 是否成功
     */
    public boolean isSuccess(){
        return SUCCESS_CODE.equals(code);
    }
}

为了更便捷地使用Result,我们再编写一个工具类Results

/**
 * 全局返回对象构造器
 * 用于构造Result
 */
public class Results {

    /**
     * 构造成功相应
     */
    public static Result<Void> success(){
        return new Result<Void>()
                .setCode(Result.SUCCESS_CODE);
    }

    /**
     * 构造代返回数据的成功相应
     */
    public static <T> Result<T> success(T data){
        return new Result<T>()
                .setCode(Result.SUCCESS_CODE)
                .setData(data);
    }

    /**
     * 构造服务端失败相应
     */
    public static Result<Void> failure(){
        return new Result<Void>()
                .setCode(BaseErrorCode.SERVICE_ERROR.code())
                .setMessage(BaseErrorCode.SERVICE_ERROR.message());
    }

    /**
     * 通过 {@link AbstractException} 构建失败响应
     */
    public static Result<Void> failure(AbstractException abstractException){
        String errorCode = Optional.ofNullable(abstractException.getErrorCode()).orElse(BaseErrorCode.SERVICE_ERROR.code());
        String errorMessage = Optional.ofNullable(abstractException.getErrorMessage()).orElse(BaseErrorCode.SERVICE_ERROR.message());
        return new Result<Void>()
                .setCode(errorCode)
                .setMessage(errorMessage);
    }

    /**
     * 通过 errorCode、errorMessage 构建失败响应
     */
    public static Result<Void> failure(String errorCode, String errorMessage) {
        return new Result<Void>()
                .setCode(errorCode)
                .setMessage(errorMessage);
    }

}

全局异常拦截器

有了异常,有了固定的返回格式,这下方便了

为了从Controller中剔除业务逻辑,我们可以直接在ServiceImplement中进行判断,如果出问题就抛出异常,若全局异常拦截器捕捉到了异常,就在控制台上打印对应错误信息和返回对应的Result

image-20240315101358966

/**
 * 全局异常处理器
 *
 */
@Component
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    /**
     * 拦截参数验证异常
     */
    @SneakyThrows
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public Result validExceptionHandler(HttpServletRequest request, MethodArgumentNotValidException ex) {
        BindingResult bindingResult = ex.getBindingResult();
        FieldError firstFieldError = CollectionUtil.getFirst(bindingResult.getFieldErrors());
        String exceptionStr = Optional.ofNullable(firstFieldError)
                .map(FieldError::getDefaultMessage)
                .orElse(StrUtil.EMPTY);
        log.error("[{}] {} [ex] {}", request.getMethod(), getUrl(request), exceptionStr);
        return Results.failure(BaseErrorCode.CLIENT_ERROR.code(), exceptionStr);
    }

    /**
     * 拦截应用内抛出的异常
     */
    @ExceptionHandler(value = {AbstractException.class})
    public Result abstractException(HttpServletRequest request, AbstractException ex) {
        if (ex.getCause() != null) {
            log.error("[{}] {} [ex] {}", request.getMethod(), request.getRequestURL().toString(), ex.toString(), ex.getCause());
            return Results.failure(ex);
        }
        log.error("[{}] {} [ex] {}", request.getMethod(), request.getRequestURL().toString(), ex.toString());
        return Results.failure(ex);
    }

    /**
     * 拦截未捕获异常
     */
    @ExceptionHandler(value = Throwable.class)
    public Result defaultErrorHandler(HttpServletRequest request, Throwable throwable) {
        log.error("[{}] {} ", request.getMethod(), getUrl(request), throwable);
        return Results.failure();
    }

    private String getUrl(HttpServletRequest request) {
        if (StringUtils.isEmpty(request.getQueryString())) {
            return request.getRequestURL().toString();
        }
        return request.getRequestURL().toString() + "?" + request.getQueryString();
    }
}

我们在Service中抛出异常

image-20240315101837844

异常拦截器就会接管并且直接返回错误给Controller

image-20240315101923616

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Controller也由原来的

image-20240315101819706

简化为

image-20240315102053112


舒服了,其实之前的问卷项目里就老是在纠结这个问题。

明明Service没有完成操作,但是我还要在Controller来进行判断,有时返回的还是success()。现在通过异常拦截器统一处理,就没有这种问题了

  • 6
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值