SpringBoot学习笔记——(四)错误页面

SpringBoot错误页面

关于SpringBoot错误页面和错误数据,SpringBoot提供的自动配置请参考ErrorMvcAutoConfiguration

1.SpringBoot为我们通过的默认错误页面为

SpringBoot学习笔记——(四)错误页面_第1张图片

2.如何定制错误页面:

首先我们打开ErrorMvcAutoConfiguration类可以发现,该类给容器中添加了一下组件:

    1. DefaultErrorAttributes
public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver, Ordered {
    private static final String ERROR_ATTRIBUTE = DefaultErrorAttributes.class.getName() + ".ERROR";
    private final boolean includeException;

    public DefaultErrorAttributes() {
        this(false);
    }

    public DefaultErrorAttributes(boolean includeException) {
        this.includeException = includeException;
    }

    public int getOrder() {
        return -2147483648;
    }

    public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        this.storeErrorAttributes(request, ex);
        return null;
    }

    private void storeErrorAttributes(HttpServletRequest request, Exception ex) {
        request.setAttribute(ERROR_ATTRIBUTE, ex);
    }

    //共享一个timestamp变量,封装错误的时间
    public Map getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
        Map errorAttributes = new LinkedHashMap();
        errorAttributes.put("timestamp", new Date());
        this.addStatus(errorAttributes, webRequest);
        this.addErrorDetails(errorAttributes, webRequest, includeStackTrace);
        this.addPath(errorAttributes, webRequest);
        return errorAttributes;
    }
    
    //共享一个status变量,该变量封装的是错误的状态码
    private void addStatus(Map errorAttributes, RequestAttributes requestAttributes) {
        Integer status = (Integer)this.getAttribute(requestAttributes, "javax.servlet.error.status_code");
        if (status == null) {
            errorAttributes.put("status", 999);
            errorAttributes.put("error", "None");
        } else {
            errorAttributes.put("status", status);

            try {
                errorAttributes.put("error", HttpStatus.valueOf(status).getReasonPhrase());
            } catch (Exception var5) {
                errorAttributes.put("error", "Http Status " + status);
            }

        }
    }
    
    //共享了一个exception变量,该变量封装的错误异常
    private void addErrorDetails(Map errorAttributes, WebRequest webRequest, boolean includeStackTrace) {
        Throwable error = this.getError(webRequest);
        if (error != null) {
            while(true) {
                if (!(error instanceof ServletException) || error.getCause() == null) {
                    if (this.includeException) {
                        errorAttributes.put("exception", error.getClass().getName());
                    }

                    this.addErrorMessage(errorAttributes, error);
                    if (includeStackTrace) {
                        this.addStackTrace(errorAttributes, error);
                    }
                    break;
                }

                error = ((ServletException)error).getCause();
            }
        }

        Object message = this.getAttribute(webRequest, "javax.servlet.error.message");
        if ((!StringUtils.isEmpty(message) || errorAttributes.get("message") == null) && !(error instanceof BindingResult)) {
            errorAttributes.put("message", StringUtils.isEmpty(message) ? "No message available" : message);
        }

    }

    //共享一个message变量,该变量封装错误的消息
    private void addErrorMessage(Map errorAttributes, Throwable error) {
        BindingResult result = this.extractBindingResult(error);
        if (result == null) {
            errorAttributes.put("message", error.getMessage());
        } else {
            if (result.getErrorCount() > 0) {
                errorAttributes.put("errors", result.getAllErrors());
                errorAttributes.put("message", "Validation failed for object='" + result.getObjectName() + "'. Error count: " + result.getErrorCount());
            } else {
                errorAttributes.put("message", "No errors");
            }

        }
    }

    private BindingResult extractBindingResult(Throwable error) {
        if (error instanceof BindingResult) {
            return (BindingResult)error;
        } else {
            return error instanceof MethodArgumentNotValidException ? ((MethodArgumentNotValidException)error).getBindingResult() : null;
        }
    }
    
    //共享一个trace变量,该变量封装错误的轨迹
    private void addStackTrace(Map errorAttributes, Throwable error) {
        StringWriter stackTrace = new StringWriter();
        error.printStackTrace(new PrintWriter(stackTrace));
        stackTrace.flush();
        errorAttributes.put("trace", stackTrace.toString());
    }
    
    //共享一个path变量,该变量封装的错误路径
    private void addPath(Map errorAttributes, RequestAttributes requestAttributes) {
        String path = (String)this.getAttribute(requestAttributes, "javax.servlet.error.request_uri");
        if (path != null) {
            errorAttributes.put("path", path);
        }

    }

    public Throwable getError(WebRequest webRequest) {
        Throwable exception = (Throwable)this.getAttribute(webRequest, ERROR_ATTRIBUTE);
        if (exception == null) {
            exception = (Throwable)this.getAttribute(webRequest, "javax.servlet.error.exception");
        }

        return exception;
    }

    private  T getAttribute(RequestAttributes requestAttributes, String name) {
        return requestAttributes.getAttribute(name, 0);
    }
}

