Spring Boot错误处理

Spring Boot提供了两种类型的错误处理机制,一种是依赖于内嵌容器的ErrorPage机制;另外一种是基于Spring Mvc的异常处理机制;

错误配置类

Spring Boot错误处理相关的自动配置主要是通过ErrorMvcAutoConfiguration实现的:

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

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

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

首先来看看DefaultErrorAttributes:

@Order(Ordered.HIGHEST_PRECEDENCE)
public class DefaultErrorAttributes
    implements ErrorAttributes, HandlerExceptionResolver, Ordered{
    @Override
    public ModelAndView resolveException(HttpServletRequest request,
            HttpServletResponse response, Object handler, Exception ex) {
        storeErrorAttributes(request, ex);
        return null;
    }
}

  • DefaultErrorAttributes实现了HandlerExceptionResolver接口,且为最高优先级,意味着当Spring MVC发生错误时,首先会由它进行处理;
  • resolveException方法将异常对象保存到request中;
  • 关于HandlerExceptionResolver是如何生效的,可以查看DispatcherServlet的相关代码;

ErrorPage机制介绍

接下来看看ErrorMvcAutoConfiguration中定义的ErrorPageCustomizer,这是ErrorPage错误处理比较关键的地方:

首先看内嵌容器的自动配置类EmbeddedServletContainerAutoConfiguration:

@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
        BeanDefinitionRegistry registry) {
    if (this.beanFactory == null) {
        return;
    }
    registerSyntheticBeanIfMissing(registry,
            "embeddedServletContainerCustomizerBeanPostProcessor",
            EmbeddedServletContainerCustomizerBeanPostProcessor.class);
    registerSyntheticBeanIfMissing(registry,
            "errorPageRegistrarBeanPostProcessor",
            ErrorPageRegistrarBeanPostProcessor.class);
}

Spring Boot注册了ErrorPageRegistrarBeanPostProcessor

@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
        throws BeansException {
    if (bean instanceof ErrorPageRegistry) {
        postProcessBeforeInitialization((ErrorPageRegistry) bean);
    }
    return bean;
}


private void postProcessBeforeInitialization(ErrorPageRegistry registry) {
    for (ErrorPageRegistrar registrar : getRegistrars()) {
        registrar.registerErrorPages(registry);
    }
}

当创建bean的时候,如果发现bean实现了ErrorPageRegistry接口,会从Spring容器中查找实现了ErrorPageRegistrar的类,并调用该类的registerErrorPages方法注册Error Page;

而在ErrorMvcAutoConfiguration里,提供了ErrorPageRegistrar的实现类ErrorPageCustomizer,默认将错误转发给/error进行处理;也就是说通过ErrorPageRegistrarBeanPostProcessor会将ErrorPageCustomizer注册到ErrorPageRegistry;

既然默认情况下将错误交给/error进行处理,那我们只要自定义controller类处理/error请求,就可以按需实现自己的错误处理;

在ErrorMvcAutoConfiguration类中,提供了BasicErrorController来处理/error的请求,但用户可以覆盖实现;

接下来看看有哪些类实现了ErrorPageRegistry接口:

public class TomcatEmbeddedServletContainerFactory
    extends AbstractEmbeddedServletContainerFactory{}

public abstract class AbstractEmbeddedServletContainerFactory
    extends AbstractConfigurableEmbeddedServletContainer
    implements EmbeddedServletContainerFactory{}

public abstract class AbstractConfigurableEmbeddedServletContainer
    implements ConfigurableEmbeddedServletContainer{}
    
public interface ConfigurableEmbeddedServletContainer extends ErrorPageRegistry{}

从上面的继承关系可以看到TomcatEmbeddedServletContainerFactory绕了一大圈,实现了ErrorPageRegistry接口,这意味着创建TomcatEmbeddedServletContainerFactory时,将会注册ErrorPage;

上面说介绍的,就是利用ErrorPage处理错误的相关实现;

Spring MVC异常处理

使用Spring Boot框架时,很多时候都是采用Spring MVC处理Http请求;Spring MVC也提供自己异常处理机制:

Spring MVC框架的入口是DispatcherServlet,其中定义了成员变量:

private List<HandlerExceptionResolver> handlerExceptionResolvers;

