springboot-全局统一异常如何优雅的处理(一)

项目中遇到运行时异常,总不能每个异常处理都去添加try catch逻辑,甚至有的时候一个偶然条件发生而导致异常,而我们没有进行对应的处理则会直接给请求者返回未知的错误,这在正式的上线的项目中是不允许,所以我们来配置全局异常处理

今天讲解一下如何在SpringBoot实现全局异常机制,在没有用springboot大家要实现这一的功能基本上都是通过aop的思想,还是有点麻烦,而现在springboot中对它要进行了一次封装,开发者使用起来更加的简单,接下先通过代码演示效果,然后再分析一下原理,好了废话不多说直接上代码,看代码结构:

1、使用到的注解:@ControllerAdvice注解是用来配置控制器通知的,我们可以配置过滤拦截具体一种或者多种类型的注解,添加annotations属性即可,在类的上方我们配置了@ControllerAdvice的annotations属性值为RestController.class,也就是只有添加了@RestController注解的控制器才会进入全局异常处理;因为我们全局返回的都是统一的Json格式的字符串,所以需要再类上配置@ResponseBody注解;@ExceptionHandler注解用来配置需要拦截的异常类型,默认是全局类型,可以通过value属性配置只对某个类型的异常起作用;@ResponseStatus注解用于配置遇到该异常后返回数据时的StatusCode的值,我们这里默认使用值500

定义一个返回的DTO工具类

//
// 定义一个返回的DTO工具类
//

package com.base.common.obj.dto;

import com.base.common.obj.enums.ResultStatusEnum;
import java.io.Serializable;

public class ResultModelDTO<T> implements Serializable {
    private ResultStatusEnum result;
    private Long code;
    private String message;
    private T model;

    public ResultModelDTO() {
    }

    public boolean successResult() {
        return this.result.equals(ResultStatusEnum.success);
    }

    public boolean failResult() {
        return !this.successResult();
    }

    public ResultModelDTO(ResultStatusEnum result, long code, String message, T model) {
        this.result = result;
        this.code = Long.valueOf(code);
        this.message = message;
        this.model = model;
    }

    public ResultModelDTO(ResultStatusEnum result, ResultCode remoteResultCode, T model) {
        this(result, remoteResultCode.getCode(), remoteResultCode.getMessage(), model);
    }

    public ResultStatusEnum getResult() {
        return this.result;
    }

    public void setResult(ResultStatusEnum result) {
        this.result = result;
    }

    public Long getCode() {
        return this.code;
    }

    public void setCode(Long code) {
        this.code = code;
    }

    public String getMessage() {
        return this.message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public T getModel() {
        return this.model;
    }

    public void setModel(T model) {
        this.model = model;
    }
}

定义一个全局异常处理类

package com.springboot.exception.global_excep;
 
import com.springboot.exception.json.JsonResult;
import org.springframework.beans.ConversionNotSupportedException;
import org.springframework.beans.TypeMismatchException;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import lombok.extern.slf4j.Slf4j 
import java.io.IOException;
 
/**
 * @Author fanghui
 * @Description 全局异常拦截器
 * @Date 16:38 2019/8/14
 * @Modify By
 */
@ControllerAdvice
@ResponseBody
@Slf4j 
public class GlobalExceptionHandler {
 
 private String getMessage(BindingResult result) {
        return result.getFieldErrors().stream().map(error -> {
            return error.getField() + "[" + error.getRejectedValue() + "]:" + error.getDefaultMessage();
        }).collect(Collectors.joining(", "));
    }

    /**
     * 捕获 controller 实体类参数@Validate验证报错
     *
     * @param e
     * @param request
     * @param response
     * @return
     */
    @ExceptionHandler(BindException.class)
    @ResponseBody
    public JsonResult validExceptionHandler(BindException e, HttpServletRequest request, HttpServletResponse response) {
        return JsonResult.error(getMessage(e.getBindingResult()), ErrorCode.ILLEGAL_PARAM.name());
    }

