SpringBoot的错误页面解析原理

d 在Spring Boot中这些展示错误页面的代码系统会自动的装配好

Spring-boot-autoconfigurejar文件中有一个ErrorMvcAutoConfiguration.class

的类,这个类是一个错误处理的类(可以在类名可以看出这是一个错误代码可以自动配置)我们可以参考这个类来进行原理分析

错误执行流程

@Bean
    @ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
    public DefaultErrorAttributes errorAttributes() {
        return new DefaultErrorAttributes(this.serverProperties.getError().isIncludeException());
    }

    @Bean
    @ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
    public BasicErrorController basicErrorController(ErrorAttributes errorAttributes) {
        return new BasicErrorController(errorAttributes, this.serverProperties.getError(), this.errorViewResolvers);
    }

    @Bean
    public ErrorPageCustomizer errorPageCustomizer() {
        return new ErrorPageCustomizer(this.serverProperties, this.dispatcherServletPath);
    }

    @Bean
    @ConditionalOnBean(DispatcherServlet.class)
    @ConditionalOnMissingBean(ErrorViewResolver.class)
    public DefaultErrorViewResolver conventionErrorViewResolver() {
        return new DefaultErrorViewResolver(this.applicationContext, this.resourceProperties);
    }

可能用到的注解

  • @ConditionalOnMissingBean:这个有个参数是value,value = ErrorAttributes.class,如果ioc中没有ErrorAttributes.class,对象就会使用这个注解下面的代码
  • ${A:${B:C}}: 在这个是在配置文件中取值先来了解下:如果在配置文件中有A就用A,没有A如果有B就用B,如果A和B都没有就用C
  • RequestMapping()注解中有一个produces='text/html'这个将会产生text/html的值,在和请求头中的**Accept**来配对,如请求头中有Accept:text/html就可配对.对这个属性理解的方法我不知道对不对,如果有错误请在下方评论,谢谢

错误执行的步骤--如何找到的错误控制器

在系统运行期间,如果发生了错误(比如:404/500的错误),ErrorPageCustomizer就会生效,就会创建一个ErrorPageCustomizer对象,在这个对象中有一个方法registerErrorPages

@Override
public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
        ErrorPage errorPage = new ErrorPage(
                this.dispatcherServletPath.getRelativePath(this.properties.getError().getPath()));
        errorPageRegistry.addErrorPages(errorPage);
}

这个方法中的this.properties.getError().getPath()方法是获取错误的请求地址,可以点进getPath方法看下

在这个方法正是返回的一个名叫path的属性,跟着代码可以找到这个属性.

public String getPath() {
        return this.path;
}
/**
 * Path of the error controller.
 */
@Value("${error.path:/error}")
private String path = "/error";

这个path的属性在注释可以看出这是一个叫做错误控制器的路径那么就一目了然,哦~如果有错误发现了,那么我就找一个叫error的控制器.我对它发出请求,其于的东西我叫让这个控制器来处理.当拿到了这个路径,创建ErrorPage的工作完成后,将这一个ErrorPage对象放到了errorPageRegistry(错误页面表中).

这里顺便代下这一句代码${error.path:/error}:这个的意思是我先在error.path找,看看有没有值,如果有值我就用这个值,如果没有就用冒号后面的这个/error.其实这里是可以定制的,比如,:我不想让错误请求/error,那么就可以自已写一个错误的控制器,在配置文件修改下这个error.path配置项就可以了.

错误控制器

当发出了error请求后,就肯定有类来处理这个请求,那么就到了BasicErrorController登场了

@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {

@RequestMapping可以发现如果没有修改配置文件的情况下在个是接收一个error的请求.所有的关于error的请求会来BasicErrorController类进行处理

@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
    public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
        HttpStatus status = getStatus(request);
        Map<String, Object> model = Collections
                .unmodifiableMap(getErrorAttributes(request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
        response.setStatus(status.value());
        ModelAndView modelAndView = resolveErrorView(request, response, status, model);
        return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
    }

    @RequestMapping
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
        HttpStatus status = getStatus(request);
        if (status == HttpStatus.NO_CONTENT) {
            return new ResponseEntity<Map<String, Object>>(status);
        }
        Map<String, Object> body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL));
        return new ResponseEntity<>(body, status);
    }

在上面的代码中有两个方法,这个分别是针对浏览器发出请求和不是浏览器发出请求的,在第一个方法上有@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)MediaType.TEXT_HTML_VALUE的值就是text/html.

/**
 * A String equivalent of {@link MediaType#TEXT_HTML}.
 */
public static final String TEXT_HTML_VALUE = "text/html";

当发送了/error请求后,在代码中可以看到会有两种的处理方式

  1. 一种是针对浏览器发送的请求(Accept)中有text/html
  2. 一种是其它客户端发送的请求(Accep)中没有text/html

浏览器发出error请求的处理方式

浏览器发出请求会执行下面的代码.(Accept)中有text/html的情况

