2021-06-28

##SpringBoot封装全局异常处理器

01、为什么Springboot需要《全局异常处理》

全局异常处理就是指把整个系统的异常统一自动处理,程序员可以做到不用些try/catch就能进行处理项目中出现的异常。
为什么需要全局异常处理呢?

  • 第一个原因:不用强制写try/catch,异常交由统一异常的处理机制进行捕获。
@GetMapping("/error1")
	public String error1() {
	int i = 1 / 0;
	return "success";
}

在开发中,如果不用try/catch进行捕获的话。客户端就会跳转到springboot默认的异常页面。报出500的错误信息。
在开发中遇见了异常一般的程序开发者都会使用try/catch来进行捕获处理,如下:

  @GetMapping("/error2")
  public String error2() {
	  try {
	  	int i = 1 / 0;
	  } catch (Exception ex) {
	  	log.info("出现了异常:{}", ex);
	 	 return "no";
	  }
	  return "success";
  }

有没有更好的处理解决方案呢?答案是:有的。可以采用全局异常处理器来解决这个问题。

  • 第二个原因:自定义异常:只能用全局异常来捕获。
@GetMapping("/error4")
public void error4() {
   throw  new RuntimeException("用户名和密码有误!!!");
}

上面的方式就没办法在try/catch进行处理。而且直接返回这些错误信息,用户是看不懂这些信息的,所以统一异常处理是非常有必要的事情!如何做呢?

  • 第三个原因:JSR303的参数验证器,参数校验不通过会抛出异常,也是无法通过try/catch进行直接捕获处理的。

##02、编码实现springboot的全局异常配置

####步骤1:统一封装异常处理枚举类
所有的运行时异常都用枚举类来进行定义即可:

package com.kuangstudy.exception;
import lombok.Getter;

/**
 * @Author xuke
 * @Description 专门处理异常
 * @Date 21:14 2021/6/25
 * @Param 
 * @return 
**/
@Getter
public enum ResultCodeEnum {

    UNKNOWN_REASON(false, 20001, "未知错误"),
    SERVER_ERROR(false, 500, "服务器忙,请稍后在试"),
    ORDER_CREATE_FAIL(false, 601, "订单下单失败");

    private Boolean success;
    private Integer code;
    private String message;
    private ResultCodeEnum(Boolean success, Integer code, String message) {
        this.success = success;
        this.code = code;
        this.message = message;
    }
}

####步骤2:封装controller的异常结果处理

package com.kuangstudy.exception;
import com.kuangstudy.common.R;
import lombok.*;
/**
 * @author 飞哥
 * @Title: 学相伴出品
 * @Description: 我们有一个学习网站:https://www.kuangstudy.com
 * @date 2021/6/2 10:32
 */
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Data
@ToString
public class ErrorHandler {
    // ErrorHandler === R 答案:不想破坏R类。
    // 异常的状态码,从枚举中获得
    private Integer status;
    // 异常的消息,写用户看得懂的异常,从枚举中得到
    private String message;
    // 异常的名字
    private String exception;

    /**
     * 对异常处理进行统一封装
     *
     * @param resultCodeEnum 异常枚举
     * @param throwable 出现异常
     * @param message 异常的消息 null /by zero
     * @return
     */
    public static ErrorHandler fail(ResultCodeEnum resultCodeEnum, Throwable throwable, String message) {
        ErrorHandler errorHandler = ErrorHandler.fail(resultCodeEnum, throwable);
        errorHandler.setMessage(message);
        return errorHandler;
    }

    /**
     * 对异常枚举进行封装
     *
     * @param resultCodeEnum
     * @param throwable
     * @return
     */
    public static ErrorHandler fail(ResultCodeEnum resultCodeEnum, Throwable throwable) {
        ErrorHandler errorHandler = new ErrorHandler();
        errorHandler.setMessage(resultCodeEnum.getMessage());
        errorHandler.setStatus(resultCodeEnum.getCode());
        errorHandler.setException(throwable.getClass().getName());
        return errorHandler;
    }
}

####步骤3:定义一个全局异常处理器

package com.kuangstudy.config.handler;

import com.kuangstudy.common.base.ErrorHandler;
import com.kuangstudy.common.base.ResultCodeEnum;
import com.kuangstudy.config.exception.BusinessException;
import com.kuangstudy.config.exception.OrderException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.servlet.http.HttpServletRequest;

