Spring Boot Web自定义错误页面及全局异常处理 时间: 2019-01-05 15:26 分类: JAVA Web,Spring,JAVA ####自定义错误页面(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.html`、`403.html`、`500.html` 第一种方法就不说了,第二种其实 Spring Boot 也已经帮我们做好了,Spring Boot 在启动的时候会自动的装配一个`DefaultErrorViewResolver`的 Bean,这个 Bean 就是默认的用来处理错误视图的。下面我们来看源码它是如何处理错误页面的: ```java public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map 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 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/ | + +- resources/ +- templates/ +- error/ | +- 500.html +- ``` 此时重新运行程序会发现即使`templates`下存在`error.html`,也是返回的`500.html`的内容。 上面两种都是 Spring Boot 默认的错误页面规则,下面我们来自定义自己的错误页面。 #####实现 ErrorViewResolver 接口的方式 这种方式简单暴力,我们可以参见 Spring Boot 默认的`DefaultErrorViewResolver`类,它就是实现了`ErrorViewResolver`接口,根据`HttpStatus`状态码返回不同的错误视图。 新建`CustomErrorViewResolver`类: ```java 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 model) { return new ModelAndView("customError", model); } } ``` 这里我就简单的直接返回`customError`视图,所以需要在`templates`下新建`customError.html`: ```html Title 这是 CustomErrorViewResolver 自定义返回的错误页面! ``` 再次运行程序,发现已经是我们自定义错误视图解析器返回的`customError.html`页面内容了。 下面介绍另外一种方式。 #####继承 BasicErrorController 或者 AbstractErrorController 类 看源码我们可以知道`BasicErrorController`继承的`AbstractErrorController`,所以这里就只讲继承`BasicErrorController`类的方法了。 新建`ErrorController`类: ```java 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 errorViewResolvers) * 里面的参数,否则会报第二个参数的 bean 找不到,容器中只有 ServerProperties,需通过 ServerProperties.getError 获取 ErrorProperties */ public ErrorController(ErrorAttributes errorAttributes, ServerProperties serverProperties, List 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 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> error(HttpServletRequest request) { HttpStatus status = getStatus(request); Map body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL)); Map 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> error(HttpServletRequest request) 在上面`errorHtml`方法中我们还是直接调用的`BasicErrorController`的`resolveErrorView`方法,而`BasicErrorController`中的`resolveErrorView`是调用的`AbstractErrorController`的`resolveErrorView`,也就是个委托机制一样,最终我们查看`AbstractErrorController`中的`resolveErrorView`: ```java protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status, Map 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][1] 当然了,在我们自定义`ErrorController`的`errorHtml`方法中,完全可以不调用父类的`resolveErrorView`来获取视图模型,根据自己需求来实现高度定制化。 ####全局异常处理 前面所有的说的都是由于异常没处理导致的错误视图渲染,接下来就是对于全局异常的处理了。 处理方法也有多种,这里只讲最常见的的一种:`@ControllerAdvice`注解。 新建全局异常处理类`GlobalExceptionHandler.java`: ```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 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`异常都会进入到该方法中,然后我们就可以做统一处理了。 [1]: https://0o0.me/usr/uploads/2019/01/723720939.png 标签: 无