跳至主要內容

32. ErrorPage 全局异常处理

安图新大约 5 分钟

32. ErrorPage 全局异常处理

前言

@ControllerAdvice 全局异常处理 和 自定义 HandlerExceptionResolver 解析异常,这两种方式只能处理在处理请求请求到达了 DispatcherServlet ,并且出现了异常后进入 processDispatchResult( ) 方法。

这两种方式不适用的场景:
1、 请求没有到达 DispatcherServlet 的核心流程,如在 filter 中抛出异常;
2、 请求进入 processDispatchResult()方法处理异常,但是在处理过程中有抛出了异常,如在@ControllerAdvice 方法中抛出了异常;

这个时候请求会进入到 ErrorPage 的处理流程。

一、ErrorPage 初始化

1、 ErrorPageCustomizer;

实现了 ErrorPageRegistrar 接口,重写了 registerErrorPages( ) 方法,用于注册 ErrorPage

(1)ErrorPageCustomizer 的定义

static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered {



		private final ServerProperties properties;

		private final DispatcherServletPath dispatcherServletPath;

		protected ErrorPageCustomizer(ServerProperties properties, DispatcherServletPath dispatcherServletPath) {


			this.properties = properties;
			this.dispatcherServletPath = dispatcherServletPath;
		}

		@Override
		public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {


			//注册错误页面,默认Path 是 /error
			ErrorPage errorPage = new ErrorPage(
					this.dispatcherServletPath.getRelativePath(this.properties.getError().getPath()));
			errorPageRegistry.addErrorPages(errorPage);
		}

		@Override
		public int getOrder() {


			return 0;
		}

	}

(2)、默认错误路径

public class ErrorProperties {



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

(3)、创建 ErrorPageCustomizer

ErrorMvcAutoConfiguration.java

	@Bean
	public ErrorPageCustomizer errorPageCustomizer(DispatcherServletPath dispatcherServletPath) {


		return new ErrorPageCustomizer(this.serverProperties, dispatcherServletPath);
	}

2、 ErrorPageRegistrarBeanPostProcessor;

ErrorPageRegistrar 的后置处理器,注册错误页面到 web 容器

public class ErrorPageRegistrarBeanPostProcessor implements BeanPostProcessor, BeanFactoryAware {



	private ListableBeanFactory beanFactory;

	private List<ErrorPageRegistrar> registrars;

	@Override
	public void setBeanFactory(BeanFactory beanFactory) {


		Assert.isInstanceOf(ListableBeanFactory.class, beanFactory,
				"ErrorPageRegistrarBeanPostProcessor can only be used with a ListableBeanFactory");
		this.beanFactory = (ListableBeanFactory) beanFactory;
	}

	@Override
	public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {


		if (bean instanceof ErrorPageRegistry) {


			//bean是 ErrorPageRegistry 类型
			postProcessBeforeInitialization((ErrorPageRegistry) bean);
		}
		return bean;
	}

	@Override
	public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {


		return bean;
	}

	private void postProcessBeforeInitialization(ErrorPageRegistry registry) {


		//调用 ErrorPageRegistrar 的 registerErrorPages() 注册错误页面
		for (ErrorPageRegistrar registrar : getRegistrars()) {


			registrar.registerErrorPages(registry);
		}
	}

	//从beanFactory中获取 ErrorPageRegistrar.class 类型的类,通过 order 排序
	private Collection<ErrorPageRegistrar> getRegistrars() {


		if (this.registrars == null) {


			// Look up does not include the parent context
			this.registrars = new ArrayList<>(
					this.beanFactory.getBeansOfType(ErrorPageRegistrar.class, false, false).values());
			this.registrars.sort(AnnotationAwareOrderComparator.INSTANCE);
			this.registrars = Collections.unmodifiableList(this.registrars);
		}
		return this.registrars;
	}

}

ErrorPage 被添加到了 web 容器中

AbstractConfigurableWebServerFactory.java

public void addErrorPages(ErrorPage... errorPages) {


		Assert.notNull(errorPages, "ErrorPages must not be null");
		this.errorPages.addAll(Arrays.asList(errorPages));
	}

3、 将 ErrorPage 添加到 DeploymentInfo 中;

我这里使用的容器是 Undertow 服务,在创建服务的过程中

refresh( ) -> onRefresh( ) -> createWebServer( ) -> getWebServer( ) -> createManager( ) -> configureErrorPages( )