@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
    public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
        HttpStatus status = getStatus(request);
        Map<String, Object> model = Collections
                .unmodifiableMap(getErrorAttributes(request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
        response.setStatus(status.value());
        ModelAndView modelAndView = resolveErrorView(request, response, status, model);
        return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
    }

在返回视图的代码也就是在

ModelAndView modelAndView = resolveErrorView(request, response, status, model);

这行代码中,在我们使用SpringMVC的时候,也可以能过ModelAndView类可以转发到其它的页面.

而在resolveErrorView中会生成一个ModelAndView

/**
     * Resolve any specific error views. By default this method delegates to
     * {@link ErrorViewResolver ErrorViewResolvers}.
     * @param request the request
     * @param response the response
     * @param status the HTTP status
     * @param model the suggested model
     * @return a specific {@link ModelAndView} or {@code null} if the default should be
     * used
     * @since 1.4.0
     */
protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status,
        Map<String, Object> model) {
    for (ErrorViewResolver resolver : this.errorViewResolvers) {
        ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
        if (modelAndView != null) {
            return modelAndView;
        }
    }
    return null;
}

在这个代码里面主要调用了一个ErrorViewResolver,是能过ErrorViewResolver得到的ModelAndView如果有就就返回,没有就返回空.

这个ErrorViewResolver就是 我个上面有提到过的DefaultErrorViewResolver,而要去的那个页面就是DefaultErrorViewResolver获取出来的

public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {

在返回那一个页面就是在下面的方法中进行处理的

@Override
    public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
        ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);
        if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
            modelAndView = resolve(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);
        if (provider != null) {
            return new ModelAndView(errorViewName, model);
        }
        return resolveResource(errorViewName, model);
    }

ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);

这行代码中便用的了一个resolve的方法,参入的值是一个状态码和一个map,

resolve方法中可以看到,所要请求的错误页面就是error/错误码.html

这里还有一个模版引擎的处理,如果有模版引擎并且可以创建成功的话,那就使用模版引擎处理

TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,this.applicationContext);
if (provider != null) {
        return new ModelAndView(errorViewName, model);
}
return resolveResource(errorViewName, model);

如果不成功就使用resolveResource方法在一次处理,

如果没有就会找getStaticLocations静态资源文件夹下的error/错误码.html,

private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
        for (String location : this.resourceProperties.getStaticLocations()) {
            try {
                Resource resource = this.applicationContext.getResource(location);
                resource = resource.createRelative(viewName + ".html");
                if (resource.exists()) {
                    return new ModelAndView(new HtmlResourceView(resource), model);
                }
            }
            catch (Exception ex) {
            }
        }
        return null;
    }

如何使用自已的错误页面

通过上面的解释我们大可知道两点:

  • 有模版引擎:查找模版引擎目录下的error文件夹下的错误码.html
  • 无模版引擎:查找静态文件夹下的error文件夹下的错误码.html

所以我们只需要将错误页面放到对应的文件夹下就可以了,下用其它的操作

如何取值

在浏览器中请求到了错误页面的时候,如何将带的信息也取到呢?

BasicErrorController类两种请求片理方式的方法里面会发现都有一个getErrorAttributes(request, isIncludeStackTrace(request, MediaType.**TEXT_HTML**))方法在调用

protected Map<String, Object> getErrorAttributes(HttpServletRequest request, boolean includeStackTrace) {
        WebRequest webRequest = new ServletWebRequest(request);
        return this.errorAttributes.getErrorAttributes(webRequest, includeStackTrace);
    }

而这个方法又调用了errorAttributesgetErrorAttributes方法

this.errorAttributes.getErrorAttributes(webRequest, includeStackTrace);

这个errorAttributes就是ErrorAttributes,在这个类的属性里面可以找到如下代码

private final ErrorAttributes errorAttributes;

而在我们IOC容器中的`ErrorAttributes中的实现类就是DefaultErrorAttributes,我们要获得哪些信息都可以在这个类中找到

DefaultErrorAttributes继承了ErrorAttributes,这个类可以帮我们在页面中共享信息

public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver, Ordered 

DefaultErrorAttributes有这么一段代码

@Override
    public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
        Map<String, Object> errorAttributes = new LinkedHashMap<>();
        errorAttributes.put("timestamp", new Date());
        addStatus(errorAttributes, webRequest);
        addErrorDetails(errorAttributes, webRequest, includeStackTrace);
        addPath(errorAttributes, webRequest);
        return errorAttributes;
    }

看大体的代码可以看出,有一些参数被添加到了errorAttributes这个map里面,然后返回了这个map.

可以获取的值

  • timestamp 时间
  • status 状态码
  • error 错误提示
  • exception 异常对象
  • message 异常消息
  • errors JSR303数据校验的错误

如果错误页面很多咋整

public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {

    private static final Map<Series, String> SERIES_VIEWS;

    static {
        Map<Series, String> views = new EnumMap<>(Series.class);
        views.put(Series.CLIENT_ERROR, "4xx");
        views.put(Series.SERVER_ERROR, "5xx");
        SERIES_VIEWS = Collections.unmodifiableMap(views);
    }

在DefaultErrorViewResolver类中的静态代码块中有一个关于客户端的错误和服务端的错误,也就是如果没有我们精确的错误页面的时候,可以在error的文件夹中创建一个4xx或5xx的文件,如果没有精确的页面就会找到4xx或5xx

如果发生了404的错误,在error的文件夹中有404和4xx两个页面,因为精确的原因,会返回404页面,如果这个文件夹没有404页面就会返回4xx页面

Last modification:October 14th, 2019 at 11:37 am
如果觉得我的文章对你有用,请随意赞赏

Leave a Comment