全局异常处理教程

全局异常处理教程

一.SpringBoot全局异常处理方式

正常的Web应用开发时,需要考虑到应用运行发生异常时或出现错误时如何来被处理,例如捕获必要的异常信息,记录日志方便日后排错,友好的用户响应输出等等.而应用程序发生错误,有可能是应用自身的问题,也有可能是客户端操作的问题.在我们的项目中全局异常处理非常重要.

大概有三种异常情况:
1️⃣.在进入Controller之前,譬如请求一个不存在的地址404错误;

2️⃣.在执行@RequestMapping时,进入逻辑处理阶段前,譬如传的参数类型错误;

3️⃣.以上都正常时,在controller里执行逻辑代码时出的异常,比如出现NullPointerException.

二.Spring Boot默认的错误处理机制。

1. 默认错误处理机制

默认情况下,Spring Boot为两种情况提供了不同的响应方式。
一种是浏览器客户端访问应用发生错误时,一般情况下浏览器默认发送的请求头中Accept: text/html(当然你更改了就另当别论了),所以Spring Boot默认会响应一个html文档内容,称作“Whitelabel Error Page”。

2. 第三方工具请求接口时

另一种是机器客户端访问应用发送错误时,Spring Boot会响应Json格式内容。这种情况更常见于利用第三方的Http工具请求接口时。


如果看过源码,会发现两种方式输出用到的内容项是一样的,只不过第一种方式用html格式显示,第二种方式使用了Json格式。

Spring Boot提供这个错误处理机制靠自动配置的BasicErrorController类。如果好奇Spring Boot是如何实现这种机制的。可以参看下BasicErrorController源码以及SpringMVC请求映射匹配规则。

三.自定义多种错误页面

上述的默认错误处理机制是一种通用的做法,你可能更期望细化一些处理,对于某些错误你可能想特殊对待。

Spring Boot提供了一种方式,笔者认为这种方式是“容器级别”的操作。
如下:

@Configuration
public class ContainerConfig {
    @Bean
    public EmbeddedServletContainerCustomizer containerCustomizer(){
        return new MyCustomizer();
    }
 
    private static class MyCustomizer implements EmbeddedServletContainerCustomizer {
        @Override
        public void customize(ConfigurableEmbeddedServletContainer container) {
            container.addErrorPages(new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/500"));
        }
    }
}

笔者新增了一个配置类,里面定义了一个bean,主要目的在于针对响应码为500的错误,采用笔者自定义的方式处理。如下:

@RestController
public class ExceptionController {
    @RequestMapping("/exception")
    public void catchException() {
        throw new RuntimeException("error occur");
    }
 
    @RequestMapping("/500")
    public String showServerError() {
        return "server error";
    }
}

那么浏览器中结果如下:


很明显这种方式依赖于响应状态码进行定制。看到这里你应该会眼熟,还记得web.xml文件里的配置吗?

<error-page>
    <exception-type>java.lang.Throwable</exception-type>
    <location>/error/500</location>
</error-page>
 
<error-page>
    <error-code>500</error-code>
    <location>/error/500</location>
</error-page>
 
<error-page>
    <error-code>401</error-code>
    <location>/error/401</location>
</error-page>
 
<error-page>
    <error-code>403</error-code>
    <location>/error/403</location>
</error-page>

笔者认为,Spring Boot的默认错误处理机制,包括自定义的错误页面,与原来的web.xml中的error-page配置是相通的,只不过Spring Boot应用默认使用内嵌Servlet容器,web.xml不见了而已。查看源码你会发现,Spring Boot在应用发生错误时会转向"/error"请求,即交由BasicErrorController处理。

四.覆盖默认的错误处理方式

默认错误处理机制的响应内容格式不一定是你相中的,理由可能如下:
1️⃣.“Whitelabel Error Page”页面的样式太单调,用户体验不好;

2️⃣.Json格式的结果字符串不统一,与你配合的前端人员更希望统一格式,好做统一的显示处理。

Spring Boot开发指南上给出了几种方法:

1️⃣.自定义一个bean,实现ErrorController接口,那么默认的错误处理机制将不再生效。

2️⃣.自定义一个bean,继承BasicErrorController类,使用一部分现成的功能,自己也可以添加新的public方法,使用@RequestMapping及其produces属性指定新的地址映射。

3️⃣.自定义一个ErrorAttribute类型的bean,那么还是默认的两种响应方式,只不过改变了内容项而已。

先说下第三种方法,其实查看BasicErrorController源码,响应结果不论是html还是json,内容源都是ErrorAttribute。第三种方法只能改变内容,却改变不了格式,特别是html页面的样式。

其实指南上只是轻描淡写了几种方法,没有很好的示例,不知道是不是指南的作者认为Spring Boot默认的错误处理机制已经很适用了。无论第一种还是第二种办法,都需要你看下BasicErrorController的继承体系及实现,因为BasicErrorController也是实现了ErrorController。

采用第一种方式你可以具有完全的控制权,你可以摒弃默认的“Whitelabel Error Page”,指定自己的视图及视图样式,你可以指定响应的Json格式内容等等,因为BasicErrorController不再起作用,可以参考这里。

由于笔者参照了BasicErrorController的源码,感觉第二种方法可能更简便些,所以实现了第二种方法。第二种方法的思路其实就是你可以通过继承,利用BasicErrorController已有的功能,或者进行扩展。

那么如何覆盖默认的处理行为呢(虽然是自定义bean,但因为是继承,没有覆盖的话还是会采用默认的处理行为)?大致有两种思路。

按照指南上所述,你可以新建public方法,使用@RequestMapping及其produces属性,例如@RequestMapping(produces="application/json"),那么只要请求头中包含“Accept: application/json”,则会映射到你的方法进行处理。看到这儿你可能会疑惑,why?还是建议你先看下BasicErrorController源码。
因为是继承,所以对BasicErrorController默认两种错误处理方式的方法进行override。
笔者希望完全覆盖及可控,所以选择了第二种思路。笔者自定义了MyErrorController,继承于BasicErrorController,注意一定要添加@Controller,不然Spring无法感知自定义的bean,继承于BasicErrorController还是会起作用!

其他
Spring Boot提供的ErrorController是一种全局性的容错机制。你还可以使用SpringMVC提供的@ControllerAdvice。

如字面意思,@ControllerAdvice是切面技术的应用,允许你对Controller中抛出的某个或某些异常进行捕获并响应输出。用法如下:

@ControllerAdvice
public class DefaultExceptionHandler  {
 