    /**
     * 捕获 普通方法传参@Validate验证报错
     *
     * @param e
     * @param request
     * @param response
     * @return
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseBody
    public JsonResult validExceptionHandler(MethodArgumentNotValidException e, HttpServletRequest request, HttpServletResponse response) {
        return JsonResult.error(getMessage(e.getBindingResult()), ErrorCode.ILLEGAL_PARAM.name());
    }

    /**
     * 捕获 controller 平铺参数@Validate验证报错
     *
     * @param e
     * @param request
     * @param response
     * @return
     */
    @ExceptionHandler(ConstraintViolationException.class)
    @ResponseBody
    public JsonResult validExceptionHandler(ConstraintViolationException e, HttpServletRequest request, HttpServletResponse response) {
        return JsonResult.error(e.getMessage(), ErrorCode.ILLEGAL_PARAM.name());
    }

    //运行时异常
    @ExceptionHandler(RuntimeException.class)
    public String runtimeExceptionHandler(HttpServletRequest request,RuntimeException ex) {
         log.info("请求路径:"+request.getRequestURL().toString()+"+"RuntimeException :"+ex);
        return ResultModelUtil.failResult(DefaultResultCodeEnum.ADD_ERROR);
    }
 
    //空指针异常
    @ExceptionHandler(NullPointerException.class)
    public String nullPointerExceptionHandler(HttpServletRequest request,NullPointerException ex) {
          log.info("请求路径:"+request.getRequestURL().toString()+"+"NullPointerException :"+ex);
        return ResultModelUtil.failResult(DefaultResultCodeEnum.ADD_ERROR);
    }
 
    //类型转换异常
    @ExceptionHandler(ClassCastException.class)
    public String classCastExceptionHandler(HttpServletRequest request,ClassCastException ex) {
         log.info("请求路径:"+request.getRequestURL().toString()+"+"ClassCastException :"+ex);
        return resultFormat(3, ex);
    }
 
    //IO异常
    @ExceptionHandler(IOException.class)
    public String iOExceptionHandler(HttpServletRequest request,IOException ex) {
        log.info("请求路径:"+request.getRequestURL().toString()+"+"IOException :"+ex);
        return resultFormat(4, ex);
    }
 
    //未知方法异常
    @ExceptionHandler(NoSuchMethodException.class)
    public String noSuchMethodExceptionHandler(NoSuchMethodException ex) {
        return resultFormat(5, ex);
    }
 
    //数组越界异常
    @ExceptionHandler(IndexOutOfBoundsException.class)
    public String indexOutOfBoundsExceptionHandler(IndexOutOfBoundsException ex) {
        return resultFormat(6, ex);
    }
 
    //400错误
    @ExceptionHandler({HttpMessageNotReadableException.class})
    public String requestNotReadable(HttpMessageNotReadableException ex) {
        System.out.println("400..requestNotReadable");
        return resultFormat(7, ex);
    }
 
    //400错误
    @ExceptionHandler({TypeMismatchException.class})
    public String requestTypeMismatch(TypeMismatchException ex) {
        System.out.println("400..TypeMismatchException");
        return resultFormat(8, ex);
    }
 
    //400错误
    @ExceptionHandler({MissingServletRequestParameterException.class})
    public String requestMissingServletRequest(MissingServletRequestParameterException ex) {
        System.out.println("400..MissingServletRequest");
        return resultFormat(9, ex);
    }
 
    //405错误
    @ExceptionHandler({HttpRequestMethodNotSupportedException.class})
    public String request405(HttpRequestMethodNotSupportedException ex) {
        return resultFormat(10, ex);
    }
 
    //406错误
    @ExceptionHandler({HttpMediaTypeNotAcceptableException.class})
    public String request406(HttpMediaTypeNotAcceptableException ex) {
        System.out.println("406...");
        return resultFormat(11, ex);
    }
 
    //500错误
    @ExceptionHandler({ConversionNotSupportedException.class, HttpMessageNotWritableException.class})
    public String server500(RuntimeException ex) {
        System.out.println("500...");
        return resultFormat(12, ex);
    }
 
    //栈溢出
    @ExceptionHandler({StackOverflowError.class})
    public String requestStackOverflow(StackOverflowError ex) {
        return resultFormat(13, ex);
    }
 
    //除数不能为0
    @ExceptionHandler({ArithmeticException.class})
    public String arithmeticException(ArithmeticException ex) {
        return resultFormat(13, ex);
    }
 
 
    //其他错误
    @ExceptionHandler({Exception.class})
    public String exception(Exception ex) {
        return resultFormat(14, ex);
    }
 
    private <T extends Throwable> String resultFormat(Integer code, T ex) {
        ex.printStackTrace();
        log.error(String.format(logExceptionFormat, code, ex.getMessage()));
        return JsonResult.failed(code, ex.getMessage());
    }
 
}

除了上面的运行时异常之外,还有一个是专门来处理业务异常的,接下来我们再来处理一个自定义业务异常类:

自定义业务异常类BaseException

package com.izkml.mlyun.framework.common.exception;

public class BaseException extends RuntimeException {
    private String errorCode;

    public String getErrorCode() {
        return this.errorCode;
    }

    public void setErrorCode(String errorCode) {
        this.errorCode = errorCode;
    }

    public BaseException(String errorCode, String message) {
        this(errorCode, message, (Throwable)null);
    }

    public BaseException(String errorCode, String message, Throwable cause) {
        super(message, cause);
        this.errorCode = errorCode;
    }

    public BaseException(ErrorEnum errorEnum) {
        this(errorEnum.getCode(), errorEnum.getText(), (Throwable)null);
    }

    public BaseException(ErrorEnum errorEnum, Throwable cause) {
        this(errorEnum.getCode(), errorEnum.getText(), cause);
    }
}

SpringMVC@ResponseStatus注解的使用

有时候,我们在实战项目中并不关心返回消息体的内容,前端只关系后端返回的状态码到底是什么,这个时候我们就要做的专业点,那么此时这个注解就派上用场了

//405-Method Not Allowed
    @ExceptionHandler(HTTPRequestMethodNotSupportedException.class)
    @ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)  //这个HttpStatus是个枚举类,可以点击进去有很多不同枚举
    public Map<String,Object> server500(RuntimeException ex) {
        Map<String,Object> map = new HashMap<String,Object>();
        map.put("code",400);
        map.put("message",e.getMessage());
        return map;
    }

springboot-全局统一异常如何优雅的处理(一)

package com.base.core.handler;

import com.base.common.constant.ResultEnum;
import com.base.common.constant.StrConstant;
import com.base.common.exception.BusinessException;
import com.base.common.vo.BaseVO;
import com.base.core.service.impl.GenerateReturnSign;
import com.alibaba.fastjson.JSON;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.servlet.http.HttpServletResponse;

import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

/**
 * 全局异常捕获处理
 *
 * @date 2021-04-23 17:37
 */
@RestControllerAdvice
@Slf4j
@RequiredArgsConstructor
public class GlobalExceptionHandler {

    /**
     * 返回报文签名生成
     */
    private final GenerateReturnSign generateReturnSign;

    /**
     * 捕获 RequestMethod 不匹配异常 {@link org.springframework.http.HttpMethod}
     *
     * @param e        RequestMethod 不匹配的具体异常信息
     * @param response Response
     * @return 异常解析后的 Json
     */
    @ExceptionHandler({HttpRequestMethodNotSupportedException.class})
    private String handleRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e,
        HttpServletResponse response) {
        StringBuilder sb = new StringBuilder();
        sb.append("请求的 Request method:");
        sb.append(e.getMethod());
        sb.append(";支持的 Request method:");
        String supportRequestMethods = Objects.requireNonNull(e.getSupportedHttpMethods()).stream()
            .map(HttpMethod::toString)
            .collect(Collectors.joining(StrConstant.COMMA));
        sb.append(StrConstant.BRACKET_START).append(supportRequestMethods).append(StrConstant.BRACKET_END);
        return logException(new BaseVO(ResultEnum.PARAM_VERIFICATION_FAIL, sb.toString()), response);
    }

    // TODO-JING 后续不再捕获 HTTP 相关异常,而是直接返回 Spring Boot 封装好的 HTTP 错误码

    /**
     * 捕获 MediaType 不匹配异常 {@link org.springframework.http.MediaType}
     *
     * @param e        MediaType 不匹配的具体异常信息
     * @param response Response
     * @return 异常解析后的 Json
     */
    @ExceptionHandler(HttpMediaTypeNotSupportedException.class)
    private String handleMediaTypeException(HttpMediaTypeNotSupportedException e, HttpServletResponse response) {
        StringBuilder sb = new StringBuilder();
        // 请求报文的 contentType
        if (e.getContentType() == null) {
            return logException(new BaseVO(ResultEnum.PARAM_VERIFICATION_FAIL, "请求报文不能为空"), response);
        }
        sb.append("请求的 ContentType:");
        sb.append(Objects.requireNonNull(e.getContentType()).getType()).append(StrConstant.SLASH).append(e.getContentType().getSubtype());
        // 接口支持的 contentType
        sb.append(";支持的 ContentType:");
        // [application/json;charset=UTF-8, text/plain, image/jpeg, text/css, text/javascript]
        sb.append(StrConstant.BRACKET_START);
        String separator = "";
        for (MediaType t : e.getSupportedMediaTypes()) {
            // application/json
            sb.append(separator).append(t.getType()).append(StrConstant.SLASH).append(t.getSubtype());
            Map<String, String> parameters = t.getParameters();
            // ;charset=UTF-8
            parameters.forEach((key, value) -> sb.append(StrConstant.SEMICOLON).append(key).append(StrConstant.EQUAL).append(value));
            separator = StrConstant.COMMA;
        }
        sb.append(StrConstant.BRACKET_END);
        return logException(new BaseVO(ResultEnum.PARAM_VERIFICATION_FAIL, sb.toString()), response);
    }

    /**
     * 捕获 报文 Json 转换异常
     *
     * @param e        报文转换失败异常
     * @param response Response
     * @return 异常解析后的 Json
     */
    @ExceptionHandler(HttpMessageNotReadableException.class)
    private String handleHttpMessageNotReadableException(HttpMessageNotReadableException e,
        HttpServletResponse response) {
        return logException(new BaseVO(ResultEnum.JSON_FORMAT_EXCEPTION), response);
    }

    /**
     * <p>捕获以下参数校验抛出的异常(处于 AOP 之前):</p>
     * <ul>
     *   <li>{@link javax.validation.constraints} 包下注解</li>
     *   <li>{@link org.hibernate.validator.constraints} 包下注解</li>
     *   <li>实现 {@link javax.validation.ConstraintValidator} 接口的自定义校验器</li>
     * </ul>
     *
     * @param e        参数校验失败时的具体异常信息,由 @RequestBody 接口抛出
     * @param response Response
     * @return 异常解析后的 Json
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    private String handleArgumentValidateException(MethodArgumentNotValidException e, HttpServletResponse response) {
        return handleBindingResult(e.getBindingResult(), response);
    }

    /**
     * <p>捕获以下参数校验抛出的异常(处于 AOP 之前):</p>
     * <ul>
     *   <li>{@link javax.validation.constraints} 包下注解</li>
     *   <li>{@link org.hibernate.validator.constraints} 包下注解</li>
     *   <li>实现 {@link javax.validation.ConstraintValidator} 接口的自定义校验器</li>
     * </ul>
     *
     * @param e        参数校验失败时的具体异常信息,由非 @RequestBody 接口抛出
     * @param response Response
     * @return 异常解析后的 Json
     */
    @ExceptionHandler(BindException.class)
    private String handleArgumentValidateException(BindException e, HttpServletResponse response) {
        return handleBindingResult(e.getBindingResult(), response);
    }

    /**
     * 解析 bindingResult
     *
     * @param bindingResult 包含各项参数校验失败的详细信息
     * @param response      Response
     * @return 解析结果
     */
    private String handleBindingResult(BindingResult bindingResult, HttpServletResponse response) {
        StringBuilder sb = new StringBuilder();
        bindingResult.getAllErrors().forEach(error -> sb.append(error.getDefaultMessage()).append(StrConstant.SEMICOLON));
        return logException(new BaseVO(ResultEnum.PARAM_VERIFICATION_FAIL, sb.toString()), response);
    }

    /**
     * 捕获自定义业务异常
     *
     * @param e        自定义业务异常
     * @param response Response
     * @return 异常解析后的 Json
     */
    @ExceptionHandler(BusinessException.class)
    private String handleBusinessException(BusinessException e, HttpServletResponse response) {
        return logException(new BaseVO(e.getStatus(), e.getMsg(), e.getRemark()), response);
    }

    /**
     * 捕获未知异常
     *
     * @param e        未知异常
     * @param response Response
     * @return 服务器内部异常
     */
    @ExceptionHandler()
    private String handleUnKnowException(Exception e, HttpServletResponse response) {
        log.warn("系统未知异常:", e);
        return logException(new BaseVO(ResultEnum.UNKNOWN_ERROR), response);
    }

    /**
     * 打印异常日志
     *
     * @param baseVO   异常信息
     * @param response Response
     * @return 解析后的异常 Json
     */
    private String logException(BaseVO baseVO, HttpServletResponse response) {
        String result = fillReturnParam(baseVO, response);
        log.warn("异常响应:{}", result);
        return result;
    }

    /**
     * 填充返回参数
     *
     * @param baseVO   异常信息
     * @param response Response
     * @return 解析后的异常 Json
     */
    private String fillReturnParam(BaseVO baseVO, HttpServletResponse response) {
        String sign = generateReturnSign.initReturnSign(baseVO, response);
        baseVO.setSign(sign);
        // TODO-JING 异常返回也需要设置 ContentType 为 APPLICATION_JSON_UTF8,而不应该为 TEXT_PLAIN
        return JSON.toJSONString(baseVO);
    }
}

springboot-网关独有全局统一异常如何优雅的处理(三)

package com.base.gateway.handler;

import com.base.common.constant.HeaderConstant;
import com.base.common.constant.ResultEnum;
import com.base.common.exception.BusinessException;
import com.base.common.util.AesUtils;
import com.base.common.vo.BaseVO;
import com.base.gateway.config.EncryptConfig;
import com.base.gateway.service.RsaService;
import com.alibaba.fastjson.JSON;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.server.ServerWebExchange;

import cn.hutool.core.util.StrUtil;
import reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;


@Slf4j
@Order(-1)
@Configuration
@RequiredArgsConstructor
public class GlobalExceptionHandler implements ErrorWebExceptionHandler {

    private final EncryptConfig encryptConfig;

    private final RsaService rsaService;

    @Override
    public Mono<Void> handle(ServerWebExchange exchange, Throwable exception) {

        HttpHeaders requestHeaders = exchange.getRequest().getHeaders();
        String secret = requestHeaders.getFirst(HeaderConstant.SECRET);
        ServerHttpResponse response = exchange.getResponse();
        HttpHeaders responseHeaders = response.getHeaders();
        String json;
        if (exception instanceof BusinessException) {
            BusinessException businessException = (BusinessException) exception;
            json = JSON.toJSONString(new BaseVO(businessException.getStatus(), businessException.getMsg(),
                businessException.getRemark()));
            log.warn("异常响应:{}", json);
        } else {
            json = JSON.toJSONString(new BaseVO(ResultEnum.UNKNOWN_ERROR));
            log.warn("系统未知异常:", exception);
        }
        // 启用加密
        if (encryptConfig.getEnable()) {
            // ContentType
            responseHeaders.setContentType(MediaType.TEXT_PLAIN);
            return response.writeWith(Mono.fromSupplier(() -> {
                DataBufferFactory bufferFactory = response.bufferFactory();
                // 没有 secret 则使用私钥加密
                if (StrUtil.isEmpty(secret)) {
                    // 500
                    response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
                    // 私钥加密
                    String encryptContent = rsaService.encryptByPrivateKeyBase64Encode(json);
                    return bufferFactory.wrap(encryptContent.getBytes(StandardCharsets.UTF_8));
                }
                // secret 加密
                String encryptContent = AesUtils.aesEncryptStr(json, secret);
                // 200
                response.setStatusCode(HttpStatus.OK);
                return bufferFactory.wrap(encryptContent.getBytes(StandardCharsets.UTF_8));
            }));
        }
        // 未启用加密
        response.setStatusCode(HttpStatus.OK);
        return response.writeWith(Mono.fromSupplier(() -> {
            DataBufferFactory bufferFactory = response.bufferFactory();
            return bufferFactory.wrap(json.getBytes(StandardCharsets.UTF_8));
        }));
    }
}

总结

我们开发无论什么服务都大体分为三层,controller,service,dao;其中servic一般不做任何系统类型异常处理(RuntimeException和Exception),只关注业务分析,但是有一种情况需要往上进行抛异常(比如,业务层中车牌号重复,需要告诉上游你的车牌号已经重复,需要一层一层的往上进行抛直到前端页面告知用户);除了这种业务需要判断的异常往上抛之外,异常需要在controller进行捕捉,通过try..catch..进行捕获或者使用全局异常进行拦截,事实上,对于服务器来说,它是200,正常返回的,而对不业务模块来说,它的状态是个500,嘿嘿,这点要清楚

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值