Spring源码深度解析笔记(3)——SpringMVC

Spring框架提供了构建Web应用程序的全功能MVC模块。通过策略接口,Spring框架是高度可配置的,而且支持多种视图配置,例如JSP技术、Velocity、Tiles、iText和POI。Spring MVC框架并不知道使用的视图,所以不会强迫使用JSP技术。Spring MVC分离了控制器模型对象分派器以及处理程序对象的角色,这种分离让它们更容易进行定制。

Spring的MVC是基于Servlet功能实现的,通过实现Servlet接口的DispatcherServlet来封装其核心功能实现,通过将请求分派给处理程序,同时带有可配置的处理程序映射视图解析本地语言主题解析以及上载文件支持。默认的处理程序是非常简单的Controller接口,只有一个modelAndView handleRequest(request,response)。Spring提供了一个控制器层次结构,可以派生子类。如果应用程序需要处理用户输入表单,那么可以继承AbstractFormController。如果需要把多页输入处理到一个表单,那么可继承AbstractWizardFormController。

Spring MVC 或者其他比较成熟的MVC框架而言,解决的问题无外乎一下几点。

1. 将Web页面的请求传给服务器。
2. 根据不同的请求处理不同的逻辑单元。
3. 返回处理数据并跳转至相应页面。

11.1 SpringMVC 快速体验

  1. 配置web.xml,一个Web中可以没有web.xml文件,也就是说,web.xml并不是Web工程必须的。web.xml是用来初始化配置信息的:比如Welcome页面servletservlet-mappingfilterlistener启动加载级别等。但是SpringMVC的实现原理是通过Servlet拦截所有URL来达到控制的目的,所以web.xml的配置是必须的。

     Spring的MVC之所以必须配置web.xml,其实最关键的是要配置两个地方。
     1. contextConfigLocation:Spring的核心就是配置文件,可以说配置文件是Spring中必不可少的东西,而这个参数就是使Web与Spring的配置文件相结合的一个关键配置。
     2. DispatcherServlet:包含了SpringMVC的请求逻辑,Spring使用此类拦截Web请求进行相应的逻辑处理
    
  2. 创建配置文件applicationContext.xml

     InternalResourceViewResolver是一个辅助Bean,会在ModelAndView返回的视图名前加上prefix指定前缀,再在最后加上suffix指定的后缀,例如:由于XXController返回的ModelAndView中的视图名是testviwe,故该视图解析器将在/WEB-INF/jsp/testviwe.jsp出查找视图。
    
  3. 创建model,模型对于SpringMVC来说是必不可少的,如果处理程序非常简单,完全可以忽略。模型创建主要的目的就是承载数据,使数据传输更加方便。

  4. 创建controller,控制器用于处理Web请求,每个控制器都对应这一个逻辑处理。

     在请求的最后返回了一个ModelAndView类型的实例,ModelAndView类在SpringMVC中占有很重要的地位,控制器执行方法都必须返回一个ModelAndView,ModelAndView对象保存了视图以及视图显示的模型数据。
    
  5. 创建视图文件,视图文件用于展示请求的处理结果,通过对JSTL的支持,可以很方便的展现在控制器中放入ModelAndView中的处理结果数据。

  6. 创建servlet配置文件Spring-Servlet.xml,应为Spring是基于Servlet的实现,所以在Web启动的时候,服务器会首先尝试加载对应于Servlet的配置文件,为为了让项目更加模块化,通常我们将Web部分的配置都存放于此配置文件中。

11.2 ContextLoarderListener

对于SpringMVC功能的实现的分析,首先从web.xml开始,在web.xml文件中我们首先配置的就是ContextLoarderListener,当使用编程方式的时候可以直接将Spring配置信息作为参数传入Spring容器中,但是在Web环境下,需要更多的是与Web环境相结合,通常的办法就是将路径context-param的方式注册并使用ContextLoarderListener进行监听读取

ContextLoarderListener的作用就是启动Web容器时,自动装配ApplicationContext的配置信息。因为它实现了ServletContextListener这个接口,在web.xml配置这个监听器,启动容器时,就会默认执行它的实现的方法,使用ServletContextListener接口,开发者能够在为客户端请求提供服务之前向ServletContext中添加对象。这个对象在ServletContext启动的时候被初始化,然后在ServletContext整个运行期间都是可见的。