通过上面的代码,仔细看注释,就可以发现,该类在发生错误的时候,在request与中共享了一些变量,这些变量分别封装了错误相关的数据, 我们在自定义错误页面的时候,就可以使用这些变量来获取相应的数据。

    1. BasicErrorController
@Controller
@RequestMapping({"${server.error.path:${error.path:/error}}"})
public class BasicErrorController extends AbstractErrorController {
    @RequestMapping(
        produces = {"text/html"}//产生html类型的错误页面;浏览器发送的请求,请求同会有一个请求同为text/html,因此就会被该方法接收
    )
    public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
        HttpStatus status = this.getStatus(request);
        //model里面就是错误页面所包含的数据
        //getErrorAttributes就是获取错误页面需要展示的模型数据
        Map model = Collections.unmodifiableMap(this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.TEXT_HTML)));
        response.setStatus(status.value());
        //去哪个页面作为错误页面、ModelView包含页面地址和页面需要展示的数据
        //然而resolveErrorView就是解析到错误页面的名字和地址进行放回
        ModelAndView modelAndView = this.resolveErrorView(request, response, status, model);
        return modelAndView != null ? modelAndView : new ModelAndView("error", model);
    }

    @RequestMapping
    @ResponseBody//产生json数据格式的错误信息,客户端发送请求会被该方法接收
    public ResponseEntity> error(HttpServletRequest request) {
        //body中就回显错误信息所包含的数据
        Map body = this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.ALL));
        HttpStatus status = this.getStatus(request);
        return new ResponseEntity(body, status);
    }

通过这段代码就可以发现,BasicErrorController就是处理/error请求的Controller。

下面就是解析错误页面的一段代码:

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;
    }

通过这段代码就可以发现,首先是拿到所有的错误视图解析器,然后再解析到错误页面。

    1. ErrorPageCustomizer
    @Value("${error.path:/error}")
    private String path = "/error";//系统一旦出现错误就会发送/error请求进行处理
    1. DefaultErrorViewResolver
    static {
        Map views = new EnumMap(Series.class);
        views.put(Series.CLIENT_ERROR, "4xx");
        views.put(Series.SERVER_ERROR, "5xx");
        SERIES_VIEWS = Collections.unmodifiableMap(views);
    }
    public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map model) {
        //通过错误状态码解析到指定的错误页面
        ModelAndView modelAndView = this.resolve(String.valueOf(status), model);
        //如果解析到的错误状态码在指定的页面路径中没有解析到页面就会以4xx或者5xx来解析
        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) {
        //解析的错误页面为:error/错误状态码
        String errorViewName = "error/" + viewName;
        //如果模板引擎能起作用,就用模板引擎来解析错误页面
        TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName, this.applicationContext);
        //如果模板引擎可用,就返回模板引擎解析的地址,
        //如果模板引擎不可用,就调用resolveResource方法来解析到的地址
        return provider != null ? new ModelAndView(errorViewName, model) : this.resolveResource(errorViewName, model);
    }
    
    private ModelAndView resolveResource(String viewName, Map model) {   
        //在静态资源下去找对应的错误页面
        String[] var3 = this.resourceProperties.getStaticLocations();
        int var4 = var3.length;

        for(int var5 = 0; var5 < var4; ++var5) {
            String location = var3[var5];

            try {
                Resource resource = this.applicationContext.getResource(location);
                resource = resource.createRelative(viewName + ".html");
                if (resource.exists()) {
                    return new ModelAndView(new DefaultErrorViewResolver.HtmlResourceView(resource), model);
                }
            } catch (Exception var8) {
                ;
            }
        }

        return null;
    }

