1. Spring Boot優(yōu)雅地處理404異常

        共 12267字,需瀏覽 25分鐘

         ·

        2020-11-22 14:48

        點(diǎn)擊上方藍(lán)色字體,選擇“標(biāo)星公眾號(hào)”

        優(yōu)質(zhì)文章,第一時(shí)間送達(dá)

        ? 作者?|??程序員自由之路

        來(lái)源 |? urlify.cn/Mv2Avq

        66套java從入門(mén)到精通實(shí)戰(zhàn)課程分享

        背景

        在使用SpringBoot的過(guò)程中,你肯定遇到過(guò)404錯(cuò)誤。比如下面的代碼:

        @RestController
        @RequestMapping(value?=?"/hello")
        public?class?HelloWorldController?{
        ????@RequestMapping("/test")
        ????public?Object?getObject1(HttpServletRequest?request){
        ????????Response?response?=?new?Response();
        ????????response.success("請(qǐng)求成功...");
        ????????response.setResponseTime();
        ????????return?response;
        ????}
        }

        當(dāng)我們使用錯(cuò)誤的請(qǐng)求地址(POST http://127.0.0.1:8888/hello/test1?id=98)進(jìn)行請(qǐng)求時(shí),會(huì)報(bào)下面的錯(cuò)誤:

        {
        ??"timestamp":?"2020-11-19T08:30:48.844+0000",
        ??"status":?404,
        ??"error":?"Not?Found",
        ??"message":?"No?message?available",
        ??"path":?"/hello/test1"
        }

        雖然上面的返回很清楚,但是我們的接口需要返回統(tǒng)一的格式,比如:

        {
        ????"rtnCode":"9999",
        ????"rtnMsg":"404?/hello/test1?Not?Found"
        }

        這時(shí)候你可能會(huì)想有Spring的統(tǒng)一異常處理,在Controller類(lèi)上加@RestControllerAdvice注解。但是這種做法并不能統(tǒng)一處理404錯(cuò)誤。

        404錯(cuò)誤產(chǎn)生的原因

        產(chǎn)生404的原因是我們調(diào)了一個(gè)不存在的接口,但是為什么會(huì)返回下面的json報(bào)錯(cuò)呢?我們先從Spring的源代碼分析下。

        {
        ??"timestamp":?"2020-11-19T08:30:48.844+0000",
        ??"status":?404,
        ??"error":?"Not?Found",
        ??"message":?"No?message?available",
        ??"path":?"/hello/test1"
        }

        為了代碼簡(jiǎn)單起見(jiàn),這邊直接從DispatcherServlet的doDispatch方法開(kāi)始分析。(如果不知道為什么要從這邊開(kāi)始,你還要熟悉下SpringMVC的源代碼)。

        ...?省略部分代碼....
        //?Actually?invoke?the?handler.
        mv?=?ha.handle(processedRequest,?response,?mappedHandler.getHandler());
        ...?省略部分代碼

        Spring MVC會(huì)根據(jù)請(qǐng)求URL的不同,配置的RequestMapping的不同,為請(qǐng)求匹配不同的HandlerAdapter。

        對(duì)于上面的請(qǐng)求地址:http://127.0.0.1:8888/hello/test1?id=98匹配到的HandlerAdapter是HttpRequestHandlerAdapter。

        我們直接進(jìn)入到HttpRequestHandlerAdapter中看下這個(gè)類(lèi)的handle方法。

        @Override
        @Nullable
        public?ModelAndView?handle(HttpServletRequest?request,?HttpServletResponse?response,?Object?handler)
        ????throws?Exception?{
        ????((HttpRequestHandler)?handler).handleRequest(request,?response);
        ????return?null;
        }

        這個(gè)方法沒(méi)什么內(nèi)容,直接是調(diào)用了HttpRequestHandler類(lèi)的handleRequest(request, response)方法。所以直接進(jìn)入這個(gè)方法看下吧。

        @Override
        public?void?handleRequest(HttpServletRequest?request,?HttpServletResponse?response)
        ????throws?ServletException,?IOException?{

        ????//?For?very?general?mappings?(e.g.?"/")?we?need?to?check?404?first
        ????Resource?resource?=?getResource(request);
        ????if?(resource?==?null)?{
        ????????logger.trace("No?matching?resource?found?-?returning?404");
        ????????//?這個(gè)方法很簡(jiǎn)單,就是設(shè)置404響應(yīng)碼,然后將Response的errorState狀態(tài)從0設(shè)置成1
        ????????response.sendError(HttpServletResponse.SC_NOT_FOUND);
        ????????//?直接返回
        ????????return;
        ????}
        ????...?省略部分方法
        }

        這個(gè)方法很簡(jiǎn)單,就是設(shè)置404響應(yīng)碼,將Response的errorState狀態(tài)從0設(shè)置成1,然后就返回響應(yīng)了。整個(gè)過(guò)程并沒(méi)有發(fā)生任何異常,所以不能觸發(fā)Spring的全局異常處理機(jī)制

        到這邊還有一個(gè)問(wèn)題沒(méi)有解決:就是下面的404提示信息是怎么返回的。

        {
        ??"timestamp":?"2020-11-19T08:30:48.844+0000",
        ??"status":?404,
        ??"error":?"Not?Found",
        ??"message":?"No?message?available",
        ??"path":?"/hello/test1"
        }

        我們繼續(xù)往下看。Response響應(yīng)被返回,進(jìn)入org.apache.catalina.core.StandardHostValve類(lèi)的invoke方法進(jìn)行處理。(不要問(wèn)我為什么知道是在這里?Debug的能力是需要自己摸索出來(lái)的,自己調(diào)試多了,你也就會(huì)了)

        @Override
        public?final?void?invoke(Request?request,?Response?response)
        ????throws?IOException,?ServletException?{
        ????
        ????Context?context?=?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());
        ????}

        ????boolean?asyncAtStart?=?request.isAsync();
        ????boolean?asyncDispatching?=?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())?{
        ????????????????????throw?new?IllegalStateException(sm.getString("standardHost.asyncStateError"));
        ????????????????}
        ????????????}
        ????????}?catch?(Throwable?t)?{
        ????????????ExceptionUtils.handleThrowable(t);
        ????????????container.getLogger().error("Exception?Processing?"?+?request.getRequestURI(),?t);
        ????????????if?(!response.isErrorReportRequired())?{
        ????????????????request.setAttribute(RequestDispatcher.ERROR_EXCEPTION,?t);
        ????????????????throwable(request,?response,?t);
        ????????????}
        ????????}
        ????????response.setSuspended(false);

        ????????Throwable?t?=?(Throwable)?request.getAttribute(RequestDispatcher.ERROR_EXCEPTION);
        ????????if?(!context.getState().isAvailable())?{
        ????????????return;
        ????????}
        ????????//?在這里判斷請(qǐng)求是不是發(fā)生了錯(cuò)誤,錯(cuò)誤的話就進(jìn)入StandardHostValve的status(Request request, Response response)方法。
        ????????//?Look?for?(and?render?if?found)?an?application?level?error?page
        ????????if?(response.isErrorReportRequired())?{
        ????????????if?(t?!=?null)?{
        ????????????????throwable(request,?response,?t);
        ????????????}?else?{
        ????????????????status(request,?response);
        ????????????}
        ????????}

        ????????if?(!request.isAsync()?&&?!asyncAtStart)?{
        ????????????context.fireRequestDestroyEvent(request.getRequest());
        ????????}
        ????}?finally?{
        ????????//?Access?a?session?(if?present)?to?update?last?accessed?time,?based
        ????????//?on?a?strict?interpretation?of?the?specification
        ????????if?(ACCESS_SESSION)?{
        ????????????request.getSession(false);
        ????????}
        ????????context.unbind(Globals.IS_SECURITY_ENABLED,?MY_CLASSLOADER);
        ????}
        ??}

        這個(gè)方法會(huì)根據(jù)返回的響應(yīng)判斷是不是發(fā)生了錯(cuò)了,如果發(fā)生了error,則進(jìn)入StandardHostValve的status(Request request, Response response)方法。這個(gè)方法“兜兜轉(zhuǎn)轉(zhuǎn)”又進(jìn)入了StandardHostValve的custom(Request request, Response response,ErrorPage errorPage)方法。這個(gè)方法中將請(qǐng)求重新forward到了"/error"接口。

        ?private?boolean?custom(Request?request,?Response?response,
        ?????????????????????????????ErrorPage?errorPage)?{

        ????????if?(container.getLogger().isDebugEnabled())?{
        ????????????container.getLogger().debug("Processing?"?+?errorPage);
        ????????}
        ????????try?{
        ????????????//?Forward?control?to?the?specified?location
        ????????????ServletContext?servletContext?=
        ????????????????request.getContext().getServletContext();
        ????????????RequestDispatcher?rd?=
        ????????????????servletContext.getRequestDispatcher(errorPage.getLocation());
        ????????????if?(rd?==?null)?{
        ????????????????container.getLogger().error(
        ????????????????????sm.getString("standardHostValue.customStatusFailed",?errorPage.getLocation()));
        ????????????????return?false;
        ????????????}
        ????????????if?(response.isCommitted())?{
        ????????????????rd.include(request.getRequest(),?response.getResponse());
        ????????????}?else?{
        ????????????????//?Reset?the?response?(keeping?the?real?error?code?and?message)
        ????????????????response.resetBuffer(true);
        ????????????????response.setContentLength(-1);
        ????????????????//?1:?重新forward請(qǐng)求到/error接口
        ????????????????rd.forward(request.getRequest(),?response.getResponse());
        ????????????????response.setSuspended(false);
        ????????????}
        ????????????return?true;
        ????????}?catch?(Throwable?t)?{
        ????????????ExceptionUtils.handleThrowable(t);
        ????????????container.getLogger().error("Exception?Processing?"?+?errorPage,?t);
        ????????????return?false;
        ????????}
        ????}

        上面標(biāo)號(hào)1處的代碼重新將請(qǐng)求forward到了/error接口。所以如果我們開(kāi)著Debug日志的話,你會(huì)在后臺(tái)看到下面的日志。

        [http-nio-8888-exec-7]?DEBUG?org.springframework.web.servlet.DispatcherServlet:891?-?DispatcherServlet?with?name?'dispatcherServlet'?processing?POST?request?for?[/error]
        2020-11-19?19:04:04.280?[http-nio-8888-exec-7]?DEBUG?org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping:313?-?Looking?up?handler?method?for?path?/error
        2020-11-19?19:04:04.281?[http-nio-8888-exec-7]?DEBUG?org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping:320?-?Returning?handler?method?[public?org.springframework.http.ResponseEntity>?org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.error(javax.servlet.http.HttpServletRequest)]
        2020-11-19?19:04:04.281?[http-nio-8888-exec-7]?DEBUG?org.springframework.beans.factory.support.DefaultListableBeanFactory:255?-?Returning?cached?instance?of?singleton?bean?'basicErrorController'

        上面是/error的請(qǐng)求日志。到這邊還是沒(méi)說(shuō)明為什么能返回json格式的404返回格式。我們繼續(xù)往下看。

        到這邊為止,我們好像沒(méi)有任何線索了。但是如果仔細(xì)看上面日志的話,你會(huì)發(fā)現(xiàn)這個(gè)接口的處理方法是:

        org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.error(javax.servlet.http.HttpServletRequest)]

        我們打開(kāi)BasicErrorController這個(gè)類(lèi)的源代碼,一切豁然開(kāi)朗。

        @Controller
        @RequestMapping("${server.error.path:${error.path:/error}}")
        public?class?BasicErrorController?extends?AbstractErrorController?{
        ????@RequestMapping(produces?=?"text/html")
        ????public?ModelAndView?errorHtml(HttpServletRequest?request,
        ????????????HttpServletResponse?response)?{
        ????????HttpStatus?status?=?getStatus(request);
        ????????Map?model?=?Collections.unmodifiableMap(getErrorAttributes(
        ????????????????request,?isIncludeStackTrace(request,?MediaType.TEXT_HTML)));
        ????????response.setStatus(status.value());
        ????????ModelAndView?modelAndView?=?resolveErrorView(request,?response,?status,?model);
        ????????return?(modelAndView?==?null???new?ModelAndView("error",?model)?:?modelAndView);
        ????}

        ????@RequestMapping
        ????@ResponseBody
        ????public?ResponseEntity>?error(HttpServletRequest?request)?{
        ????????Map?body?=?getErrorAttributes(request,
        ????????????????isIncludeStackTrace(request,?MediaType.ALL));
        ????????HttpStatus?status?=?getStatus(request);
        ????????return?new?ResponseEntity>(body,?status);
        ????}
        ????...?省略部分方法
        }

        BasicErrorController是Spring默認(rèn)配置的一個(gè)Controller,默認(rèn)處理/error請(qǐng)求。BasicErrorController提供兩種返回錯(cuò)誤一種是頁(yè)面返回、當(dāng)你是頁(yè)面請(qǐng)求的時(shí)候就會(huì)返回頁(yè)面,另外一種是json請(qǐng)求的時(shí)候就會(huì)返回json錯(cuò)誤。

        自定義404錯(cuò)誤處理類(lèi)

        我們先看下BasicErrorController是在哪里進(jìn)行配置的。

        在IDEA中,查看BasicErrorController的usage,我們發(fā)現(xiàn)這個(gè)類(lèi)是在ErrorMvcAutoConfiguration中自動(dòng)配置的。

        @Configuration
        @ConditionalOnWebApplication(type?=?Type.SERVLET)
        @ConditionalOnClass({?Servlet.class,?DispatcherServlet.class?})
        //?Load?before?the?main?WebMvcAutoConfiguration?so?that?the?error?View?is?available
        @AutoConfigureBefore(WebMvcAutoConfiguration.class)
        @EnableConfigurationProperties({?ServerProperties.class,?ResourceProperties.class?})
        public?class?ErrorMvcAutoConfiguration?{
        ????
        ????@Bean
        ?@ConditionalOnMissingBean(value?=?ErrorController.class,?search?=?SearchStrategy.CURRENT)
        ?public?BasicErrorController?basicErrorController(ErrorAttributes?errorAttributes)?{
        ??return?new?BasicErrorController(errorAttributes,?this.serverProperties.getError(),
        ????this.errorViewResolvers);
        ?}
        ?...?省略部分代碼
        }

        從上面的配置中可以看出來(lái),只要我們自己配置一個(gè)ErrorController,就可以覆蓋掉BasicErrorController的行為。

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

        ????@Value("${server.error.path:${error.path:/error}}")
        ????private?String?path;

        ????public?CustomErrorController(ServerProperties?serverProperties)?{
        ????????super(new?DefaultErrorAttributes(),?serverProperties.getError());
        ????}

        ????/**
        ?????*?覆蓋默認(rèn)的JSON響應(yīng)
        ?????*/
        ????@Override
        ????public?ResponseEntity>?error(HttpServletRequest?request)?{

        ????????HttpStatus?status?=?getStatus(request);
        ????????Map?map?=?new?HashMap(16);
        ????????Map?originalMsgMap?=?getErrorAttributes(request,?isIncludeStackTrace(request,?MediaType.ALL));
        ????????String?path?=?(String)originalMsgMap.get("path");
        ????????String?error?=?(String)originalMsgMap.get("error");
        ????????String?message?=?(String)originalMsgMap.get("message");
        ????????StringJoiner?joiner?=?new?StringJoiner(",","[","]");
        ????????joiner.add(path).add(error).add(message);
        ????????map.put("rtnCode",?"9999");
        ????????map.put("rtnMsg",?joiner.toString());
        ????????return?new?ResponseEntity>(map,?status);
        ????}

        ????/**
        ?????*?覆蓋默認(rèn)的HTML響應(yīng)
        ?????*/
        ????@Override
        ????public?ModelAndView?errorHtml(HttpServletRequest?request,?HttpServletResponse?response)?{
        ????????//請(qǐng)求的狀態(tài)
        ????????HttpStatus?status?=?getStatus(request);
        ????????response.setStatus(getStatus(request).value());
        ????????Map?model?=?getErrorAttributes(request,
        ????????????????isIncludeStackTrace(request,?MediaType.TEXT_HTML));
        ????????ModelAndView?modelAndView?=?resolveErrorView(request,?response,?status,?model);
        ????????//指定自定義的視圖
        ????????return?(modelAndView?==?null???new?ModelAndView("error",?model)?:?modelAndView);
        ????}
        }

        默認(rèn)的錯(cuò)誤路徑是/error,我們可以通過(guò)以下配置進(jìn)行覆蓋:

        server:
        ??error:
        ????path:?/xxx

        更詳細(xì)的內(nèi)容請(qǐng)參考Spring Boot的章節(jié)。

        簡(jiǎn)單總結(jié)

        • 如果在過(guò)濾器(Filter)中發(fā)生異常,或者調(diào)用的接口不存在,Spring會(huì)直接將Response的errorStatus狀態(tài)設(shè)置成1,將http響應(yīng)碼設(shè)置為500或者404,Tomcat檢測(cè)到errorStatus為1時(shí),會(huì)將請(qǐng)求重現(xiàn)forward到/error接口;

        • 如果請(qǐng)求已經(jīng)進(jìn)入了Controller的處理方法,這時(shí)發(fā)生了異常,如果沒(méi)有配置Spring的全局異常機(jī)制,那么請(qǐng)求還是會(huì)被forward到/error接口,如果配置了全局異常處理,Controller中的異常會(huì)被捕獲;

        • 繼承BasicErrorController就可以覆蓋原有的錯(cuò)誤處理方式。






        粉絲福利:實(shí)戰(zhàn)springboot+CAS單點(diǎn)登錄系統(tǒng)視頻教程免費(fèi)領(lǐng)取

        ???

        ?長(zhǎng)按上方微信二維碼?2 秒
        即可獲取資料



        感謝點(diǎn)贊支持下哈?

        瀏覽 34
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
          
          

            1. 日韩黄片在线 | 99999国产精品 | 黄色小说扒灰 | 大学生交换做爰3在线观看 | 在线亚洲人成电影网站色www |