	private void configureErrorPages(DeploymentInfo servletBuilder) {


		//获取到刚才注册的 ErrorPage
		for (ErrorPage errorPage : getErrorPages()) {


			servletBuilder.addErrorPage(getUndertowErrorPage(errorPage));
		}
	}

封装成 undertow api 的 ErrorPage

private io.undertow.servlet.api.ErrorPage getUndertowErrorPage(ErrorPage errorPage) {


		if (errorPage.getStatus() != null) {


			return new io.undertow.servlet.api.ErrorPage(errorPage.getPath(), errorPage.getStatusCode());
		}
		if (errorPage.getException() != null) {


			return new io.undertow.servlet.api.ErrorPage(errorPage.getPath(), errorPage.getException());
		}
		return new io.undertow.servlet.api.ErrorPage(errorPage.getPath());
	}

添加到 DeploymentInfo 缓存起来

DeploymentInfo .java

public DeploymentInfo addErrorPage(final ErrorPage errorPage) {


        this.errorPages.add(errorPage);
        return this;
    }

4、

configureErrorPages( )执行后, ->manager.deploy(); -> initializeErrorPages(deployment, deploymentInfo);

(1)initializeErrorPages()

private void initializeErrorPages(final DeploymentImpl deployment, final DeploymentInfo deploymentInfo) {


        final Map<Integer, String> codes = new HashMap<>();
        final Map<Class<? extends Throwable>, String> exceptions = new HashMap<>();
        String defaultErrorPage = null;
        for (final ErrorPage page : deploymentInfo.getErrorPages()) {


            if (page.getExceptionType() != null) {


            	//exceptions 保存异常类型和路径的映射
                exceptions.put(page.getExceptionType(), page.getLocation());
            } else if (page.getErrorCode() != null) {


            	//codes 保存异常状态码和路径的映射
                codes.put(page.getErrorCode(), page.getLocation());
            } else {


            	//默认异常路径只能有一个
                if (defaultErrorPage != null) {


                    throw UndertowServletMessages.MESSAGES.moreThanOneDefaultErrorPage(defaultErrorPage, page.getLocation());
                } else {


                    defaultErrorPage = page.getLocation();
                }
            }
        }
        deployment.setErrorPages(new ErrorPages(codes, exceptions, defaultErrorPage));
    }

5、 BasicErrorController;

请求出现错误时,且没有被其他的全局异常处理拦截到,会将请求流转到 ErrorController 中的 /error 和 /errorHtml 接口

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



public BasicErrorController(ErrorAttributes errorAttributes, ErrorProperties errorProperties,
			List<ErrorViewResolver> errorViewResolvers) {


		super(errorAttributes, errorViewResolvers);
		Assert.notNull(errorProperties, "ErrorProperties must not be null");
		this.errorProperties = errorProperties;
	}

@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, getErrorAttributeOptions(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<>(status);
		}
		Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
		return new ResponseEntity<>(body, status);
	}

二、ErrorPage 处理流程

当请求出现错误时,错误被抛给 Undertow 时,错误会被 Undertow 拦截到,下面从 Undertow 拦截到错误开始分析