    private static final Logger LOGGER = LoggerFactory.getLogger(DefaultExceptionHandler.class); //日志记录器
 
    @ExceptionHandler({MissingServletRequestParameterException.class, TypeMismatchException.class, IllegalArgumentException.class, IllegalStateException.class})
    @ResponseStatus(value = HttpStatus.BAD_REQUEST)
    @ResponseBody
    public JsonResult conversionErrorHandler(Exception ex) {
        //记录日志
        LOGGER.error("参数异常捕获", ex);
 
        return new JsonResult(false, ErrorCode2Msg.getMessage(10004));
    }
}

笔者以往的开发习惯,使用@ControllerAdvice捕获应用级别的异常,使用web.xml中的error-page配置处理容器级别的报错。假设定义的过滤器抛出的异常,@ControllerAdvice是无法处理的(假设定义的过滤器抛出的异常,@ControllerAdvice是无法处理的)。

改用Spring Boot后,@ControllerAdvice没有捕获的异常,ErrorController会帮你“捡起来”。

五.实现步骤

package com.syc.boot.handler;
 
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.FieldError;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
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 org.springframework.web.context.request.WebRequest;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.servlet.NoHandlerFoundException;
 
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
 
/**
 * ControllerAdvice注解:控制器增强,
 * 用来使@ExceptionHandler、@InitBinder、@ModelAttribute注解的方法应用到
 * 所有的 @RequestMapping注解的方法。
 */
@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {
 
    /**
     * 拦截所有Exception类的异常
     */
    @ExceptionHandler(Exception.class)
    @ResponseBody
    public Map<String, Object> exceptionHandler(Exception e) {
        Map<String, Object> result = new HashMap<>();
        result.put("respCode", "1000");
        result.put("respMsg", e.getMessage());
        return result;
    }
 
    /**
     * 400 请求参数封装到bean时 类型转换错误
     * 400 (Bad Request)
     */
    @ExceptionHandler({MethodArgumentTypeMismatchException.class,
            HttpMessageNotReadableException.class,
            MissingServletRequestParameterException.class})
    @ResponseBody
    public Map<String, Object> handleRequestParamFormatError(Exception ex) {
        log.error("handleRequestParamFormatError() bad request ex:{}", ex.getLocalizedMessage());
        Map<String, Object> result = new HashMap<>();
        result.put("respCode", "400");
        result.put("respMsg", ex.getLocalizedMessage());
        return result;
    }
 
    /**
     * 404 (Not Found)
     */
    @ExceptionHandler(NoHandlerFoundException.class)
    @ResponseBody
    public Map<String, Object> handleNoHandlerFoundException(NoHandlerFoundException ex, WebRequest request) {
        log.error("handleNoHandlerFoundException() {}{}{} ", request, " exception message:", ex.getLocalizedMessage());
        Map<String, Object> result = new HashMap<>();
        result.put("respCode", "404");
        result.put("respMsg", ex.getLocalizedMessage());
        return result;
    }
 
    /**
     * 405 (Method Not Allowed)
     */
    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
    @ResponseBody
    public Map<String, Object> handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException ex) {
        log.error("handleHttpRequestMethodNotSupportedException() request Method Not Allowed(405) exception message:{}", ex.getLocalizedMessage());
        Map<String, Object> result = new HashMap<>();
        result.put("respCode", "405");
        result.put("respMsg", ex.getLocalizedMessage());
        return result;
    }
 
 
    //跳转到自定义的错误处理页面
    //由于工作中都是才有前后端分离开发模式,所以一般上都没有直接返回资源页的需求了,
    // 一般上都是返回固定的响应格式,如respCode、respMsg、data,
    // 前端通过判断respCode的值进行业务判断,是弹窗还是跳转页面。
 
    //在校验不通过时,返回的异常信息是不友好的,此时可利用统一异常处理,
    // 对校验异常进行特殊处理,特别说明下,对于异常处理类,
    // 共有以下几种情况(被@RequestBody和@RequestParam注解的请求实体,校验异常类是不同的)
//    @ExceptionHandler(MethodArgumentNotValidException.class)
//    public Map<String, Object> handleBindException(MethodArgumentNotValidException ex) {
//        FieldError fieldError = ex.getBindingResult().getFieldError();
//        log.info("参数校验异常:{}({})", fieldError.getDefaultMessage(), fieldError.getField());
//        Map<String, Object> result = new HashMap<>();
//        result.put("respCode", "1001");
//        result.put("respMsg", fieldError.getDefaultMessage());
//        return result;
//    }
 
}

猜你喜欢

转载自blog.csdn.net/qfchenjunbo/article/details/108361660
今日推荐