/**
 * @author 飞哥
 * @Title: 学相伴出品
 * @Description: 我们有一个学习网站:https://www.kuangstudy.com
 * @date 2021/6/2 10:40
 */
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    /**
     * 对服务器端出现500异常进行统一处理
     */
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ExceptionHandler(Throwable.class)
    public ErrorHandler makeExcepton(Throwable e, HttpServletRequest request) {
        ErrorHandler errorHandler = ErrorHandler.fail(ResultCodeEnum.SERVER_ERROR, e);
        log.error("请求的地址是:{},出现的异常是:{}", request.getRequestURL(), e);
        return errorHandler;
    }

}


  • makeExcepton方法的作用是:把运行时异常封装为ErrorHandler对象进行统一捕获处理。
  • @RestControllerAdvice和@ControllerAdvice它是对controller的增强扩展处理,而全局异常就是一种扩展能力之一。
  • @ExceptionHandler(Throwable.class) :统一处理某一类型异常,从而减少代码的出现异常的复杂度和重复率,
  • @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR):指定客户端收到的http状态码,这里配置的是500,就显示成500错误。不指定也是没问题的。因为返回是根据自己的枚举进行处理了。

步骤4:定义测试类

package com.kuangstudy.controller;

import com.kuangstudy.entity.User;
import io.swagger.annotations.Api;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

/**
 * @description:
 * @author: xuke
 * @time: 2021/6/25 20:16
 */
@RestController
@Api(description = "用户中心")
public class UserController {


    @GetMapping("/error1")
    public User error1(Integer id) {
        if (id.equals(1)) {
            throw new RuntimeException("用户名和密码有误!!!");
        }
        User user = new User();
        user.setId(1);
        user.setNickname("yykk");
        user.setPassword("451212");
        user.setAddress("梅州");
        return user;
    }


    @GetMapping("/error2")
    public User error2(Integer id) {
        int i = 1 / 0;
        User user = new User();
        user.setId(1);
        user.setNickname("yykk");
        user.setPassword("451212");
        user.setAddress("梅州");
        return user;
    }


    @GetMapping("/getuser")
    public User getuser(Integer id) {
        User user = new User();
        user.setId(1);
        user.setNickname("yykk");
        user.setPassword("451212");
        user.setAddress("梅州");
        return user;
    }


    @ResponseBody //---标记--jackson A 有 B没有
    @GetMapping("/getname")
    public String getusername() {
        return "yykk";
    }
}

####步骤5:运行查看结果

http://localhost:8080/error1
http://localhost:8080/error2

执行四个请求的异常处理都是:

{
  "status": 500,
  "message": "服务器忙,请稍后在试",
  "exception": "java.lang.RuntimeException"
}

都被拦截成到全局异常处理了。访问error4并没有显示对应的自定义异常处理信息,如何进行处理呢?答案是:自定义异常统一处理

##03、自定义异常,并集成自定义异常处理器

自定义异常的好处是:可以根据自定异常的信息快速的定位错误出现的模块以及方便记录日志文件,快速进行错误的分析和判定。

####步骤1:添加自定义异常

package com.kuangstudy.config.exception;

import com.kuangstudy.common.base.ErrorHandler;
import com.kuangstudy.common.base.ResultCodeEnum;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.servlet.http.HttpServletRequest;

/**
 * @author 飞哥
 * @Title: 学相伴出品
 * @Description: 我们有一个学习网站:https://www.kuangstudy.com
 * @date 2021/6/2 10:40
 */
@Data
public class BusinessException extends RuntimeException {

    private Integer code;
    private String message;

    public BusinessException(ResultCodeEnum resultCodeEnum) {
        this.code = resultCodeEnum.getCode();
        this.message = resultCodeEnum.getMessage();
    }

    public BusinessException(Integer code, String message) {
        this.code = code;
        this.message = message;
    }

}


package com.kuangstudy.config.exception;

import com.kuangstudy.common.base.ErrorHandler;
import com.kuangstudy.common.base.ResultCodeEnum;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.servlet.http.HttpServletRequest;

/**
 * @author 飞哥
 * @Title: 学相伴出品
 * @Description: 我们有一个学习网站:https://www.kuangstudy.com
 * @date 2021/6/2 10:40
 */
@Data
public class OrderException extends RuntimeException {

    private Integer code;
    private String message;

    public OrderException(ResultCodeEnum resultCodeEnum) {
        this.code = resultCodeEnum.getCode();
        this.message = resultCodeEnum.getMessage();
    }

    public OrderException(Integer code, String message) {
        this.code = code;
        this.message = message;
    }
}


####步骤2:添加自定义异常处理方法

package com.kuangstudy.config.handler;

import com.kuangstudy.common.base.ErrorHandler;
import com.kuangstudy.common.base.ResultCodeEnum;
import com.kuangstudy.config.exception.BusinessException;
import com.kuangstudy.config.exception.OrderException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.servlet.http.HttpServletRequest;

/**
 * @author 飞哥
 * @Title: 学相伴出品
 * @Description: 我们有一个学习网站:https://www.kuangstudy.com
 * @date 2021/6/2 10:40
 */
@RestControllerAdvice(basePackages = "com.kuangstudy")
@Slf4j
public class GlobalExceptionHandler {