1、 handleFirstRequest();

		try {


            ...
            }
        } catch (Throwable t) {



            servletRequestContext.setRunningInsideHandler(false);
            AsyncContextImpl asyncContextInternal = servletRequestContext.getOriginalRequest().getAsyncContextInternal();
            if(asyncContextInternal != null && asyncContextInternal.isCompletedBeforeInitialRequestDone()) {


                asyncContextInternal.handleCompletedBeforeInitialRequestDone();
            }
            if(asyncContextInternal != null) {


                asyncContextInternal.initialRequestFailed();
            }
            //by default this will just log the exception
            boolean handled = exceptionHandler.handleThrowable(exchange, request, response, t);

            if(handled) {


                exchange.endExchange();
            } else if (request.isAsyncStarted() || request.getDispatcherType() == DispatcherType.ASYNC) {


            	//异步请求
                exchange.unDispatch();
                servletRequestContext.getOriginalRequest().getAsyncContextInternal().handleError(t);
            } else {


                if (!exchange.isResponseStarted()) {


                	//重置 response
                    response.reset();                       //reset the response
                    exchange.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR);
                    //清理了响应头,这里很重要,缺少了响应头,可能会引起一些其他的错误
                    exchange.getResponseHeaders().clear();
                    //获取新的请求错误路径
                    String location = servletContext.getDeployment().getErrorPages().getErrorLocation(t);
                    if (location == null) {


                        location = servletContext.getDeployment().getErrorPages().getErrorLocation(StatusCodes.INTERNAL_SERVER_ERROR);
                    }
                    if (location != null) {


                        RequestDispatcherImpl dispatcher = new RequestDispatcherImpl(location, servletContext);
                        try {


                        	//将请求分发到错误路径上,也就是访问默认异常控制器 BasicErrorController
                            dispatcher.error(servletRequestContext, request, response, servletRequestContext.getOriginalServletPathMatch().getServletChain().getManagedServlet().getServletInfo().getName(), t);
                        } catch (Exception e) {


                            UndertowLogger.REQUEST_LOGGER.exceptionGeneratingErrorPage(e, location);
                        }
                    } else {


                        if (servletRequestContext.displayStackTraces()) {


                            ServletDebugPageHandler.handleRequest(exchange, servletRequestContext, t);
                        } else {


                            servletRequestContext.getOriginalResponse().doErrorDispatch(StatusCodes.INTERNAL_SERVER_ERROR, StatusCodes.INTERNAL_SERVER_ERROR_STRING);
                        }
                    }
                }
            }

        } finally {


            ...
        }

2、 getErrorLocation();

优先根据异常类型查找路径,不存在时再根据状态码查找路径,都不存在时使用默认路径

public String getErrorLocation(final Throwable exception) {


        if (exception == null) {


            return null;
        }
        //todo: this is kinda slow, but there is probably not a great deal that can be done about it
        String location = null;
        for (Class c = exception.getClass(); c != null && location == null; c = c.getSuperclass()) {


            location = exceptionMappings.get(c);
        }
        if (location == null && exception instanceof ServletException) {


            Throwable rootCause = ((ServletException) exception).getRootCause();
            //Iterate through any nested JasperException in case it is in JSP development mode
            while (rootCause != null && rootCause instanceof ServletException && location == null) {


                for (Class c = rootCause.getClass(); c != null && location == null; c = c.getSuperclass()) {


                    location = exceptionMappings.get(c);
                }
                rootCause = ((ServletException) rootCause).getRootCause();
            }
            if (rootCause != null && location == null) {


                for (Class c = rootCause.getClass(); c != null && location == null; c = c.getSuperclass()) {


                    location = exceptionMappings.get(c);
                }
            }
        }
        if (location == null) {


            location = getErrorLocation(StatusCodes.INTERNAL_SERVER_ERROR);
        }
        return location;
    }

三、自定义 ErrorPage

1、 自定义 GlobalErrorPageRegistrar;

可以通过状态码或者具体的异常类型对应要访问的路径

@Configuration
@Slf4j
public class GlobalErrorPageRegistrar implements ErrorPageRegistrar {


    @Override
    public void registerErrorPages(ErrorPageRegistry registry) {


        //状态码和路径映射
        registry.addErrorPages(
                new ErrorPage(HttpStatus.BAD_REQUEST, "/400"),
                new ErrorPage(HttpStatus.NOT_FOUND, "/404"),
                new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/500")
        );
        //异常类型和路径映射
        registry.addErrorPages(
                new ErrorPage(IllegalArgumentException.class, "/400"),
                new ErrorPage(HttpTimeoutException.class, "/408")
        );
    }
}

2、 ErrorPageController;

配置错误的请求对应的方法

@RestController
public class ErrorPageController {



    @RequestMapping(value = "/400", produces = {

     MediaType.APPLICATION_JSON_VALUE})
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ResponseEntity to400() {


        return new ResponseEntity(400, "请求有误");
    }

    @RequestMapping(value = "/404", produces = {

     MediaType.APPLICATION_JSON_VALUE})
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public ResponseEntity to404() {


        return new ResponseEntity(404, "找不到资源");
    }

    @RequestMapping(value = "/408", produces = {

     MediaType.APPLICATION_JSON_VALUE})
    @ResponseStatus(HttpStatus.REQUEST_TIMEOUT)
    public ResponseEntity to408() {


        return new ResponseEntity(408, "请求超时");
    }

    @RequestMapping(value = "/500", produces = {

     MediaType.APPLICATION_JSON_VALUE})
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ResponseEntity to500() {


        return new ResponseEntity(500, "服务器错误");
    }

}