【Spring MVC】创建过程
1 前言
本章将分析SpringMVC自身的创建过程。首先分析SpringMVC的整体结构,然后具体分析每一层的创建过程。
2 整体结构介绍
SpringMVC中核心Servlet的继承结构如图所示:
可以看到在Servlet的继承结构中一共有5个类,GenericServlet 和HttpServlet在java中,前面已经讲过,剩下的三个类 HttpServletBean、FrameworkServlet和 DispatcherServlet是SpringMVC中的,本章主要讲解这三个类的创建过程。
这三个类直接实现三个接口:EnvironmentCapable、EnvironmentAware和Application-ContextAware。XXXAware 在 spring 里表示对XXX可以感知,通俗点解释就是:如果在某个类里面想要使用spring 的一些东西,就可以通过实现XXXAware接口告诉spring,spring看到后就会给你送过来,而接收的方式是通过实现接口唯一的方法 set-XXX。比如,有一个类想要使用当前的ApplicationContext,那么我们只需要让它实现ApplicationContextAware 接口,然后实现接口中唯一的方法 void setApplicationContext(ApplicationContext applicationContext)就可以了,spring会自动调用这个方法将applicationContext 传给我们,我们只需要接收就可以了!很方便吧! EnvironmentCapable,顾名思义,当然就是具有 Environment的能力,也就是可以提供 Environment,所以 EnvironmentCapable唯一的方法是 Environment getEnvironment0),用于实现EnvironmentCapable 接口的类,就是告诉spring它可以提供 Environment,当spring需要Environment的时候就会调用其getEnvironment方法跟它要。
了解了Aware和Capable 的意思,下面再来看一下ApplicationContext 和 Environment。前者相信大家都很熟悉了,后者是环境的意思,具体功能与之前讲过的 ServletContext有点类似。实际上在HttpServletBean 中 Environment使用的是Standard-Servlet-Environment(在createEnvironment方法中创建),这里确实封装了 ServletContext,同时还封装了 ServletConfigJndiProperty、系统环境变量和系统属性,这些都封装到了其 propertySources 属性下。为了让大家理解得更深刻,在前面项目的 GoController 中获取 Environment,然后通过调试看一下。首先让GoController 实现 EnvironmentAware 接口,这样spring 就会将 Environment传给我们然后在处理请求的方法中加入断点,这样就可以查看 spring传进来的内容了。修改后代码如下:
为了看得更加清楚,显示设置Servlet 定义时的contextConfigLocation 属性。
然后启动调试,并且打开浏览器发送一个根路径的请求。程序会中断到我们设置的断点的地方。这时将鼠标放到变量上就可以看到其内容了,如图 9-2 所示
可以看到 propertySources 中确实包含前面所说的5个属性,然后再来看一下ServletConfigPropertySource的内部结构,如图9-3所示。
从图中可以看到 ServletConfigPropertySource的 source 的类型是StandardWrapperFacade.也就是Tomcat里定义的ServletConfig类型,所以ServletConfigPropertySource 封装的就是ServletConfg。在 webxml中定义的 contextConfgLocation 可以在 config 下的 parameters 里看到,这里还可以看到name 以及parent 等属性。当然,这里的confg 是私有的,不可以直接调用,config其实是Tomcat 中的 StandardWrapper 存放 Servlet 的容器(图9-4)。
ServletContextPropertySource 中保存的是ServletContext(图9-5)。
JndiPropertySource 从名字就很容易理解,存放的是Jndi,由于这里没有使用Jndi,所以就不细讲了。MapPropertySource 存放的是我们所用的虚拟机的属性,如Java 的版本、所用操作系统的名称、操作系统的版本号、用户主目录、临时目录Catalina(Tomcat)的目录等内容SystemEnvironmentPropertySource 存放的是环境变量,也就是我们设置的JAVA HOME、Path等属性。
可见Environment的功能非常强大。通过慢慢体会Environment,我们就可以对程序中的“环境”这个词有更深入的理解。
看完整体结构,接下来分别看一下spring 中三个类的具体创建过程。
2.1 HttpServletBean
通过前面对 Servlet 的分析,我们知道 Servlet 创建时可以直接调用无参数的init 方法HttpServletBean的init 方法如下:
可以看到,在 HttpServletBean 的 init 中,首先将 Servlet 中配置的参数使用 BeanWrapper设置到 DispatcherServle 的相关属性,然后调用模板方法 initServletBean,子类就通过这个方法初始化。
2.2 FrameworkServlet
从HttpServletBean 中可知,FrameworkServlet 的初始化人口方法应该是initServletBean,其代码如下:
可以看到这里的核心代码只有两句:一句用于初始化 WebApplicationContext,另一句用于初始化FrameworkServlet,而且 initFrameworkServlet 方法是模板方法,子类可以覆盖然后在里面做一些初始化的工作,但子类并没有使用它。这两句代码如下:
可见FrameworkServlet 在构建的过程中的主要作用就是初始化了 WebApplicationContext。
下面来看一下initWebApplicationContext 方法。
initWebApplicationContext方法做了三件事:
- 获取spring的根容器rootContext。
- 设置webApplicationContext并根据情况调用onRefresh 方法。
- 将webApplicationContext设置到 ServletContext中。
获取spring的根容器rootContext
获取根容器的原理是,默认情况下spring 会将自己的容器设置成ServletContext 的属性默认根容器的 key 为orgspringframework.web.context.WebApplicationContext.ROOT,定义在org.springframework.web.context.WebApplicationContext 中。
设置 webApplicationContext 并根据情况调用onRefresh 方法
设置webApplicationContext一共有三种方法。
第一种方法是在构造方法中已经传递webApplicationContext参数,这时只需要对其进行些设置即可。这种方法主要用于 Servlet3.0 以后的环境中,Servlet3.0之后可以在程序中使用ServletContext.addServlet 方式注册 Servlet,这时就可以在新建 FrameworkServlet 和其子类的时候通过构成方法传递已经准备好的 webApplicationContext。
第二种方法是webApplicationContext 已经在ServletContext 中了。这时只需要在配置Servlet 的时候将 ServletContext 中的 webApplicationContext 的 name 配置到 contextAttribute属性就可以了。比如,在ServletContext 中有一个叫 haha的webApplicationContext,可以这么将它配置到SpringMVC中:
第三种方法是在前面两种方式都无效的情况下自己创建一个。正常情况下就是使用的这种方式。创建过程在createWebApplicationContext方法中,createWebApplicationContext 内部又调用了configureAndRefreshWebApplicationContext方法,代码如下:
这里首先调用getContextClass 方法获取要创建的类型,它可以通过 contextClass 属性设置到 Servlet 中,默认使用orgspringframework.web.context.support.Xml-WebApplication-Context.然后检查属不属于 ConfgurableWebApplicationContext类型,如果不属于就抛出异常。接下来通过 BeanUtils.instantiateClass(contextClass)进行创建,创建后将设置的 contextConfigLocation传人,如果没有设置,默认传人WEB-INFO/TServletName]-Servletxml,然后进行配置。其他内容基本上都很容易理解,需要说明的是,在configureAndRefreshWebApplicationContext方法中给wac添加了监听器。
SourceFilteringListener 可以根据输入的参数进行选择,所以实际监听的是 ContextRefreshListener 所监听的事件。ContextRefreshListener 是 FrameworkServlet 的内部类,监听 Context.RefreshedEvent事件,当接收到消息时调用FrameworkServlet 的onApplicationEvent方法,在onApplicationEvent 中会调用一次 onRefresh 方法,并将refreshEventReceived 标志设置为 true表示已经refresh 过,代码如下:
再回到initWebApplicationContext 方法,可以看到后面会根据 refreshEventReceived标志来判断是否要运行onRefresh。
当使用第三种方法初始化时已经refresh,不需要再调用onRefresh。同样在第一种方式中也调用了 configureAndRefreshWebApplicationContext方法,也refresh 过,所以只有使用第二种方式初始化 webApplicationContext 的时候才会在这里调用onRefresh 方法。不过不管用哪种方式调用,onRefresh 最终肯定会而且只会调用一次,而且 DispatcherServlet 正是通过重写这个模板方法来实现初始化的。
将webApplicationContext 设置到 ServletContext 中
最后会根据 publishContext标志判断是否将创建出来的 webApplicationContext设置到ServletContext的属性中,publishContext标志可以在配置Servlet 时通过 init-param参数进行设置,HttpServletBean初始化时会将其设置到publishContext 参数。之所以将创建出来的webApplicationContext 设置到 ServletContext 的属性中,主要是为了方便获取,在前面获取RootApplicationContext 的时候已经介绍过。
前面介绍了配置 Servlet 时可以设置的一些初始化参数,总结如下:
- contextAttribute:在 ServletContext 的属性中,要用作 WebApplicationContext 的属性名称。
- contextClass:建WebApplicationContext的类型。
- contextConfigLocation: Spring MVC配置文件的位置。
- publishContext;是否将 webApplicationContext 设置到 ServletContext 的属性。
2.3 DispatcherServlet
onRefresh 方法是 DispatcherServlet 的人口方法。onRefresh 中简单地调用了initStrategies,在initStrategies中调用了9个初始化方法:
可能有读者不理解为什么要这么写,为什么不将intStrategies 的具体实现直接写到onRefresh 中呢?initStrategies 方法不是多余的吗?其实这主要是分层的原因,onRefresh是用来刷新容器的,initStrategies 用来初始化一些策略组件。如果把 initStrategies 里面的代码直接写到onRefresh 里面,对于程序的运行也没有影响,不过这样一来,如果在onRefresh中想再添加别的功能,就会没有将其单独写一个方法出来逻辑清晰,不过这并不是最重要的.更重要的是,如果在别的地方也需要调用 initStrategies 方法(如需要修改一些策略后进行热部署),但initStrategies 没独立出来,就只能调用onRefresh,那样在onRefresh 增加了新功能的时候就麻烦了。另外单独将initStrategies 写出来还可以被子类覆盖,使用新的模式进行初始化。
initStrategies 的具体内容非常简单,就是初始化的9个组件,下面以LocaleResolver 为例来分析具体的初始化方式:
初始化方式分两步:首先通过context.getBean在容器里面按注册时的名称或类型(这里指”localeResolver”名称或者 LocaleResolver.class类型)进行查找,所以在 Spring MVC的配置文件中只需要配置相应类型的组件,容器就可以自动找到。如果找不到就调用getDefaultStrategy按照类型获取默认的组件。需要注意的是,这里的context 指的是 Frame-workServlet 中创建的 WebApplicationContext,而不是 ServletContext。下面绍 getDefault-Strategy 是怎样获取默认组件的。
可以看到 getDefaultStrategy 中调用了 getDefaultStrategies,后者返回的是 List,这是因为HandlerMapping等组件可以有多个,所以定义了 getDefaultStrategies 方法,getDefaultStrategy直接调用了getDefaultStrategies 方法,并返回返回值的第一个结果。
getDefaultStrategies 中实际执行创建的方法是 ClassUtils.forName,它需要的参数是className,所以最重要的是看 className 怎么来的,找到了 className 的来源,也就可以理解默认初始化的方式。className 来自 classNames,classNames 又来自 value,而 value 来自 defaultStrategies.getProperty (key)。所以关键点就在 defaultStrategies 中,defaultStrategies 是一个静态属性,在static块中进行初始化的。
我们看到 defaultStrategies 是 DispatcherServlet类所在包下的DEFAULT_STRATEGIES_PATH文件里定义的属性,DEFAULT_STRATEGIES_PATH 的值是 DispatcherServlet.properties。所以 defaultStrategies 里面存放的是 orgspringframework.web.DispatcherServlet.properties 里面定义的键值对,代码如下:
可以看到,这里确实定义了不同组件的类型,一共定义了8个组件,处理上传组件 MultipartResolver 是没有默认配置的,这也很容易理解,并不是每个应用都需要上传功能,即使需要上传也不一定就要使用 MultipartResolver,所以MultipartResolver 不需要默认配置。另外HandlerMapping、HandlerAdapter 和 HandlerExceptionResolver都配置了多个,其实View.Resolver也可以有多个,只是默认的配置只有一个。
这里需要注意两个问题:首先默认配置并不是最优配置,也不是 spring 的推荐配置,只是在没有配置的时候可以有个默认值,不至于空着。里面的有些默认配置甚至已经被标注为@Deprecated,表示已弃用,如 DefaultAnnotationHandlerMapping、Annotation-MethodHandlerAdapter以及AnnotationMethodHandlerExceptionResolver。另外需要注意的一点是,默认配置是在相应类型没有配置的时候才会使用,如当使用<mvc:annotation-driven/>后,并不会全部使用默认配置。因为它配置了HandlerMapping、HandlerAdapter 和 Handler-ExceptionResolver,而且还做了很多别的工作,更详细的内容可以查看 orgspringframework.web.servlet.config.AnnotationDrivenBeanDefinitionParser。
DispatcherServlet 的创建过程主要是对9大组件进行初始化,具体每个组件的作用后面具体讲解。
3 小结
本章主要分析了 Spring MVC自身的创建过程,Spring MVC 中 Servlet一共有三个层次分别是 HtpServletBean、FrameworkServlet 和 DispatcherServlet。HttpServletBean 直接继承自Java 的 HttpServlet,其作用是将 Servlet 中配置的参数设置到相应的属性;
FrameworkServlet初始化了 WebApplicationContext,DispatcherServlet初始化了自身的9个组件。FrameworkServlet 初始化 WebApplicationContext一共有三种方式,过程中使用了 Servlet中配置的一些参数。
整体结构非常简单一一分三个层次做了三件事,但具体实现过程还是有点复杂的。这其实也是 spring 的特点:结构简单,实现复杂。结构简单主要是顶层设计好,实现复杂的主要是提供的功能比较多,可配置的地方也非常多。当然,正是因为实现复杂,才让 Spring MVC使用起来更加灵活,这一点在后面会有更深刻的体会。如果能静下心来对照着源代码耐心地去看,还是很容易理解的。