每一个Web应用都有一个ServletContext与之关联。ServletContext对象在应用启动时被创建,在应用关闭时被销毁。ServletContext在全局范围内有效,类似与应用中的一个全局变量。

在ServletContextListener中的核心逻辑便是初始化WebApplicationContext实例并存放至ServletContext中。

11.2.1 ServletContextListener的使用
  1. 创建自定义ServletContextListener,首先创建ServletContextListener,目的是在系统启动的时添加自定义的属性,以便于在全局范围内可以随时调用。系统启动的时候会用ServletContextListener实现类的contextInitialized方法,所以需要在这个方法中实现初始化逻辑。
  2. 注册监听器
  3. 测试,一旦Web应用程序启动的时候,我们就能在任意的Servlet或者JSP中通过getServletContext().getAttribute(…);方法获取初始化的参数。
11.2.2 Spring中的ContextLoarderListener

分析了ServletContextListener的使用方式后再来分析Spring中的ServletContextListener的实现就容易理解多了,虽然ServletContextListener实现的逻辑要复杂得多,但是大致的套路还是万变不离其宗。

ServletContext启动之后会调用ServletContextListener的contextInitialized方法,这里涉及一个常用类WebApplicationContext继承自ApplicationContext,在ApplicationContext的基础上又追加了一些特定于Web的操作和属性,非常类似于我们通过编程方式使用Spring时使用的ClassPathXmlApplicationContext类提供的功能。

initWebApplicationContext函数主要是体现了创建WebApplicationContext实例的一个功能架构,从函数中可以看出初始化的大致步骤

1. WebApplicationContext存在性的验证,在配置文件中只允许声明一次ContextLoarderListener,多次声明会扰乱Spring的执行逻辑,首先这里首先做的就是对此验证,在Spring中如果创建WebApplicationContext实例会记录在ServletContext中方便全局调用,而使用的key就是WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE,所以验证的方式就是查看ServletContext实例中是否有对应key的属性。
2. 创建WebApplicationContext实例,如果通过验证,则Spring将创建WebApplicationContext实例委托给createWebApplicationContext函数。综合以上代码,在初始化的过程中,程序首先会读取ContextLoarder类的同目录下的属性文件ContextLoarder.properties,并根据其中的配置提取将要实现WebApplicationContext接口的实现类,并根据这个实现类通过反射的方式进行实例的创建。
3. 将实例记录在ServletContext中。
4. 映射当前的类加载器与创建的实例到全局变量currentContextPerThread中。

11.3 DispatcherServlet

在Spring中,ContextLoarderListener只是辅助功能,用于创建WebApplicationContext类型实例,而真正的逻辑实现其实是在DispatcherServlet中进行的,DispatcherServlet是实现servlet接口实现了。

servlet是一个Java编写的程序,此程序是基于Http协议的,在服务器端运行的,是按照servlet规范编写的一个Java类。主要是处理客户端的请求并将其结果发送到客户端。servlet的声明周期是有servlet容器来控制的,它可以分为三个阶段:初始化、运行和销毁。

  1. 初始化阶段
  1. servlet容器加载servlet类,把.class文件中的数据读取到内存中。
  2. servlet容器创建一个ServletConfig对象。ServletConfig对象包含了servlet的初始化配置信息。
  3. servlet容器创建一个servlet对象。
  4. servlet容器调用servlet对象的init方法进行初始化。
  1. 运行阶段

当servlet接收到一个请求时,servlet容器会针对这个请求创建servletRequest和servletResponse对象,然后调用service方法。service方法通过servletRequest对象获得请求信息。并处理该请求。再通过servletResponse对象生产这个请求的响应结果。然后销毁servletRequest和servletResponse对象。不管这个请求是post请求还是get请求提交的,最终这个请求都会有service方法处理。

  1. 销毁阶段

当web应用被终止时,servlet容器首先会调用servlet对象的destroy方法,让后在销毁对象,同时也会销毁与servlet对象相关的servletConfig对象。我们可以在destroy方法的实现中,释放servlet所占用的资源,如关闭数据库连接,关闭文件输入输出流等。
servlet的框架由两个Java包组成:javax.servlet和javax.servlet.http。在javax.servlet包中定义了所有servlet类必须实现或扩展的通用接口和类,在javax.servlet.http包中定义了采用HTTP通信协议的HttpServlet类。
servlet被设计成请求驱动,servlet的请求可能包含多个数据项,当Web容器接收到某个servlet请求时,servlet把请求封装成一个HttpServletRequest对象,然后把对象传给servlet的对应的服务方法。
HTTP的请求方法包括delete、get、options、post、put和trace,在Httpservlet类中分别提供了相应的服务方法,它们是doDelete()、doGet()、doOptions()、doPost()、doPut()和doTrace().

11.3.1 servlet的使用
  1. 实例中包含了对init方法和get/post方法的处理,init方法保证在servlet加载的时候能做一些逻辑操作,而HttpServlet类则帮助我们根据方法类型的不同而将逻辑引入不同的函数,在子类中我们只需要重写对应的逻辑便可。

  2. 添加配置

11.3.2 DispatcherServlet的初始化

在servlet初始化阶段会调用其init方法,首先要查看DispatcherServlet中是否重写了init方法。我们在其父类HttpServletBean中找到了该方法。

DispatcherServlet的初始化过程主要是通过将当前的servlet类型的实例转换为BeanWrapper类型的实例,以便使用Spring中提供的注入功能进行对应属性的注入。这些属性包括contextAttribute、contextClass、nameSpace、contextConfigLocation等,都可以在web.xml文件中以初始化参数的方式配置在servlet的生命中。DispatcherServlet继承在FrameworkServlet,FrameworkServlet类上包含对应的同名属性,Spring会保证这些参数被注入到对应的值中。属性注入包含一下几个步骤。

  1. 封装及验证初始化参数,servletConfigPropertyValues除了封装属性外还有对属性验证的功能,封装属性主要是对初始化的参数进行封装,也就是servlet中配置的< init-param >中配置的封装。当然,用户可以通过对requireProperties参数来强制验证某些属性的必要性,这样,在属性封装的过程中,一旦检测到requireProperties中的属性没有指定初始值就会抛出异常。
  2. 将当前servlet实例转换为BeanWrapper实例PropertyAccessorFactory.forBeanPropertyAccess是Spring中提供的工具方法,主要用于将指定实例转化为Spring中可以处理的BeanWrapper类型的实例。
  3. 注册相对于Resource的属性编辑器,这里使用属性编辑器的目的是在对当前实例(DispatcherServlet)属性注入过程中一旦遇到Resource类型的属性就会使用ResourceEditor去解析。
  4. 属性注入,BeanWrapper为Spring中的方法,支持Spring的自动注入,其实我们最常用的属性注入无非是contextAttribute、contextClass、nameSpace、contextConfigLocation等属性。
  5. servletBean的初始化,在ContextLoarderListener加载的时候已经创建了WebApplicationContext实例,而在initServletBean函数中最重要的就是对这个实例进行进一步的补充初始化。父类FrameworkServlet覆盖了HttpServletBean中的initServletBean函数,该函数设计了计时器来统计初始化执行时间,而且提供了一个扩展方法initFrameworkServlet()用于子类的覆盖操作,而作为关键的初始化逻辑实现委托给了initWebApplicationContext()。
11.3.3 WebApplicationContext的初始化

initWebApplicationContext函数的主要工作就是创建和刷新WebApplicationContext实例并对servlet功能所使用的变量进行初始化。对于本函数的初始化包含几个部分。

  1. 寻找或创建对应的WebApplicationContext实例
    1.1 通过构造函数的注入进行初始化
    当进入initWebApplicationContext函数后通过判断this.webApplicationContext != null后,便可以确定this.webApplicationContext是否是通过构造函数来初始化的,在Web中包含SpringWeb的核心逻辑的DispatcherServlet只可被声明一次,在Spring中已经存在验证,所以这就确保如果this.webApplicationContext != null,则可以直接判定this.webApplicationContext已经通过构造函数初始化了。
    1.2. 通过contextAttribute进行初始化,通过web.xml文件中配置的servlet参数contextAttribute来查找ServletContext中对应的属性,默认为WebApplicationContext.class.getName() +".ROOT",也就是在ContextLoarderListener加载时会创建WebApplicationContext实例,并将实例以WebApplicationContext.class.getName() + ".ROOT"为key放入ServletContext中,
    1.3. 重新创建WebApplicationContext实例。如果通过以上两种方法并没有找到任何突破,只能重新创建新的实例了(createWebApplicationContext(…))。
  1. configureAndRefreshWebApplicationContext,无论是通过构造函数注入还是单独创建都免不了单独调用configureAndRefreshWebApplicationContext方法来对已经创建的WebApplicationContext实例进行配置及刷新。只要使用ApplicationContext所提供的功能最后都免不了使用公共父类AbstractApplicationContext提供的refresh()进行配置文件加载。
  1. 刷新,onRefresh是FrameworkServlet类中提供的模板方法,在其子类DispatcherServlet中进行了重写,主要用于刷新Spring在Web功能实现中所必须使用的全局变量(initSrategies())。
    3.1 初始化MultipartResolver(initMultipartResolver(…)),在Spring中,MultipartResolver主要是用来处理文件上传。默认情况下,Spring是没有multipart处理的,因为一些开发者想要自己处理它们。如果想使用Spring的Multipart,则需要在Web应用的上下文中添加multipart解析器。这样,每个请求都会被检查是否包含multipart。然而,如果请求中包含multipart,那么上下文中定义的MultipartResolver就会解析它,这样请求中的multipart属性就会像其他属性一样被处理。
    3.2 初始化LocaleResolver(initLocaleResolver(…)),在Spring的国际化配置中一共有3种配置方式:基于URL参数的配置(AcceptHeaderLocaleResolver),通过URL参数来控制国际化,比如在页面上加一句< a href="?locale=zh_CN" > 简体中文 < /a >来控制项目中使用的国际化参数。而提供这个功能的就是AcceptHeaderLocaleResolver,默认的参数名就是locale,里面放的就是提交的参数,比如en_US、zh_CN。 基于session的配置(SessionLocaleResolver),它通过检测用户会话中预置的属性来解析区域。最常用的是根据用户本次会话过程中的语言设置决定语言种类(例如:用户登录时选择语言种类,则此次登录周期内统一使用次语言设定),如果该语言会话属性不存在,他会根据accept-language HTTP头部确定默认区域。基于cookie的国际化配置(CookieLocaleResolver)用于通过cookie设置取得Locale对象,这种策略在应用过程中不支持会话或者状态必须保存在客户端是有用。这三种方法都可以解决国际化问题,但是,对于LocaleResolver的使用就出是在DispatcherServlet中的初始化。提取配置文件中的LocaleResolver来初始化DispatcherServlet的LocaleResolver属性。
    3.3 初始化ThemeResolve(initThemeResolve(…)),在Web开发过程中经常会遇到通过主题Theme来控制网页风格,这将进一步改善用户体验,简单的说,一个主题就是一个静态资源,他们可以影响应用程序的视觉效果。Spring中的主题和国际化功能非常类似。构建主题功能主要包括如下内容:主题资源:Spring的主题需要通过ThemeSource接口来实现存放主题信息的资源。默认状态下是在类路径根目录下查找相应的资源文件,也可以铜鼓basenamePrefix来指定。主题解析器:Theme定义了一些主题资源,那么不同的用户使用什么主题资源由谁定义呢?ThemeResolve是主题解析器的接口,主题解析的工作便是由它的子类完成的。对于主题解析器的子类主要有3个比较常用的实现,FixedThemeResolve用于选择一个固定的主题;CookieThemeResolve用于实现用户所选的主题,以Cookie的形式存在客户端的机器上;SessionThemeResolve用于主题保存的HTTP Session中;AbstractThemeResolve是一个抽象类被SessionThemeResolve和FixedThemeResolve继承,用户也可以继承他来自定义主题解析器。拦截器,如果需要根据用户请求来改变主题,那么Spring提供了一个已经实现的拦截器ThemeChangeIntercepter拦截器,其中设置用户请求参数名为themeName,即URL为?themeName=具体的主题名称。此外,还需要在handlerMapping中配置拦截器。当然需要在handleMapping中添加拦截器。
    3.4 初始化HandlerMappings(initHandlerMappings(…)),当客户端发出请求时DispatcherServlet会将请求提交给HandlerMapping,然后HandlerMapping根据WebApplicationContext的配置来回传给DispatcherServlet相应的Controller,在基于SpringMVC的web程序中,我们可以为DispatcherServlet提供多个HandlerMapping供其使用。DispatcherServlet在选用HandlerMapping的过程中,将根据我们所指定的一系列HandlerMapping的优先级进行排序,然后优先使用优先级高的HandlerMapping。如果当前的HandlerMapping能够返回可用的Handler,DispatcherServlet则使用当前的Handler进行Web请求处理,而不在继续询问其他的HandlerMapping。否则DispatcherServlet将继续按照各个HandlerMapping的优先级进行询问,直到获得一个可用的Handler为止。默认情况下,SpringMVC将加载当前系统中所有实现了HandlerMapping接口的Bean。如果只期望SpringMVC加载指定的HandlerMapping时,可修改web.xml中的DispatcherServlet的初始化参数,将detectAllHandlerMappings的值设为false。此时,SpringMVC将查找名为HandlerMapping的bean,并作为当前系统中唯一的HandlerMapping,如果没有定义HandlerMapping的话,则SpringMVC将按照DispatcherServlet所在目录下的DispatcherServlet.properties所定义的HandlerMapping的内容来加载默认HandlerMapping。
    3.5 初始化HandlerAdapter(initHandlerAdapter(…)),从名字也能联想到这是一个典型的适配器模式,在计算机编程中,适配器模式将一类接口适配成用户所期待的。使用适配器,可以使接口不兼容而无法在一起工作的类协同工作。做法是将自己的接口包裹在一个已存在的类中。那么在处理handler是为什么会使用适配器模式呢?在初始化的过程中涉及了一个变量detectAllHandlerAdapters,detectAllHandlerAdapters作用和detectAllHandlerMappings类似,只不过作用对象是HandlerAdapters。也可以通过配置强制系统只加载beanName为“handlerAdapter”的handlerAdapter,在getDefaultStrategies函数中,Spring会尝试从DefaultStrategies中加载对应的handlerAdapter属性,在系统记载的时候,DefaultStrategies来初始化本身,查看DispatcherServlet.properties中对应的HandlerAdapter的属性(HttpRequestHandlerAdapter、SimpleControllerHandlerAdapter、AnnotationMethodHandlerAdapter),由此可以,如果开发人员没有在文件中配置文件中定制自己的适配器,那么Spring会默认加载配置文件中的3个适配器。作为总控制器的派遣期servlet通过处理器映射得到处理器后,会轮训处理器适配器模块,查找能够处理当前HTTP请求的处理器适配器的实现,处理器适配器模块根据处理器映射返回的处理器类型来选择某一个适当的处理器适配器的是想,从而适配当前的HTTP请求。,HTTP请求处理器适配器(HttpRequestHandlerAdapter)仅仅支持对HTTP请求处理器的适配。他简单的将HTTP请求对象和响应对象传递给HTTP请求处理器,他并不需要返回值。它主要应用是基于HTTP的远程调用的实现上。简单控制器处理器适配器(SimpleControllerHandlerAdapter),这个实现类将HTTP请求适配到一个控制器的实现进行处理。这里控制器的实现是一个简单的控制器的接口的实现。简单控制器处理器适配器被设计成一个框架类的实现,不需要被改写,客户化的业务逻辑通常是在控制器接口实现类中实现的。 注解方法处理器适配器(AnnotationMethodHandlerAdapter),这个类的实现是基于注解实现的,它需要结合注解方法映射和注解方法处理器协同工作。它通过解析声明在注解控制器的请求映射信息来解析相应的处理器方法来处理当前HTTP请求。在处理过程中,它通过反射来发现探测处理器方法的参数,调用处理器方法,并映射返回值到模型和控制器对象,最后返回模型和控制器对象给作为主控制器的派遣器Servlet。
    3.6 初始化HandlerExceptionResolvers(initHandlerExceptionResolvers(…)),基于HandlerExceptionResolver接口的异常处理,使用这种方式只需要实现ResolverException方法,该方法返回一个ModelAndView对象,在方法内部对异常的类型进行判断,然后尝试产生对应的ModelAndView对象,该方法返回了null,则Spring会继续寻找其他的实现了HandlerExceptionResolver接口的Bean。换句话说,Spring会搜索所有注册在其环境中的实现了HandlerExceptionResolver接口的Bean,这个执行直到返回一个ModelAndView对象。
    3.7 初始化RequestToViewNameTranslator(initRequestToViewNameTranslator(…)),当Controller处理器方法没有返回一个View对象或逻辑视图名称,并且在该方法中没有直接向response的输出流里面写数据的时候,Spring就会采用约定好的方式提供一个逻辑视图名。这个逻辑视图名称是通过Spring定义的RequestToViewNameTranslator接口的getViewName方法来实现的,我们可以自己实现RequestToViewNameTranslator接口来约定好没有返回视图名称的时候如何确定视图名称,Spring已经给我们提供了一个它自己的实现,那就是DefaultRequestToViewNameTranslator
    3.8 初始化ViewResolvers(initViewResolver(…)),在SpringMVC中,当Controller将请求处理结果放到ModelAndView以后,DispatcherServlet会根据ModelAndView选择合适的视图进行渲染,ViewResolver接口定义了resolverViewName方法,根据viewName创建合适类型的View实现。
    3.9 初始化FlashMapManager(initFlashMapManager(…)),SpringMVC Flash Attribute提供了一个请求存储属性,可供其他请求使用,在使用重定向时候非常必要。例如Post/Redirect/Get模式, Flash Attributes在重定向之前暂存(就像存在session中)以便重定向之后还能使用,并立即删除。SpringMVC主要有两个抽象来支持Flash Attributes。FlashMap由于保持Flash Attributes,而FlashMapManager又来存储、检索、管理FlashMap实例。 Flash Attribute支持默认开启并不需要显示开启,它永远不会导致HTTP Session创建。这两个FlashMap实例都可以通过静态方法RequestContextUtils从SpringMVC的任何位置访问。

11.4 DispatcherServlet的逻辑处理

在这里插入图片描述
根据之前的实例,我们知道HttpServlet类中分别提供了相应的服务方法,他们是doDelete(),doGet(),doOPtions(),doPost(),doPut()和doTrace(),他会根据请求的不同形式将程序引导至对应的函数进行处理。这几个函数中最常用的无非就是doGet()和doPost(),那么我们直接查看DispatcherServlet中对于这两个函数的是逻辑实现。

对于不同的方法,Spring并没有做特殊的处理,而是统一将程序再一次的引导至processRequest(request,response)中。

函数中已经开始了对请求的处理,虽然把细节转移到了doService函数中实现,但我们不难看出处理请求前后所做的准备和处理工作。

  1. 为了保证当前线程的LocaleContext以及RequestAttributes可以在当前请求后还能恢复,提起当前线程的两个属性。
  2. 根据当前Request创建对应的LocaleContext和RequestAttributes,并绑定到当前线程。
  3. 委托doService方法进一步处理。
  4. 请求处理结束后恢复线程到原始状态。
  5. 请求处理结束后无论成功与否发布时间通知。

我们猜想对请求处理至少包含一些诸如寻找Handler并跳转页面之类的逻辑处理,但是在doService中并没有看到相关的逻辑,相反却同样是一些准备工作,但是这些准备工作是必不可少的。Spring将已初始化的功能辅助工具变量,比如localeResolver、themeResolver等设置到request属性中,而这些属性会在接下来的处理中派上用场。

经过层层的准备工作,终于在doDispatch函数中看到了完整的请求处理过程。

  1. 如果是multipartContent类型的request,则转换request为MultipartHttpServletRequest类型的request(checkMultipart(…))。
  2. 根据request信息查找对应的Handler(getHandler(…)),如果没有对应的handler则通过response反馈错误信息。
  3. 根据当前的handler查找对应的HandlerAdapter(getHandlerAdapter(…))。
  4. 如果当前handler支持last-modified头处理。
  5. 拦截器preHandler方法的调用(triggerAfterCompletion(…))。
  6. 激活handler并返回视图,视图名称转换应用于需要添加前后缀的情况。
  7. 应用所有拦截器的postHandler方法(processHandlerException(…))。
  8. 如果在handler实例的处理返回了view,那么需要做页面的处理(render(…))。
  9. 完成处理激活触发器(triggerAfterCompletion(…))

doDispatch函数展示了Spring请求处理所涉及的主要逻辑,而我们之前设置在request中的各种辅助属性也都有派上用场。

11.4.1 MultipartContent类型的request处理

对于请求的处理,Spring首先考虑的是对于Multipart的处理,如果是MultipartContent类型的request,则转换request为MultipartHttpServletRequest类型的request。

11.4.2 根据request信息寻找对应的Handler

在Spring加载的过程中,Spring会将类型为SimpleUrlHandlerMapping的实例加载到this.handlerMappings中,按照常理腿短,根据request提取对应的Handler,无非是提取对应的Controller,但是单钱的Controller为继承自AbstractController类型实例,与HandlerExecutionChain无任何关联,那么这一步是如何封装的呢?

在之前的内容中提到过,在系统启动时Spring会将所有的映射类型的bean注册到this.handlerMappings变量中,所以此函数的目的就是遍历所有的HandlerMapping,并调用其getHandler方法进行封装处理。以SimpleUrlHandlerMapping为例查看其getHandler方法

  1. getHandlerInternal(…):根据request获取对应的handler,如果没有对应request的handler则使用默认的handler,如果也没有提供默认的handler则无法继续处理返回null。
  2. getHandlerExcutionChain(…):获得处理器执行链。

函数首先首先会使用getHandlerInternal方法根据Request信息获取对应的Handler,如果以SimpleUrlHandlerMapping为例分析,此步骤提供的功能就是根据URL找到匹配的Controller并返回。当然如果没找到对应的Controller处理器那么程序会尝试查找配置中默认处理器。当然,当查找的controller为String类型时,那就意味着返回的是配置的bean名称,需要根据bean名称查找对应的bean,最后,还要通过getHandlerExcutionChain方法对返回的Handler进行封装,以保证满足返回类型的匹配。

  1. 根据request查找对应的Handler(getHandlerInternal(…))
  1. 截取用于匹配的url有效路径。
  2. 根据路径寻找Handler(lookupHandler(…)),根据URL获得对应Handler的匹配规则代码考虑了直接匹配和通配符匹配两种情况。其中buildPathExposingHandler(…)函数,他将Handler封装成HandlerExcutionChain,在函数中通过将Handler一参数形式传入,并构建HandlerExcutionChain类型实例,加入了两个拦截器。链处理机制,是Spring中非常常用的处理方式。是AOP中的重要组成部分,可以方便的对目标对象进行扩展和拦截,这是非常有些的涉及。
  1. 加入拦截器到执行链(getHandlerExcutionChain(…))

getHandlerExcutionChain函数最主要的目的是将配置中的对应的拦截器加入到执行链中,以保证这些拦截器可以有效的作用于目标对象。

11.4.3 没有找到对应的Handler的错误处理

每个请求都应该对应这一个Handler,因为每个请求都会在后台有相应的逻辑对应,而逻辑的实现就在Handler中,所以一旦遇到没有找到Handler的情况(正常情况下如果没有URL匹配的Handler,开发人员可以设置默认的Handler来处理请求,但是默认的请求也未设置就会出现Handler为空的情况),就只能通过request向用户返回错误信息了。

11.4.4 根据当前Handler寻找对应的HandlerAdapter

在WebApplicationContext的初始化过程中HandlerAdapter已被初始化,在默认情况下普通的Web请求会交给SimpleControllerHandlerAdapter去处理。下面以SimpleControllerHandlerAdapter为例分析获取适配器逻辑。

通过函数可知,对于获取适配器的逻辑无非就是遍历所有适配器并选择适合的适配器并返回,而某个适配器是否适用当前的Handler逻辑被封装在具体的适配器中。

分析到这里,一切已经明了,SimpleControllerHandlerAdapter就是用于处理普通请求的,而对于SpringMVC来说,我们会把逻辑封装至Controller中。

11.4.5 缓存处理

在研究Spring对缓存处理的功能支持前,需要先了解一个概念:Last-Modified缓存机制。

  1. 客户端第一次输入URL时,服务器端会返回内容和状态码200,表示请求成功,同时会添加一个Last-Modified的响应头,表示此文件在服务器上的最后更新时间。
  2. 客户端在二次请求此URL时,客户端会向服务器发送请求头“If-Modified-Since”,询问服务器时间之后当前请求内容是否有修改过,如果服务器内容没有变化,则自动返回HTTP 304状态码,只要响应头,内容为空,这样节省了带宽。

Spring提供的对Last-Modified机制的支持,只需要实现LastModified接口的getLastModified方法,保证当前内容发生改变是返回最新的修改时间即可。

Spring判断是否过期,通过判断“If-Modified-Since”是否大于等于当前的getLastModified方法的时间戳,如果是,则认为没有修改。

11.4.6 HandlerInterceptor的处理

servlet API定义的servlet过滤器可以在servlet处理每个Web请求的前后分别对它进行前置处理和后置处理。此外有些时候,可能只想处理由某些SpringMVC处理程序处理的Web请求,并在这些处理程序返回的模型属性被传递到视图之前,对它进行一些操作。

SpringMVC允许你通过Web拦截Web请求,进行前置会处理和后置处理。拦截器是在Spring的Web应用程序上下文配置的,因此他们可以利用容器特性,并引用容器中声明的任何bean。处理拦截是针对特殊的处理程序映射进行注册的,因此它只拦截通过这些处理程序映射的请求。每个处理拦截都必须实现HandlerInterceptor接口,它包含三个需要实现的回调方法:preHandler(),postHandler(),afterHandler()和afterCompletion(),第一个和第二个方法分别是在处理程序处理请求之前和之后被调用的。第二个方法还允许访问返回的ModelAndView对象,因此可以在它里面操作模型属性。最后一个方法是在所有请求处理完成之后被调用(视图呈现之后)。

11.4.7 逻辑处理

对于逻辑处理其实是通过适配器中转调用Handler并返回视图(handlerRequest(…))

调用用户逻辑(handleRequestInternal(…))

11.4.8 异常视图的处理

有时候系统运行过程中出现异常,而我们并不希望就此中断对用户的服务,而至少告知客户当前系统在处理逻辑的过程中出现异常,甚至告知他们因为什么原因导致的。Spring中的异常处理机制会帮我们完成这个工作。其实,这里Spring主要是将逻辑引导至HandlerExceptionResolver类的resolverException方法。

11.4.9 根据视图跳转页面

无论是一个系统还是一个站点,最终的工作都是与用户进行交互,用户操作系统后无论下发的命令成功与否都需要可以个反馈,以便用户进行下一步判断。所以在逻辑处理的最后一定会涉及一个页面跳转的问题。

  1. 解析视图名称,DispartcherServlet会根据ModelAndView选择适合的是视图进行渲染,而这一功能就是在resolverViewName函数中完成。在父类UrlBasedViewResolver中重写createView函数。此处的功能主要考虑到了几个方面
    1.1 基于效率的考虑,提供缓存支持。
    1.2 提供了对redirect:xx 和 forward:xx前缀的支持。
    1.3 添加了前缀以及后缀,并向View中加入了必需的属性设置。
  2. 当通过viewName解析对应View后,就可以进一步处理跳转逻辑了(render(…)),对于ModelAndView的使用,可以将一些属性直接放入其中,然后在页面上直接通过JSTL语法或者原始的request获取。这是一个方便也很神器的功能,但是实现却并不复杂,无非是把我们将要用到的属性放入request中,以便在其他地方可以直接调用,而解析这些属性的工作就是在createMergedOutputModel函数中完成的。处理页面跳转是在renderMergedOutputModel函数中完成的。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值