private void initHandlerExceptionResolvers(ApplicationContext context) {
    this.handlerExceptionResolvers = null;
        //默认为true
    if (this.detectAllHandlerExceptionResolvers) {
        // Find all HandlerExceptionResolvers in the ApplicationContext, including ancestor contexts.
        Map<String, HandlerExceptionResolver> matchingBeans = BeanFactoryUtils
                .beansOfTypeIncludingAncestors(context, HandlerExceptionResolver.class, true, false);
        if (!matchingBeans.isEmpty()) {
            this.handlerExceptionResolvers = new ArrayList<HandlerExceptionResolver>(matchingBeans.values());
            // We keep HandlerExceptionResolvers in sorted order.
            AnnotationAwareOrderComparator.sort(this.handlerExceptionResolvers);
        }
    }
    else {
        try {
            HandlerExceptionResolver her =
                    context.getBean(HANDLER_EXCEPTION_RESOLVER_BEAN_NAME, HandlerExceptionResolver.class);
            this.handlerExceptionResolvers = Collections.singletonList(her);
        }
        catch (NoSuchBeanDefinitionException ex) {
            // Ignore, no HandlerExceptionResolver is fine too.
        }
    }

    // Ensure we have at least some HandlerExceptionResolvers, by registering
    // default HandlerExceptionResolvers if no other resolvers are found.
    if (this.handlerExceptionResolvers == null) {
        this.handlerExceptionResolvers = getDefaultStrategies(context, HandlerExceptionResolver.class);
        if (logger.isDebugEnabled()) {
            logger.debug("No HandlerExceptionResolvers found in servlet '" + getServletName() + "': using default");
        }
    }
}

那么Spring Boot在什么地方提供了HandlerExceptionResolver的实现呢?具体的实现在WebMvcAutoConfiguration类,这里为了方便介绍,将EnableWebMvcConfiguration和它的父类WebMvcConfigurationSupport代码放到一起:

@Bean
public HandlerExceptionResolver handlerExceptionResolver() {
    List<HandlerExceptionResolver> exceptionResolvers = new ArrayList<HandlerExceptionResolver>();
    configureHandlerExceptionResolvers(exceptionResolvers);
    if (exceptionResolvers.isEmpty()) {
        addDefaultHandlerExceptionResolvers(exceptionResolvers);
    }
    extendHandlerExceptionResolvers(exceptionResolvers);
    HandlerExceptionResolverComposite composite = new HandlerExceptionResolverComposite();
    composite.setOrder(0);
    composite.setExceptionResolvers(exceptionResolvers);
    return composite;
}

@Override
protected void configureHandlerExceptionResolvers(
        List<HandlerExceptionResolver> exceptionResolvers) {
    super.configureHandlerExceptionResolvers(exceptionResolvers);
    if (exceptionResolvers.isEmpty()) {
        addDefaultHandlerExceptionResolvers(exceptionResolvers);
    }
    if (this.mvcProperties.isLogResolvedException()) {
        for (HandlerExceptionResolver resolver : exceptionResolvers) {
            if (resolver instanceof AbstractHandlerExceptionResolver) {
                ((AbstractHandlerExceptionResolver) resolver)
                        .setWarnLogCategory(resolver.getClass().getName());
            }
        }
    }
}

protected final void addDefaultHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) {
    ExceptionHandlerExceptionResolver exceptionHandlerResolver = createExceptionHandlerExceptionResolver();
    exceptionHandlerResolver.setContentNegotiationManager(mvcContentNegotiationManager());
    exceptionHandlerResolver.setMessageConverters(getMessageConverters());
    exceptionHandlerResolver.setCustomArgumentResolvers(getArgumentResolvers());
    exceptionHandlerResolver.setCustomReturnValueHandlers(getReturnValueHandlers());
    if (jackson2Present) {
        exceptionHandlerResolver.setResponseBodyAdvice(
                Collections.<ResponseBodyAdvice<?>>singletonList(new JsonViewResponseBodyAdvice()));
    }
    exceptionHandlerResolver.setApplicationContext(this.applicationContext);
    exceptionHandlerResolver.afterPropertiesSet();
    exceptionResolvers.add(exceptionHandlerResolver);

    ResponseStatusExceptionResolver responseStatusResolver = new ResponseStatusExceptionResolver();
    responseStatusResolver.setMessageSource(this.applicationContext);
    exceptionResolvers.add(responseStatusResolver);

    exceptionResolvers.add(new DefaultHandlerExceptionResolver());
}

从上面的代码可以看到,最终提供了HandlerExceptionResolverComposite包装类作为HandlerExceptionResolver的实现,包装了ExceptionHandlerExceptionResolver、ResponseStatusExceptionResolver和
DefaultHandlerExceptionResolver三个具体的实现类;
其中ExceptionHandlerExceptionResolver会读取方法的@ExceptionHandler注解,从而进行异常处理;

ExceptionHandlerExceptionResolver首先会搜索Controller类本身,接着从添加了ControllerAdvice注解的类里面寻找;

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 222,681评论 6 517
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 95,205评论 3 399
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 169,421评论 0 362
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 60,114评论 1 300
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 69,116评论 6 398
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,713评论 1 312
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 41,170评论 3 422
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 40,116评论 0 277
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,651评论 1 320
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,714评论 3 342
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,865评论 1 353
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,527评论 5 351
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 42,211评论 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,699评论 0 25
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,814评论 1 274
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 49,299评论 3 379
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,870评论 2 361

推荐阅读更多精彩内容