之前和朋友聊过SpringMVC的初始化过程。相信大家对SpringMVC的初始化流程都有一个基本的了解。今天我们就来看看它在请求到来时的执行情况。过程是怎样的?当然这个过程比较长,宋哥可能分两篇分享给大家。很多朋友都知道SpringMVC的核心是DispatcherServlet,而DispatcherServlet的父类是FrameworkServlet,那么我们先来看看FrameworkServlet,这对我们理解DispatcherServlet会有帮助。1.FrameworkServletFrameworkServlet继承自HttpServletBean,而HttpServletBean继承自HttpServlet,HttpServlet是JavaEE里的东西,这里不讨论,它是HttpServletBean的一个framework的东西,但是HttpServletBean比较特殊,特殊是因为它不执行任何Requestprocessing只涉及一些初始化操作,比较简单,我们在上一篇文章中已经分析过了,所以这里不分析HttpServletBean,直接从它的子类FrameworkServlet入手。与所有Servlet一样,FrameworkServlet对请求的处理从服务方法开始。我们来看看FrameworkServlet#service的方法:@Overrideprotectedvoidservice(HttpServletRequestrequest,HttpServletResponseresponse)throwsServletException,IOException{HttpMethodrethodhttpMethod=HttpMethod(HttpMethodrethodhttpMethod=HttpMethod));if(httpMethod==HttpMethod=HttpMethod){HttpMethodrethodhttpMethod=HttpMethod)request,response);}else{super.service(request,response);}}可以看到在这个方法中,首先get到当前请求方法,然后额外处理patch请求,其他所有类型的请求由super.service处理。但是在HttpServlet中,并没有对doGet、doPost等请求进行实质性处理,所以FrameworkServlet也重写了各种请求对应的方法,如doDelete、doGet、doOptions、doPost、doPut、doTrace等。其实除了doHead所有其他方法都被覆盖。我们先来看看doDelete、doGet、doPost以及doPut四个方法:@OverrideprotectedfinalvoiddoGet(HttpServletRequestrequest,HttpServletResponseresponse)throwsServletException,IOException{processRequest(request,response);}@OverrideprotectedfinalvoiddoPost(HttpServletRequestrequest,HttpServletResponseresponse)throwsServletException,IOException{processRequest(request,response);}@OverrideprotectedfinalvoiddoPut(HttpServletRequestrequest,HttpServletResponseresponse)throwsServletException,IOException{processRequest(request,response);}@OverrideprotectedfinalvoiddoDelete(HttpServletRequestrequest,HttpServletResponseresponse)throwsServletException,IOException{processRequest(request,response);}可以看到,这里又把请求交给processRequest处理。在processRequest方法中,还会进一步调用doService,对不同类型的请求进行分类处理。doOptions和doTrace则稍有一些不同,如下:@OverrideprotectedvoiddoOptions(HttpServletRequestrequest,HttpServletResponseresponse)throwsServletException,IOException{if(this.dispatchOptionsRequest||CorsUtils.isPreFlightRequest(request)){processRequest(request,response.con);if(response允许”)){return;}}super.doOptions(request,newHttpServletResponseWrapper(response){@OverridepublicvoidsetHeader(Stringname,Stringvalue){if(“Allow”.equals(name)){value=(StringUtils.hasLength(value)?value+",":"")+HttpMethod.PATCH.name();}super.setHeader(name,value);}});}@OverrideprotectedvoiddoTrace(HttpServletRequestrequest,HttpServletResponseresponse)throwsServletException,IOException{if(this.dispatchTraceRequest){processRequest(request,response);if("message/http".equals(response.getContentType())){return;}}super.doTrace(request,response);}可以看到这两个方法有更多的处理一层逻辑是选择在当前方法中处理对应的请求还是交给over交给父类处理,因为dispatchOptionsRequest和dispatchTraceRequest变量默认为false,所以默认将这两类请求交给父类处理2.processRequest我们再来看processRequest,这算是FrameworkServlet的核心方法了:protectedfinalvoidprocessRequest(HttpServletRequestrequest,HttpServletResponseresponse)throwsServletException,IOException{longstartTime=System.currentTimeMillis();ThrowablefailureCause=null;LocaleContextpreviousLocaleContext=LocaleContextHolder.getLocaleContext();LocaleContextlocaleContext=buildLocaleContext(请求);RequestAttributespreviousAttributes=RequestContextHolder.getRequestAttributes();ServletRequestAttributesrequestAttributes=buildRequestAttributes(请求,响应,previousAttributes);WebAsyncManagerasyncManager=WebAsyncUtils.getAsyncManager(请求);asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(),newRequestBindingInterceptor());initContextHolders(请求,localeContext,requestAttributes);try{doService(request,response);}catch(ServletException|IOExceptionex){failureCause=ex;throwex;}catch(Throwableex){failureCause=ex;thrownewNestedServletException(“请求处理失败”,ex);}finally{resetContextHolders(request,previousLocaleContext,previousAttributes);if(requestAttributes!=null){requestAttributes.requestCompleted();}logResult(request,response,failureCause,asyncManager);publishRequestHandledEvent(response,startTime,failureCause);}}这个方法虽然比较长,但是它的核心其实就是最中间的doService方法。以doService为界,我们可以把这个方法的内容分为三部分:在doService之前,主要是一些准备工作,准备工作主要做两件事。首先是分别从LocaleContextHolder和RequestContextHolder中获取原始的LocaleContext和RequestAttributes对象,然后调用buildLocaleContext和buildRequestAttributes方法获取当前请求的LocaleContext和RequestAttributes对象,然后通过initContextHolders方法将当前请求的LocaleContext和RequestAttributes对象分别设置为LocaleContextHolder和RequestContextHolder对象;第二件事是获取异步管理器并设置拦截器。接下来是doService方法,它是一个抽象方法,具体this的实现是在DispatcherServlet中,这个歌哥是放在DispatcherServlet中然后和大家??一起分析的。第三部分是finally,做了两件事:第一件事是将LocaleContextHolder和RequestContextHolder中对应的对象恢复到原来的状态(参考第一步);第二件事是发布一个ServletRequestHandledEvent类型的消息。经过上面的分析,我们发现processRequest其实做了两件事。第一件事是处理LocaleContext和RequestAttributes,第二件事是发布事件。让我们分别研究这两个东西。2.1LocaleContext和RequestAttributesLocaleContext和RequestAttributes都是接口,不同的是里面存储的对象不同。2.1.1LocaleContextLocaleContext存储Locale,即本地化信息。如果我们需要支持国际化,将使用Locale。在国际化的时候,如果我们需要使用Locale对象,第一反应就是从HttpServletRequest中获取,像这样:Localelocale=req.getLocale();但是大家知道,HttpServletRequest只存在于Controller中,如果我们要在Service层获取到HttpServletRequest时,还得从Controller传参,比较麻烦,尤其是Service中相关方法已经定义好并且那么修改,就更麻烦了。所以SpringMVC也为我们提供了LocaleContextHolder,用于保存当前请求的LocaleContext。看到LocaleContextHolder,不知道是不是眼熟。宋师兄在之前的SpringSecurity系列教程中谈到了SecurityContextHolder。两者的原理基本相同。它们都是基于ThreadLocal来保存变量。对ThreadLocal不熟悉的朋友可以看看松哥的SpringSecurity系列,之前有详细分析(公众号后台回复ss)。使用LocaleContextHolder,我们可以在任何地方获取Locale。比如在Service中,我们可以通过以下方式获取Locale:Localelocale=LocaleContextHolder.getLocale();上面的Locale对象实际上是取自LocaleContextHolder中的LocaleContext。需要注意的是,SpringMVC中还有一个LocaleResolver解析器,所以之前的req.getLocale()并不总是获取到Locale的值。这位宋兄会在以后的文章中和小伙伴们详细聊聊。2.1.2RequestAttributesRequestAttributes是一个接口,可以用来获取/设置/删除某个属性。RequestAttributes有很多实现类,默认是ServletRequestAttributes,通过ServletRequestAttributes,我们可以getRequest、getResponse和getSession。在ServletRequestAttributes的具体实现中,会通过scope参数来判断是操作request还是操作session(如果不记得spring中的scope问题,可以在公众号后台回复spring,并且看松哥录制的免费Spring入门教程,里面有讲),我们看一下ServletRequestAttributes#setAttribute方法(get/remove方法的执行逻辑类似):publicvoidsetAttribute(Stringname,Objectvalue,intscope){if(scope==0){if(!this.isRequestActive()){thrownewIllegalStateException("Cannotsetrequestattribute-requestisnotactiveanymore!");}this.request.setAttribute(name,value);}else{HttpSessionsession=this.obtainSession();this.sessionAttributesToUpdate.remove(name);session.setAttribute(name,value);}}可以看到,这里会先判断scope,如果scope为0则进行request操作,如果范围为1,将运行会话。如果操作是请求,需要先通过isRequestActive方法判断当前请求是否执行。如果执行完成,则不能对其进行其他操作(finally代码块中的requestAttributes.requestCompleted方法执行时,isRequestActive将返回false)。与LocaleContext类似,RequestAttributes存储在RequestContextHolder中,RequestContextHolder的原理也与SecurityContextHolder类似,这里不再赘述。看完上面的解释,你应该已经发现,在SpringMVC中,如果我们需要在Controller以外的地方使用request、response、session,我们不需要每次都从Controller中传递request、response、session等对象,we完全可以直接通过RequestContextHolder来获取,像下面这样:ServletRequestAttributesservletRequestAttributes=(ServletRequestAttributes)RequestContextHolder.getRequestAttributes();HttpServletRequestrequest=servletRequestAttributes.getRequest();HttpServletResponseresponse=servletRequestAttributes.getResponse();是不是非常easy!2.2事件发布最后就是processRequest方法中的事件被发布。在finally代码块中会调用publishRequestHandledEvent方法发送一个ServletRequestHandledEvent类型的事件,具体发送代码如下:privatevoidpublishRequestHandledEvent(HttpServletRequestrequest,HttpServletResponseresponse,longstartTime,@NullableThrowablefailureCause){if(this.publishEvents&&this.webApplicationContext!=null){//Whetherornotwesucceeded,publishanevent.longprocessingTime=System.currentTimeMillis()-startTime;this.webApplicationContext.publishEvent(newServletRequestHandledEvent(this,request.getRequestURI(),request.getRemoteAddr(),request.getMethod(),getServletConfig().getServletName(),WebUtils.getSessionId(request),getUsernameForRequest(request),processingTime,failureCause,response.getStatus()));}}可以看出发送事件需要publishEvents为true,这个变量默认为true。如果需要修改该变量的值,可以在web.xml中配置DispatcherServlet时通过init-param节点配置该变量的值。一般情况下,这个事件会一直发出去。如果项目需要的话,我们可以监听事件,如下:request被执行,事件就会被触发。3.小结本文主要分享给小伙伴们SpringMVC中DispatcherServlet的父类FrameworkServlet,FrameworkServlet的功能其实比较简单,主要是在服务中加入PATCH的处理方法,然后对其他类型的请求进行归类。在processRequest方法中统一处理,processRequest方法分为三部分,首先处理LocaleContext和RequestAttributes,然后执行doService,最后恢复LocaleContext和RequestAttributes中的属性最后的y代码块,并发布请求在同一时间结束的事件。本文转载自微信公众号“江南的一场小雨”,可通过以下二维码关注。转载本文请联系江南一点鱼公众号。
