1.问题描述
Forwarding to error page from request [/aaa/bbb/ccc/bookhistory/] due to exception [账号未登录] com.xxx.web.base.exception.BusinessException: 账号未登录
at com.xxx.web.core.interceptor.LoginInterceptor.preHandle(LoginInterceptor.java:20)
at org.springframework.web.servlet.HandlerExecutionChain.applyPreHandle(HandlerExecutionChain.java:136)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:986)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:925)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:974)
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:866)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:622)
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:851)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:729)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:291)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at org.apache.logging.log4j.web.Log4jServletFilter.doFilter(Log4jServletFilter.java:71)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at com.sankuai.oceanus.http.filter.InfFilter.doFilter(InfFilter.java:94)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at org.springframework.web.filter.HttpPutFormContentFilter.doFilterInternal(HttpPutFormContentFilter.java:109)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:93)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at org.springframework.boot.web.servlet.support.ErrorPageFilter.doFilter(ErrorPageFilter.java:130)
at org.springframework.boot.web.servlet.support.ErrorPageFilter.access$000(ErrorPageFilter.java:66)
at org.springframework.boot.web.servlet.support.ErrorPageFilter$1.doFilterInternal(ErrorPageFilter.java:105)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.springframework.boot.web.servlet.support.ErrorPageFilter.doFilter(ErrorPageFilter.java:123)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:200)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:212)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:106)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:502)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:141)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:79)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:88)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:521)
at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1096)
at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:674)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1500)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.run(NioEndpoint.java:1456)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:745)
对登录校验,做了一个拦截器,统一校验,对于未登录用户抛出自定义异常,然后使用Spring的统一异常处理统一处理,然而还是报错了。
##2.具体问题
正常来说,哪怕用户未登录,也不是抛出异常,而是友好提示,如:项目中拦截器抛出的未登录异常,正常来说是会被程序中的异常统一处理器,统一处理:
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(value = BusinessException.class)
@ResponseBody
public RespNVO<String> businessExceptionHandle(HttpServletRequest request,BusinessException e){
return RespNVO.error(e.getCode(),e.getMessage());
}
//...略过部分代码
}
为什么这个异常未被统一处理器处理掉?原因在于请求路径有问题:这个请求是非法请求URL,例如“/aaa/bbb/ccc/bookhistory/”,项目中并没有对应的请求处理方法,我们来看Spring对于这种请求抛出的异常是如何处理的:
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// 获取当前请求路径的处理handler:mappedHandler,包括具体处理该请求的handler以及对该请求其作用的相关拦截器
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
// 获取具体处理请求的适配器
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// Process last-modified header, if supported by the handler.
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (logger.isDebugEnabled()) {
logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified);
}
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}
//处理拦截器前置请求,本次在这里会抛出异常,由下方代码catch住
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// 处理请求
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
applyDefaultViewName(processedRequest, mv);
//拦截器后置处理
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
//异常缓存
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
//不管是否有异常,均需要执行processDispatchResult
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
// Clean up any resources used by a multipart request.
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}
1.如果处理请求中未抛出异常,则走的是非法请求路径的处理方法:
非法请求路径的时候,mappedHandler的值:
HandlerExecutionChain with handler [ResourceHttpRequestHandler [locations=[class path resource [META-INF/resources/], class path resource [resources/], class path resource [static/], class path resource [public/], ServletContext resource [/]], resolvers=[org.springframework.web.servlet.resource.PathResourceResolver@433ab5fd]]] and 3 interceptors
其中ResourceHttpRequestHandler是Spring对http请求静态资源的一种描述(六个资源路径:META-INF/resources/、resources/、static/、public/、/,以及默认的[]),由PathResourceResolver负责寻找对应路径资源。
如果资源未找到,则跳到springboot默认错误页面(如果未配置对应的错误跳转页面的话):404:Whitelabel Error Page(具体见ErrorMvcAutoConfiguration)
对于正常请求,返回的mappedHandler的具体值如:
HandlerExecutionChain with handler [com.vo.RespNVO<com.vo.CoachListVO> com.controller.CoachReadController.fetchCoachList(java.lang.Integer,java.lang.Integer)] and 4 interceptors
其handler类型为HandlerMethod,其值包含相应的处理方法相关信息。
handler的类型对之后的异常处理流程是有影响的,详见后续分析。
2.请求中抛出异常:看代码知道,有异常的情况,异常会先被暂存在dispatchException,并传到processDispatchResult方法中进行处理:
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
@Nullable Exception exception) throws Exception {
boolean errorView = false;
if (exception != null) {
if (exception instanceof ModelAndViewDefiningException) {
logger.debug("ModelAndViewDefiningException encountered", exception);
mv = ((ModelAndViewDefiningException) exception).getModelAndView();
}
else {
Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
//异常处理流程
mv = processHandlerException(request, response, handler, exception);
errorView = (mv != null);
}
}
// Did the handler return a view to render?
if (mv != null && !mv.wasCleared()) {
render(mv, request, response);
if (errorView) {
WebUtils.clearErrorRequestAttributes(request);
}
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Null ModelAndView returned to DispatcherServlet with name '" + getServletName() +
"': assuming HandlerAdapter completed request handling");
}
}
if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
// Concurrent handling started during a forward
return;
}
if (mappedHandler != null) {
mappedHandler.triggerAfterCompletion(request, response, null);
}
}
processDispatchResult逻辑:有异常,进行异常处理逻辑:ModelAndViewDefiningException与其他异常处理。这里此次请求即会进入processHandlerException方法内,进行异常处理流程:
protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,
@Nullable Object handler, Exception ex) throws Exception {
// Check registered HandlerExceptionResolvers...
ModelAndView exMv = null;
if (this.handlerExceptionResolvers != null) {
for (HandlerExceptionResolver handlerExceptionResolver : this.handlerExceptionResolvers) {
exMv = handlerExceptionResolver.resolveException(request, response, handler, ex);
if (exMv != null) {
break;
}
}
}
if (exMv != null) {
if (exMv.isEmpty()) {
request.setAttribute(EXCEPTION_ATTRIBUTE, ex);
return null;
}
// We might still need view name translation for a plain error model...
if (!exMv.hasView()) {
String defaultViewName = getDefaultViewName(request);
if (defaultViewName != null) {
exMv.setViewName(defaultViewName);
}
}
if (logger.isDebugEnabled()) {
logger.debug("Handler execution resulted in exception - forwarding to resolved error view: " + exMv, ex);
}
WebUtils.exposeErrorRequestAttributes(request, ex, getServletName());
return exMv;
}
throw ex;
}
具体异常处理,由handlerExceptionResolvers(List),即一系列的异常处理器处理,内置的有:DefaultErrorAttributes(缓存错误信息,供后面错误视图使用)以及HandlerExceptionResolverComposite(其内同样包含一个List,是一种有序的异常处理器)。
对于HandlerExceptionResolverComposite,其内置的异常处理器有如下三个:
第一个,ExceptionHandlerExceptionResolver,即上面提到的统一处理器入口。
第二个,ResponseStatusExceptionResolver,需要配合ResponseStatus使用,故本次情况可以忽略。
第三个,DefaultHandlerExceptionResolver,默认的异常处理器,是Spring提供的对默认一些异常的处理方法,比如常见的TypeMismatchException、MethodArgumentNotValidException、BindException等处理。
不管是第一个还是第三个异常处理器,其具体处理流程由AbstractHandlerExceptionResolver(是二者的抽象实现父类)实现:
public ModelAndView resolveException(
HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
if (shouldApplyTo(request, handler)) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Resolving exception from handler [" + handler + "]: " + ex);
}
prepareResponse(ex, response);
ModelAndView result = doResolveException(request, response, handler, ex);
if (result != null) {
logException(ex, request);
}
return result;
}
else {
return null;
}
}
这里的重点是shouldApplyTo以及对应的doResolveException(具体实现由实现类实现)方法,shouldApplyTo这个方法是对以上内置的三个异常处理器是否可以处理本次请求异常的判定,具体判定,内置实现有两种:AbstractHandlerExceptionResolver以及其实现抽象类AbstractHandlerMethodExceptionResolver,其中上面说到的ExceptionHandlerExceptionResolver的直接抽象类为AbstractHandlerMethodExceptionResolver,而第三个DefaultHandlerExceptionResolver直接抽象类是AbstractHandlerExceptionResolver(具体类图见最后附录):
//AbstractHandlerExceptionResolver
protected boolean shouldApplyTo(HttpServletRequest request, @Nullable Object handler) {
//请求处理的handler是否为空
if (handler != null) {
//是否有手动设置的对应处异常理器
if (this.mappedHandlers != null && this.mappedHandlers.contains(handler)) {
return true;
}
//是否有手动异常处理类
if (this.mappedHandlerClasses != null) {
for (Class<?> handlerClass : this.mappedHandlerClasses) {
if (handlerClass.isInstance(handler)) {
return true;
}
}
}
}
// 默认二者均为空,即所有handler均可进入异常处理
return (this.mappedHandlers == null && this.mappedHandlerClasses == null);
}
//AbstractHandlerMethodExceptionResolver
protected boolean shouldApplyTo(HttpServletRequest request, @Nullable Object handler) {
if (handler == null) {
//为空,走默认判定流程
return super.shouldApplyTo(request, null);
}
else if (handler instanceof HandlerMethod) {
//如果是HandlerMethod类型
HandlerMethod handlerMethod = (HandlerMethod) handler;
handler = handlerMethod.getBean();
return super.shouldApplyTo(request, handler);
}
else {
return false;
}
}
AbstractHandlerExceptionResolver的shouldApplyTo方法,默认是所有handler发生异常时均可进入异常处理流程的。
对于AbstractHandlerMethodExceptionResolver来说,若handler类型是HandlerMethod,会将请求处理类的bean当做handler放入异常处理判定中。
对于本次请求,handler类型为ResourceHttpRequestHandler,第一个内置异常处理器ExceptionHandlerExceptionResolver,shouldApplyTo返回false(所以,非法请求的异常是不会被统一异常处理器处理的),第三个返回true,即进入默认的异常处理器,而默认处理器中并没有处理本次自定义异常类型(BusinessException),故最终流程会走到processHandlerException方法的最后:重新抛出该异常。异常被再次抛出后将无后续异常处理方法,故前端收到的是后端抛出的异常代码。
3.结论
对于非法请求,自定义的统一异常处理器无法处理其内发生的异常。
附录: