Spring Boot优雅地处理404异常问题
背景
在使用SpringBoot的过程中,你肯定遇到过404错误。比如下面的代码:
@RestController @RequestMapping(value="/hello") publicclassHelloWorldController{ @RequestMapping("/test") publicObjectgetObject1(HttpServletRequestrequest){ Responseresponse=newResponse(); response.success("请求成功..."); response.setResponseTime(); returnresponse; } }
当我们使用错误的请求地址(POSThttp://127.0.0.1:8888/hello/test1?id=98)进行请求时,会报下面的错误:
{ "timestamp":"2020-11-19T08:30:48.844+0000", "status":404, "error":"NotFound", "message":"Nomessageavailable", "path":"/hello/test1" }
虽然上面的返回很清楚,但是我们的接口需要返回统一的格式,比如:
{ "rtnCode":"9999", "rtnMsg":"404/hello/test1NotFound" }
这时候你可能会想有Spring的统一异常处理,在Controller类上加@RestControllerAdvice注解。但是这种做法并不能统一处理404错误。
404错误产生的原因
产生404的原因是我们调了一个不存在的接口,但是为什么会返回下面的json报错呢?我们先从Spring的源代码分析下。
{ "timestamp":"2020-11-19T08:30:48.844+0000", "status":404, "error":"NotFound", "message":"Nomessageavailable", "path":"/hello/test1" }
为了代码简单起见,这边直接从DispatcherServlet的doDispatch方法开始分析。(如果不知道为什么要从这边开始,你还要熟悉下SpringMVC的源代码)。
...省略部分代码.... //Actuallyinvokethehandler. mv=ha.handle(processedRequest,response,mappedHandler.getHandler()); ...省略部分代码
SpringMVC会根据请求URL的不同,配置的RequestMapping的不同,为请求匹配不同的HandlerAdapter。
对于上面的请求地址:http://127.0.0.1:8888/hello/test1?id=98匹配到的HandlerAdapter是HttpRequestHandlerAdapter。
我们直接进入到HttpRequestHandlerAdapter中看下这个类的handle方法。
@Override @Nullable publicModelAndViewhandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler) throwsException{ ((HttpRequestHandler)handler).handleRequest(request,response); returnnull; }
这个方法没什么内容,直接是调用了HttpRequestHandler类的handleRequest(request,response)方法。所以直接进入这个方法看下吧。
@Override publicvoidhandleRequest(HttpServletRequestrequest,HttpServletResponseresponse) throwsServletException,IOException{ //Forverygeneralmappings(e.g."/")weneedtocheck404first Resourceresource=getResource(request); if(resource==null){ logger.trace("Nomatchingresourcefound-returning404"); //这个方法很简单,就是设置404响应码,然后将Response的errorState状态从0设置成1 response.sendError(HttpServletResponse.SC_NOT_FOUND); //直接返回 return; } ...省略部分方法 }
这个方法很简单,就是设置404响应码,将Response的errorState状态从0设置成1,然后就返回响应了。整个过程并没有发生任何异常,所以不能触发Spring的全局异常处理机制。
到这边还有一个问题没有解决:就是下面的404提示信息是怎么返回的。
{ "timestamp":"2020-11-19T08:30:48.844+0000", "status":404, "error":"NotFound", "message":"Nomessageavailable", "path":"/hello/test1" }
我们继续往下看。Response响应被返回,进入org.apache.catalina.core.StandardHostValve类的invoke方法进行处理。(不要问我为什么知道是在这里?Debug的能力是需要自己摸索出来的,自己调试多了,你也就会了)
@Override publicfinalvoidinvoke(Requestrequest,Responseresponse) throwsIOException,ServletException{ Contextcontext=request.getContext(); if(context==null){ response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, sm.getString("standardHost.noContext")); return; } if(request.isAsyncSupported()){ request.setAsyncSupported(context.getPipeline().isAsyncSupported()); } booleanasyncAtStart=request.isAsync(); booleanasyncDispatching=request.isAsyncDispatching(); try{ context.bind(Globals.IS_SECURITY_ENABLED,MY_CLASSLOADER); if(!asyncAtStart&&!context.fireRequestInitEvent(request.getRequest())){ return; } try{ if(!asyncAtStart||asyncDispatching){ context.getPipeline().getFirst().invoke(request,response); }else{ if(!response.isErrorReportRequired()){ thrownewIllegalStateException(sm.getString("standardHost.asyncStateError")); } } }catch(Throwablet){ ExceptionUtils.handleThrowable(t); container.getLogger().error("ExceptionProcessing"+request.getRequestURI(),t); if(!response.isErrorReportRequired()){ request.setAttribute(RequestDispatcher.ERROR_EXCEPTION,t); throwable(request,response,t); } } response.setSuspended(false); Throwablet=(Throwable)request.getAttribute(RequestDispatcher.ERROR_EXCEPTION); if(!context.getState().isAvailable()){ return; } //在这里判断请求是不是发生了错误,错误的话就进入StandardHostValve的status(Requestrequest,Responseresponse)方法。 //Lookfor(andrenderiffound)anapplicationlevelerrorpage if(response.isErrorReportRequired()){ if(t!=null){ throwable(request,response,t); }else{ status(request,response); } } if(!request.isAsync()&&!asyncAtStart){ context.fireRequestDestroyEvent(request.getRequest()); } }finally{ //Accessasession(ifpresent)toupdatelastaccessedtime,based //onastrictinterpretationofthespecification if(ACCESS_SESSION){ request.getSession(false); } context.unbind(Globals.IS_SECURITY_ENABLED,MY_CLASSLOADER); } }
这个方法会根据返回的响应判断是不是发生了错了,如果发生了error,则进入StandardHostValve的status(Requestrequest,Responseresponse)方法。这个方法“兜兜转转”又进入了StandardHostValve的custom(Requestrequest,Responseresponse,ErrorPageerrorPage)方法。这个方法中将请求重新forward到了"/error"接口。
privatebooleancustom(Requestrequest,Responseresponse, ErrorPageerrorPage){ if(container.getLogger().isDebugEnabled()){ container.getLogger().debug("Processing"+errorPage); } try{ //Forwardcontroltothespecifiedlocation ServletContextservletContext= request.getContext().getServletContext(); RequestDispatcherrd= servletContext.getRequestDispatcher(errorPage.getLocation()); if(rd==null){ container.getLogger().error( sm.getString("standardHostValue.customStatusFailed",errorPage.getLocation())); returnfalse; } if(response.isCommitted()){ rd.include(request.getRequest(),response.getResponse()); }else{ //Resettheresponse(keepingtherealerrorcodeandmessage) response.resetBuffer(true); response.setContentLength(-1); //1:重新forward请求到/error接口 rd.forward(request.getRequest(),response.getResponse()); response.setSuspended(false); } returntrue; }catch(Throwablet){ ExceptionUtils.handleThrowable(t); container.getLogger().error("ExceptionProcessing"+errorPage,t); returnfalse; } }
上面标号1处的代码重新将请求forward到了/error接口。所以如果我们开着Debug日志的话,你会在后台看到下面的日志。
[http-nio-8888-exec-7]DEBUGorg.springframework.web.servlet.DispatcherServlet:891-DispatcherServletwithname'dispatcherServlet'processingPOSTrequestfor[/error]
2020-11-1919:04:04.280[http-nio-8888-exec-7]DEBUGorg.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping:313-Lookinguphandlermethodforpath/error
2020-11-1919:04:04.281[http-nio-8888-exec-7]DEBUGorg.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping:320-Returninghandlermethod[publicorg.springframework.http.ResponseEntity>org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.error(javax.servlet.http.HttpServletRequest)]
2020-11-1919:04:04.281[http-nio-8888-exec-7]DEBUGorg.springframework.beans.factory.support.DefaultListableBeanFactory:255-Returningcachedinstanceofsingletonbean'basicErrorController'
上面是/error的请求日志。到这边还是没说明为什么能返回json格式的404返回格式。我们继续往下看。
到这边为止,我们好像没有任何线索了。但是如果仔细看上面日志的话,你会发现这个接口的处理方法是:
org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.error(javax.servlet.http.HttpServletRequest)]
我们打开BasicErrorController这个类的源代码,一切豁然开朗。
@Controller @RequestMapping("${server.error.path:${error.path:/error}}") publicclassBasicErrorControllerextendsAbstractErrorController{ @RequestMapping(produces="text/html") publicModelAndViewerrorHtml(HttpServletRequestrequest, HttpServletResponseresponse){ HttpStatusstatus=getStatus(request); Mapmodel=Collections.unmodifiableMap(getErrorAttributes( request,isIncludeStackTrace(request,MediaType.TEXT_HTML))); response.setStatus(status.value()); ModelAndViewmodelAndView=resolveErrorView(request,response,status,model); return(modelAndView==null?newModelAndView("error",model):modelAndView); } @RequestMapping @ResponseBody publicResponseEntity
BasicErrorController是Spring默认配置的一个Controller,默认处理/error请求。BasicErrorController提供两种返回错误一种是页面返回、当你是页面请求的时候就会返回页面,另外一种是json请求的时候就会返回json错误。
自定义404错误处理类
我们先看下BasicErrorController是在哪里进行配置的。
在IDEA中,查看BasicErrorController的usage,我们发现这个类是在ErrorMvcAutoConfiguration中自动配置的。
@Configuration @ConditionalOnWebApplication(type=Type.SERVLET) @ConditionalOnClass({Servlet.class,DispatcherServlet.class}) //LoadbeforethemainWebMvcAutoConfigurationsothattheerrorViewisavailable @AutoConfigureBefore(WebMvcAutoConfiguration.class) @EnableConfigurationProperties({ServerProperties.class,ResourceProperties.class}) publicclassErrorMvcAutoConfiguration{ @Bean @ConditionalOnMissingBean(value=ErrorController.class,search=SearchStrategy.CURRENT) publicBasicErrorControllerbasicErrorController(ErrorAttributeserrorAttributes){ returnnewBasicErrorController(errorAttributes,this.serverProperties.getError(), this.errorViewResolvers); } ...省略部分代码 }
从上面的配置中可以看出来,只要我们自己配置一个ErrorController,就可以覆盖掉BasicErrorController的行为。
@Controller @RequestMapping("${server.error.path:${error.path:/error}}") publicclassCustomErrorControllerextendsBasicErrorController{ @Value("${server.error.path:${error.path:/error}}") privateStringpath; publicCustomErrorController(ServerPropertiesserverProperties){ super(newDefaultErrorAttributes(),serverProperties.getError()); } /** *覆盖默认的JSON响应 */ @Override publicResponseEntity>error(HttpServletRequestrequest){ HttpStatusstatus=getStatus(request); Map map=newHashMap (16); Map originalMsgMap=getErrorAttributes(request,isIncludeStackTrace(request,MediaType.ALL)); Stringpath=(String)originalMsgMap.get("path"); Stringerror=(String)originalMsgMap.get("error"); Stringmessage=(String)originalMsgMap.get("message"); StringJoinerjoiner=newStringJoiner(",","[","]"); joiner.add(path).add(error).add(message); map.put("rtnCode","9999"); map.put("rtnMsg",joiner.toString()); returnnewResponseEntity >(map,status); } /** *覆盖默认的HTML响应 */ @Override publicModelAndViewerrorHtml(HttpServletRequestrequest,HttpServletResponseresponse){ //请求的状态 HttpStatusstatus=getStatus(request); response.setStatus(getStatus(request).value()); Map model=getErrorAttributes(request, isIncludeStackTrace(request,MediaType.TEXT_HTML)); ModelAndViewmodelAndView=resolveErrorView(request,response,status,model); //指定自定义的视图 return(modelAndView==null?newModelAndView("error",model):modelAndView); } }
默认的错误路径是/error,我们可以通过以下配置进行覆盖:
server: error: path:/xxx
更详细的内容请参考SpringBoot的
简单总结#
- 如果在过滤器(Filter)中发生异常,或者调用的接口不存在,Spring会直接将Response的errorStatus状态设置成1,将http响应码设置为500或者404,Tomcat检测到errorStatus为1时,会将请求重现forward到/error接口;
- 如果请求已经进入了Controller的处理方法,这时发生了异常,如果没有配置Spring的全局异常机制,那么请求还是会被forward到/error接口,如果配置了全局异常处理,Controller中的异常会被捕获;
- 继承BasicErrorController就可以覆盖原有的错误处理方式。
到此这篇关于SpringBoot优雅地处理404异常的文章就介绍到这了,更多相关SpringBoot404异常内容请搜索毛票票以前的文章或继续浏览下面的相关文章希望大家以后多多支持毛票票!