    /**
     * 对服务器端出现500异常进行统一处理
     */
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ExceptionHandler(Throwable.class)
    public ErrorHandler makeExcepton(Throwable e, HttpServletRequest request) {
        ErrorHandler errorHandler = ErrorHandler.fail(ResultCodeEnum.SERVER_ERROR, e);
        log.error("请求的地址是:{},出现的异常是:{}", request.getRequestURL(), e);
        return errorHandler;
    }

    /**
     * 对自定义异常进行统一处理
     */
    @ExceptionHandler(BusinessException.class)
    public ErrorHandler handlerBusinessException(BusinessException e, HttpServletRequest request) {
        ErrorHandler errorHandler = ErrorHandler.builder()
                .status(e.getCode())
                .message(e.getMessage())
                .exception(e.getClass().getName())
                .build();
        log.error("请求的地址是:{},BusinessException出现异常:{}", request.getRequestURL(), e);
        return errorHandler;
    }

    /**
     * 对自定义异常进行统一处理
     */
    @ExceptionHandler(OrderException.class)
    public ErrorHandler handlerOrderException(OrderException e, HttpServletRequest request) {
        ErrorHandler errorHandler = ErrorHandler.builder()
                .status(e.getCode())
                .message(e.getMessage())
                .exception(e.getClass().getName())
                .build();
        log.error("请求的地址是:{},OrderException出现异常:{}", request.getRequestURL(), e);
        return errorHandler;
    }

}


####步骤3:定义方法进行测试

 @GetMapping("/error5")
 public void error5() {
 	throw new BusinessException(ResultCodeEnum.LOGIN_CODE_FAIL_ERROR);
 }

@GetMapping("/error6")
public void error6() {
	throw new OrderException(ResultCodeEnum.LOGIN_PHONE_ERRROR);
}

访问:http://localhost:9999/error5

{
  "status": 20007,
  "message": "短信验证码失效,请重新发送",
  "exception": "com.kuangstudy.config.exception.BusinessException"
}

访问:http://localhost:9999/error6

{
  "status": 20002,
  "message": "手机号码不能为空",
  "exception": "com.kuangstudy.config.exception.OrderException"
}

##04、统一返回&异常返回进行结合处理

因为显示统一异常处理和统一返回处理存在两种格式的返回,这个时候如果是接口的调用者就会出现凌乱的感觉。我们可以使用Controller的结果返回拦截处理进行结果处理在返回。

统一异常和统一结果返回结合处理:

package com.kuangstudy.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.kuangstudy.common.R;
import com.kuangstudy.exception.ErrorHandler;
import lombok.SneakyThrows;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import org.springframework.web.servlet.view.json.MappingJackson2JsonView;

/**
 * @author 飞哥
 * @Title: 学相伴出品
 * @Description: 我们有一个学习网站:https://www.kuangstudy.com
 * @date 2021/6/2 11:16
 * <p>
 * bug: (basePackages = "com.kuangstudy")建议扫包
 * 为什么?
 * 如果你项目中没有使用Swagger,你可以扫包也可以不扫。都是正常的。
 * 但是如果你项目使用了Swagger,因为Swagger本身也是一个springmvc的项目,他里面也是一个个http请求
 * 这个请求的时候如果你项目中配置了拦截器,或者一些通知类xxxAdvice,那么就会把Swagger都会进行拦截。
 * 就会造成Swagger失效。
 */
@ControllerAdvice(basePackages = "com.kuangstudy")
public class ResultResponseHandler implements ResponseBodyAdvice<Object> {
    /**
     * 是否支持advice功能,true是支持 false是不支持
     *
     * @param methodParameter
     * @param aClass
     * @return
     */
    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
        //直接写return true对spingmvc中所有请求的结构不论什么数据类型都进行结果处理
//        Executable executable = methodParameter.getExecutable();
//        String name = executable.getName();
//        Class<?> declaringClass = executable.getDeclaringClass();
//        return name.equals("getCourse");
        return true;
    }

    // 参数o 代表其实就是springmvc的请求的方法的结果
    @SneakyThrows
    @Override
    public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        // 对请求的结果在这里统一返回和处理
        if (o instanceof ErrorHandler) {
            // 1、如果返回的结果是一个异常的结果,就把异常返回的结构数据倒腾到R.fail里面即可
            ErrorHandler errorHandler = (ErrorHandler) o;
            return R.fail(errorHandler.getStatus(), errorHandler.getMessage());
        } else if (o instanceof String) {
            // 2、因为springmvc数据转换器对String是有特殊处理 StringHttpMessageConverter
            ObjectMapper objectMapper = new ObjectMapper();
            R r = R.success(o);
            return objectMapper.writeValueAsString(r);
        }

        return R.success(o);
    }
}
上一篇:Servlet异常处理


下一篇:【leetcode】Validate Binary Search Tree(middle)