admin

Spring Boot Web自定义错误页面及全局异常处理
自定义错误页面(Custom Error Pages)在默认情况下,Spring Boot Web 后台发生错误(...
扫描右侧二维码阅读全文
05
2019/01

Spring Boot Web自定义错误页面及全局异常处理

自定义错误页面(Custom Error Pages)

在默认情况下,Spring Boot Web 后台发生错误(异常未处理导致)时,会出现类似如下的错误页面:

Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.

Sat Jan 05 14:02:38 CST 2019
There was an unexpected error (type=Internal Server Error, status=500).
/ by zero

很显然,出现这种页面给用户看到是不友好的,那么我们就需要来自定义错误页面。
如果我们此时直接在templates下新建error.html,那么再次运行程序就会发现,错误页面变成error.html的内容了,也就是说,Spring Boot 默认情况下会先去找error.html这个视图文件,如果存在的话就进行渲染返回,否则返回最上面的Whitelabel Error Page
当然了,这是最简单的自定义错误页面的方法,如果我们想针对不同的错误响应不同的信息给用户呢?

  1. error.html里面统一处理
  2. 对于每一种不同的错误都建一个独立的html页面,比如 404、403、500 错误,对应404.html403.html500.html

第一种方法就不说了,第二种其实 Spring Boot 也已经帮我们做好了,Spring Boot 在启动的时候会自动的装配一个DefaultErrorViewResolver的 Bean,这个 Bean 就是默认的用来处理错误视图的。下面我们来看源码它是如何处理错误页面的:

public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
    ModelAndView modelAndView = this.resolve(String.valueOf(status.value()), model);
    if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
        modelAndView = this.resolve((String)SERIES_VIEWS.get(status.series()), model);
    }

    return modelAndView;
}

private ModelAndView resolve(String viewName, Map<String, Object> model) {
    String errorViewName = "error/" + viewName;
    TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName, this.applicationContext);
    return provider != null ? new ModelAndView(errorViewName, model) : this.resolveResource(errorViewName, model);
}

可以看到上面对应服务器错误页面的处理是根据HttpStatus状态码来寻找错误视图的,而寻找的路径是classpath: templates/error
所以对于之前那个 500 错误,我们在templates下新建error目录并添加500.html文件:

src/
 +- main/
     +- java/
     | + <source code>
     +- resources/
     +- templates/
            +- error/
            | +- 500.html
     +- <other templates>

此时重新运行程序会发现即使templates下存在error.html,也是返回的500.html的内容。
上面两种都是 Spring Boot 默认的错误页面规则,下面我们来自定义自己的错误页面。

实现 ErrorViewResolver 接口的方式

这种方式简单暴力,我们可以参见 Spring Boot 默认的DefaultErrorViewResolver类,它就是实现了ErrorViewResolver接口,根据HttpStatus状态码返回不同的错误视图。
新建CustomErrorViewResolver类:

package me._0o0.errorhandling.viewresolver;

import org.springframework.boot.autoconfigure.web.servlet.error.ErrorViewResolver;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import java.util.Map;

/**
 * @program: error-handling
 * @description:    自定义错误视图解析器
 * @author: Mr.Xu
 * @create: 2019-01-05 11:46
 **/
@Component
public class CustomErrorViewResolver implements ErrorViewResolver {

    @Override
    public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
        return new ModelAndView("customError", model);
    }
}

这里我就简单的直接返回customError视图,所以需要在templates下新建customError.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    这是 CustomErrorViewResolver 自定义返回的错误页面!
</body>
</html>

再次运行程序,发现已经是我们自定义错误视图解析器返回的customError.html页面内容了。
下面介绍另外一种方式。

继承 BasicErrorController 或者 AbstractErrorController 类

看源码我们可以知道BasicErrorController继承的AbstractErrorController,所以这里就只讲继承BasicErrorController类的方法了。
新建ErrorController类:

package me._0o0.errorhandling.controller;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController;
import org.springframework.boot.autoconfigure.web.servlet.error.ErrorViewResolver;
import org.springframework.boot.web.servlet.error.ErrorAttributes;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @program: error-handling
 * @description:
 * @author: Mr.Xu
 * @create: 2019-01-05 10:00
 **/
@Controller
public class ErrorController extends BasicErrorController {

    Logger logger = LoggerFactory.getLogger(ErrorController.class);