通过对上面源码的分析:我们就能知道DefaultErrorViewResolver的作用啦:
1.若果模板引擎不存在,就直接访问静态资源文件夹下面的 /error/状态码.html 路径下的资源作为错误页面进行展示。2.如果模板引擎存在,就以模板引擎解析的地址页面作为错误页面的展示页面。3.如果解析的错误页面路径中没有指定状态码的错误页面,就会去该路径下寻找是否存在4xx或者5xx(4xx代表以4开头的所有状态的错误,5xx就是代表以5开头的错误)的页面。

说明:首先一旦系统出现4xx或者5xx的错误,ErrorPageCustomizer就会起作用。(ErrorPageCustomizer他会定制出现错误的响应规则),会发起/error请求,然后就会被、BasicErrorController处理。处理后会返回错误页面或者错误数据,去哪个错误页面就会由DefaultErrorViewResolver来解析得到的。那么,解析到的页面,页面中的数据又是从哪里来的呢?其实就是从DefaultErrorAttributes来封装页面需要展示的错误信息等数据。

总结:通过上面的分析:Springboot对错误页面做了如下的配置,如果系统出现错误。如果模板引擎存在,就会通过模板引擎解析错误页面展示相应的错误页面。如果模板引擎不存在,就会从静态资源中解析用户自定义的错误页面展示,如果用户没有定义和没有模板引擎的解析到用户定义的错误页面,就会被:WhitelabelErrorViewConfiguration解析,也就是SpringBoot提供的默认错误页面。

在静态资源目录下创建一个error目录,并添加一个404.html或者4xx.html页面,我们启动应用,访问一个不存在的路径,就会用对应的自定义错误页面显示出来。如果需要在错误页面中展示相关的错误信息,就可以在页面中获取DefaultErrorAttributes为我们提供的一些变量来获取数据。

3.如何定制错误数据:

前面我们已经说了,SpringBoot根据访问地址的客户端不同,如果服务器发生错误,响应的错误信息也不同,如果是浏览器,就返回一个错误页面。如果是其他客户端,就返回错误的json数据。上面已经讲解了如何自定义错误页面。那么接下来,就是我们讲解如何自定义错误信息。

  • 方式一:传统方式,就是自定义一个异常处理器,来处理服务器发生错误请求。

    首先定义一个请求:
@Controller
public class TestController {
    @RequestMapping("hello")
    @ResponseBody
    public String hello(@RequestParam("user") String user) throws Exception {
        if(user.equals("aaa")){
            throw new Exception("jiushi");
        }
        return "hello world";
    }
}

然后再定义一个异常处理器:

@ControllerAdvice
public class TestExceptionHanlder {

    @ResponseBody
    @ExceptionHandler(Exception.class)
    public Map handlerException(Exception e){
        Map map = new HashMap<>();
        map.put("code","user not exist");
        map.put("exception",e.getMessage());
        return map;
    }
}

这样,就能保证服务报错,就会被该异常处理器捕获并返回报错信息。但是这样存在缺点就是,不能自适应,当浏览器访问的时候也会打印报错信息。而不是返回报错页面。


如果改写成下面这种方式就可以啦:

@ControllerAdvice
public class TestExceptionHanlder {

    @ExceptionHandler(Exception.class)
    public String  handlerException(Exception e){
        Map map = new HashMap<>();
        map.put("code","user not exist");
        map.put("exception",e.getMessage());
        return "forward:/error";
    }
}

但是这种方式并没有吧我们自定义的错误信息,在客户端访问的时候打印出现。那么该如何把我们自定义封装的变量的数据也展示出去呢?

方式有两种:

  • 重写AbstractErrorController
    这种方式太,麻烦。因此不建议采用这种方式。
  • 重写ErrorAttributes
@Component
public class MyErrorAttributes extends DefaultErrorAttributes {
    @Override
    public Map getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
        Map map = super.getErrorAttributes(webRequest, includeStackTrace);
        map.put("ext","hahha");
        return map;
    }
}

你可能感兴趣的:(SpringBoot,SpringBoot)