    /**
     * 注意,不能用 BasicErrorController(ErrorAttributes errorAttributes, ErrorProperties errorProperties, List<ErrorViewResolver> errorViewResolvers)
     * 里面的参数,否则会报第二个参数的 bean 找不到,容器中只有 ServerProperties,需通过 ServerProperties.getError 获取 ErrorProperties
     */
    public ErrorController(ErrorAttributes errorAttributes, ServerProperties serverProperties, List<ErrorViewResolver> errorViewResolvers) {
        super(errorAttributes, serverProperties.getError(), errorViewResolvers);
        logger.debug("ErrorController created!");
    }


    /**
     * 覆盖原始的 html 错误响应
     * @param request
     * @param response
     * @return
     */
    @Override
    public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {

        HttpStatus status = getStatus(request);
        response.setStatus(status.value());

        Map<String, Object> model = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.TEXT_HTML));

        //resolveErrorView 默认是根据 status 去 classpath: templates/error 下找对应的 status.html(比如 500.html、404.html)
        //没有找到对应的模板文件则返回 null
        ModelAndView modelAndView = resolveErrorView(request, response, status, model);

        //如果没有找到对应 status 的模板文件,则新建一个自定义的 ModelAndView,viewName 为 error,即对应 classpath: templates/error.html 模板文件
        return modelAndView != null ? modelAndView : new ModelAndView("error", model);
    }

    /**
     * 覆盖原始的 json 错误响应
     * @param request
     * @return
     */
    @Override
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {

        HttpStatus status = getStatus(request);

        Map<String, Object> body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL));

        Map<String, Object> result = new HashMap<>();
        result.put("code", status.value());
        result.put("msg", body.get("message"));

        return new ResponseEntity(result, status);
    }
}

主要重写两个方法:

  1. public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response)
  2. public ResponseEntity<Map<String, Object>> error(HttpServletRequest request)

在上面errorHtml方法中我们还是直接调用的BasicErrorControllerresolveErrorView方法,而BasicErrorController中的resolveErrorView是调用的AbstractErrorControllerresolveErrorView,也就是个委托机制一样,最终我们查看AbstractErrorController中的resolveErrorView

protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status, Map<String, Object> model) {
    Iterator var5 = this.errorViewResolvers.iterator();

    ModelAndView modelAndView;
    do {
        if (!var5.hasNext()) {
            return null;
        }

        ErrorViewResolver resolver = (ErrorViewResolver)var5.next();
        modelAndView = resolver.resolveErrorView(request, status, model);
    } while(modelAndView == null);

    return modelAndView;
}

神奇的是和之前讲的ErrorViewResolver结合起来了,可以看到在上面这个方法中是迭代errorViewResolvers列表,默认是只有一个DefaultErrorViewResolver,如果我们之前自定义了CustomErrorViewResolver,那么就会有两个:
20190105150147.png
当然了,在我们自定义ErrorControllererrorHtml方法中,完全可以不调用父类的resolveErrorView来获取视图模型,根据自己需求来实现高度定制化。

全局异常处理

前面所有的说的都是由于异常没处理导致的错误视图渲染,接下来就是对于全局异常的处理了。
处理方法也有多种,这里只讲最常见的的一种:@ControllerAdvice注解。
新建全局异常处理类GlobalExceptionHandler.java

package me._0o0.errorhandling.controller;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
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.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;

/**
 * @program: error-handling
 * @description: 全局异常处理器
 * @author: Mr.Xu
 * @create: 2019-01-05 15:09
 **/
@ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

    @ExceptionHandler(ArithmeticException.class)
    @ResponseBody
    ResponseEntity<?> handleArithmeticException(HttpServletRequest request, Throwable ex) {
        HttpStatus status = getStatus(request);

        Map<String, Object> body = new HashMap<>();
        body.put("code", status.value());
        body.put("msg", ex.getMessage());

        return new ResponseEntity<>(body, status);
    }
    private HttpStatus getStatus(HttpServletRequest request) {
        Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code");
        if (statusCode == null) {
            return HttpStatus.INTERNAL_SERVER_ERROR;
        }
        return HttpStatus.valueOf(statusCode);
    }
}

该类继承了ResponseEntityExceptionHandler,这个类里面实现了很多的异常处理,有兴趣的可以看看源码。
最开始我们测试的是一个除数为0的异常,所以在上面的全局异常处理器中,handleArithmeticException方法上我们添加@ExceptionHandler(ArithmeticException.class)注解并指定处理的异常是ArithmeticException,此时系统中未处理的ArithmeticException异常都会进入到该方法中,然后我们就可以做统一处理了。

Last modification:January 5th, 2019 at 03:26 pm
If you think my article is useful to you, please feel free to appreciate

Leave a Comment