spring mvc 父子容器
spring MVC的实现方式
我们在早期的项目开发过程中,在web框架方面要不使用原生的servlet进行编程,要么使用struts,要么使用spring mvc web框架,但是在目前的行业中,可能spring mvc使用的频率是最高的,我记得我在2012年的时候在一家软件公司,公司中就使用的是spring的mvc,但是当初还没有微服务这个概念,都是前后端在一起的,前后端没有分离,使用的是sprinv mvc的视图解析器,前端使用的framemarker模板引擎技术实现的数据展示。到现如今,struts2基本上已经退出历史舞台了,特别是现在的微服务中使用的都是spring的一整套解决方案,但是我们在早期的spring mvc中,可能大家搭建环境的时候影响都非常深刻,在web.xml中会配置两个xml文件,比如像下面这种:
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring.xml</param-value>
</context-param>
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-servlet.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
在上面的xml文件中有一个监听器,一个servlet,在使用这种方式来配置我们的项目的时候,其实这两个配置就是spring mvc启动的时候需要根据这两个来创建容器,其中ContextLoaderListener是spring的默认容器,也就是在ContextLoaderListener中会创建一个父容器,作为我们项目中来使用,而DispatcherServlet是mvc中非常重要的一个servlet,这个servlet在启动过程中也会创建一个子容器,在controller中使用,这两个容器的创建过程都是tomcat本身来完成的,前面我们说过tomcat在启动的过程中会注册监听器,当容器向内引爆的启动到了context容器过后,那么这个时候启动了context容器过后,就会调用注册的监听器,具体的tomcat代码在StartdarContext中的startInternal启动conext容器的方法里面
这里就会去调用注册的监听器中的contextInitialized方法
来创建父容器,父容器在也是spring的一个IOC容器,只是这里面存放的是非controller的bean对象,但是如果把controller的扫描路径也放进去,也能扫描出来controller来存放到里面,这里就来说下父子容器。
spring 父子容器
通过从上面的web.xml中我们可以知道,tomcat在启动过程中会调用一个监听器和一个servlet,那么这两个类都是spring提供的实现,所以这里虽然是tomcat作为web容器启动sevlet容器,但是其实是spring来完成的servle容器的初始化和启动,根据上面两个我们可以简单看下spring的配置文件:
spring.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">
<context:component-scan base-package="com.dev1.web.service"/>
<mvc:annotation-driven />
</beans>
spring-servlet.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">
<context:component-scan base-package="com.dev1.web.controller"/>
<mvc:annotation-driven />
</beans>
spring作为父容器,而spring-servlet作为子容器启动,父容器是在监听器ContextLoaderListener中完成的启动,而子容器也就是web容器是在DispatcherServlet中完成的启动;
那么如果说我只有一个容器,也就是我只配置一个配置文件有没有问题,也没有问题或者说两个配置文件都配置一样的路径,也没有问题,spring也会启动成功,但是注意的是,在service中也就是父容器中是不能注入父容器controller对象的,而在conroller中是可以注入父容器中的sevice bean对象的,这个是一个继承关系,类似于java中类的继承关系,父类中是不可以访问子类的方法的,而子类是可以访问父类的方法的,这个在前面的spring专题的笔记中在源码中都已经体现了,sprng在getBean的时候,如果当前容器没有获取到,会去调用getParent获取父容器来找bean,如果都没有找到才会报错,所以要明白的是父子容器的一个关系,但是如果说你把controller的扫描路径也配置到父容器中,那么也可以扫描出来的,但是这样就很乱,子容器和父容器中有相同的对象在一起,这样不仅浪费了资源,而且启动过程性能还要受一定的影响,这样做没有意义,要不你把父子容器的bean对象进行分类存储,要不就不要父子容器,直接在一个容器里面搞多好,在controller注入的时候还不需要找两次,当前容器找不到还要去父容器去找。在早期的spring mvc项目中,其实是分了两个 容器的,那么为什么要怎么做,在早期的项目中,市面上还有其他的web框架,sprig只是作为一个后端服务的ioc框架,而比如像struts一样,也有自己的容器框架,那么两个配置文件,把各自的bean都放在对应的xml文件中,那么这样比如说替换框架什么的就很方面,如果你都在一个配置文件中,首先你还要区分出那些bean是controller的,那些是service的,这样做就乱,分开过后子容器管理子容器的bean,父容器管理父容器的bean,并且父容器不能去访问子容器的bean,但是到目前为止,现在都是零配置的的项目了,已经没有分父子容器了,像spring boot已经没有分父子容器了,都在一个容器中,因为现在spring有了一整套生态了,所有的东西都是自己的,controller也是自己的mvc,所以先应该 很多项目都没有父子容器了。
父容器创建过程
我们前面已经说了父容器的创建是在ContextLoadLIstener中完成的,所以我这里就开始分析ContextLoadListener的父容器创建过程的源码,虽然说父容器的创建是在ContextLoadLIstener,但是其实创建的过程都是大同小异,无非就是获取到容器类,然后得到容器类的实例对象,然后加载web.xml中配置的spring配置文件路径,然后将xml中bean标签扫描成一个一个的BeanDefinition,然后refresh,启动容器,最后将创建成功的容器对象实例放入到servlet的上下文中,那么只要servlet的生命周期内,这个容器对象都是可用的,这里值得一说的是我们在服务层经常会实现一个接口ApplicationContextAware接口,然后可以得到spring当前容器的上下文对象,然后有时候会通过这个对象获取到sevlet上下文信息,其实这个是在容器的创建过程中将servlet的上下文信息添加到了spring的上下文中,这样两个上下文就可以相通了,这个在源码中会有体现,ContextLoadLIstener这个监听器还隐藏了一个非常有用的扩展点,我们通过源码可以知道ContextLoadLIstener是创建父容器的过程,但是这里隐藏了一个扩展点就是我们可以再次创建一个顶级的容器,然后作为一个最顶级的父级容器,然后ContextLoadLIstener创建的容器作为第二级容器,这个待会儿通过源码和实例来分析;
ContextLoadLIstener是一个监听器,在tomcat扫描的时候会注册这个监听器,然后容器启动完成过后会来回调这个监听器的contextInitialized方法,所以源码如下:
/**
* Initialize the root web application context.
* 这个就是spring提供给容器回调的一个监听器中回调的方法
* 在这个监听器的方法中,spring启动了一个父容器,这个父容器就是
* 启动加载了我们服务层的一些bean,这里加载得到的父容器,spring会将
* 它放在servlet的上下文中
* 1.找到父容器class对象;
* 2.得到父容器XmlWebApplicationContext的实例对象;
* 3.将servlet上下文信息放入到容器中;
* 4.设置容器的spring配置文件加载路径;
* 5.调用容器的refresh方法启动容器(在容器的obtainFreshBeanFactory加载xml文件的bean标签);
* 6.将创建的容器对象设置到servlet上下文中;
* 7.将创建的容器放入一个map<当前线程的类加载器,容器>.
*/
@Override
public void contextInitialized(ServletContextEvent event) {
initWebApplicationContext(event.getServletContext());
}
initWebApplicationContext
initWebApplicationContext是父容器的创建过程,我们这里详细来看下这个方法
public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
/**
* 这里先获取一个属性WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE,这个名字就是
* 父容器的名字,从servlet的上下文中获取,如果说这里获取到的容器不为空,则直接报错
* 因为这个方法就是来创建父容器的,如果这里为空的话,那么肯定直接报错的,因为已经有父容器存在了
* 什么情况下会出现这里获取到不为空呢?我觉得除非了你配置了多个监听器,而这些监听器都在创建父容器,
* 所以说一个web框架的spring项目中,不可能存在多个父容器
*/
if (servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null) {
throw new IllegalStateException(
"Cannot initialize context because there is already a root application context present - " +
"check whether you have multiple ContextLoader* definitions in your web.xml!");
}
servletContext.log("Initializing Spring root WebApplicationContext");
Log logger = LogFactory.getLog(ContextLoader.class);
if (logger.isInfoEnabled()) {
logger.info("Root WebApplicationContext: initialization started");
}
long startTime = System.currentTimeMillis();
try {
// Store context in local instance variable, to guarantee that
// it is available on ServletContext shutdown.
//context是当前的容器,创建父容器的时候这里一般都是为空的
//如果不为空,则上面就会报错
if (this.context == null) {
//得到容器上下文的实例对象
this.context = createWebApplicationContext(servletContext);
}
//从createWebApplicationContext方法可知,我们这里得到的容器上下文类肯定是ConfigurableWebApplicationContext类型
//否则都要报错
if (this.context instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
if (!cwac.isActive()) {
// The context has not yet been refreshed -> provide services such as
// setting the parent context, setting the application context id, etc
/**
* 下面的这个方法是什么意思呢?loadParentContext是一个空实现,返回的是一个null
* 其实这里就是spring在设计的时候考虑到一个问题就是,我这里创建的是父容器,在DispatcherServletWebRequest
* 中创建的是controoler的子容器,那么spring这里的下面几行代码是什么意思呢?
* 其实我们读源码的目的就是要知道它有哪些扩展点,其实就是说如果说当前创建出来的
*容器的父容器如果是空的,那么可以提供一个给子类去实现,比如你又添加了一个子容器的实现,
* 那么这里通过你的实现类继承了ContextLoader去重写了loadParentContext,得到一个父容器
* 也就是说这里的父容器的父容器,可以设置到当前父容器的父容器中去
* 所以我的理解就是下面的if代码块中的两行代码的意思就是为了提供给子类去扩展的,
* 去扩展自己的实现的个性化需求的
*/
if (cwac.getParent() == null) {
// The context instance was injected without an explicit parent ->
// determine parent for root web application context, if any.
//这里是提供给子类去实现的,如果子类实现得到了一个新的容器,那么可以
//作为当前容器的父容器
ApplicationContext parent = loadParentContext(servletContext);
cwac.setParent(parent);
}
//这行代码大家都知道,就是配置和刷新webApplicationContext容器
//简单来说就是启动我们创建的容器
configureAndRefreshWebApplicationContext(cwac, servletContext);
}
}
//将当前创建的容器设置到servlet上下文中,也就是创建一个新的容器,作为父容器放置到servlet上下文中
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
//将当前创建的容器放置到当前线程的类加载器中
ClassLoader ccl = Thread.currentThread().getContextClassLoader();
if (ccl == ContextLoader.class.getClassLoader()) {
currentContext = this.context;
}
else if (ccl != null) {
currentContextPerThread.put(ccl, this.context);
}
if (logger.isInfoEnabled()) {
long elapsedTime = System.currentTimeMillis() - startTime;
logger.info("Root WebApplicationContext initialized in " + elapsedTime + " ms");
}
return this.context;
}
catch (RuntimeException | Error ex) {
logger.error("Context initialization failed", ex);
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex);
throw ex;
}
}
createWebApplicationContext
在这里插入代码片
protected WebApplicationContext createWebApplicationContext(ServletContext sc) {
//这里得到一个容器上下文类的calss对象,判断得到的类是否是ConfigurableWebApplicationContext
//如果不是ConfigurableWebApplicationContext则报错,从determineContextClass我们知道
//得到的类是XmlWebApplicationContext类,是继承了ConfigurableWebApplicationContext的
//所以这里是不满足下面的if条件的
Class<?> contextClass = determineContextClass(sc);
if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
throw new ApplicationContextException("Custom context class [" + contextClass.getName() +
"] is not of type [" + ConfigurableWebApplicationContext.class.getName() + "]");
}
//得到XmlWebApplicationContext的实例对象
return (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
}
configureAndRefreshWebApplicationContext
protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
// The application context id is still set to its original default value
// -> assign a more useful id based on available information
//这里是判断是否有配置一个容器的ID,如果没有就spring就生成一个
String idParam = sc.getInitParameter(CONTEXT_ID_PARAM);
if (idParam != null) {
wac.setId(idParam);
}
else {
// Generate default id...
wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
ObjectUtils.getDisplayString(sc.getContextPath()));
}
}
/**
* 这里将servlet的上下文添加到容器中, 这样的话,在容器中可以访问servlet中,也就是容器上下文相通 了
*/
wac.setServletContext(sc);
//这里是获取web.xml中的ContextLoadListener中配置的contextConfigLocation路径,也就是我们常看见的
/**
*classpath:spring-servlet.xml 文件,这里把获取到的配置文件的路径设置到容器中去
* 因为设置进去过后,在refresh中会去加载这个配置文件
*/
String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM);
if (configLocationParam != null) {
//将配置文件设置到上下文中
wac.setConfigLocation(configLocationParam);
}
// The wac environment's #initPropertySources will be called in any case when the context
// is refreshed; do it eagerly here to ensure servlet property sources are in place for
// use in any post-processing or initialization that occurs below prior to #refresh
//这里是获取环境变量信息,也就是spring中Environment对象,将系统的、项目的环境信息放入到这个对象中,然后在把servlet中的初始化对象放入对象中
ConfigurableEnvironment env = wac.getEnvironment();
if (env instanceof ConfigurableWebEnvironment) {
((ConfigurableWebEnvironment) env).initPropertySources(sc, null);
}
customizeContext(sc, wac);
//刷新容器,也就是spring中最核心的容器启动过程,这个
//比较复杂,之前已经全面分析过一次了,这里主要看下我们在web.xml中配置的文件是如何加载成一个一个的BeanDefinition的
/**
* 在refresh方法中的obtainFreshBeanFactory完成的对spring.xml中的classpath:spring.xml文件的解析,并且将所有的bean标签解析成一个
* 一个的BeanDefinition
* 下面的refresh方法调用完成过后,当前容器就启动完成了
*/
wac.refresh();
}
上面贴的代码就是spring在创建父容器的核心代码,代码的注释在贴的代码上已经写的非常详细了,其他我就不说了,我这里说下创建父容器的两个核心点,一个是刚刚说的创建父容器的一个扩展点,一个是创建的容器和一般比如spring boot的创建的容器的区别;
首先我们要明白的是这种方式是在xml的配置文件中配置了一个spring.xml的文件路径,然后加载的时候会去加载这个目录,具体的加载这个这个xml然后将bean标签解析成一个一个的BeanDefinition是在spring的最最核心的一个类AbstractApplicationContext中的refresh方法中的obtainFreshBeanFactory得到一个工厂的方法里面,如果是web 的方式,那么会调用AbstractRefreshableApplicationContext中的refreshBeanFactory去实现的bean标签解析成一个一个的BeanDefinition这里就不多说了。
再者就是在创建父容器过程中发现的一个扩展点,我们看源码的目的之一就是要发现源码中提供的扩展点,方便以后我们自己来扩展,在上面贴的代码中的initWebApplicationContext方法中代码片段如下:
这里会演示下如何利用这个扩展点
示例
我这里新建了一个工厂dev-201121,因为今天的日期是11月21号,难得去想名字了,我这里使用的是tomcat的内嵌版本,通过mvn来启动的,关于tomcat的容器概念前面已经说了,我这里只提如何利用tomcat的内嵌版本来启动,先来看pom:
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.2</version>
<configuration>
<path>/</path>
<port>8080</port>
</configuration>
</plugin>
然后在启动的窗口里面新增一个mvn启动
就可以了;现在我们启动一个spring mvc的简单项目,配置两个容器,spring的配置文件如下:
//父容器的配置,里面当然可以配置bean的标签,只是这里我没有配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">
//父容器只扫描我的service层的bean
<context:component-scan base-package="com.dev201121.service"/>
<mvc:annotation-driven />
</beans>
//子容器的配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">
//子容器只扫描我web的bean,这个一定要分开,当然不分开也不报错,但是。。。这个看你吧
<context:component-scan base-package="com.dev201121.web"/>
<mvc:annotation-driven />
</beans>
我只想说如果你不分容器来的话,就是整个项目中只有一个spring容器的话,那么你就 可以虽然怎么配置扫描路径,如果是分了容器的话,最后每个容器都不要有重复的bean,因为那样很乱,而且spring的单例池的概念是什么呢?就是进入单例池的对象在spring的整个生命周期中只存在一个对象,如果一个bean在每个bean的容器中都扫描进去了,那么在这个系统中就会存在几个实例对象,这既是浪费了空间,也毫无意义;我们都知道spring的getBean会在当前容器中找,如果找不到就去父容器找,如果你觉得这样性能有影响,但是我只想说这只是在启动阶段的=依赖注入阶段完成的时候,如果你实在忍受不了, 那么就不要启动那么多容器,就不要父容器 ,全面塞进一个容器就好了。既然用了父子容器就要按照父子容器的类型来存放不同类型的bean。
Controller:
@RestController
public class HelloController implements ApplicationContextAware {
private ApplicationContext applicationContext;
@RequestMapping("/hello.do")
public String hello(){
System.out.println("hello....");
return "ni hao ";
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
我们在15行打个断点,看下父子容器中的bean

可以看到当前的容器下面有个parent就是XmlWebApplicationContext,就是父容器,就是上面分析的源码中的父容器;
上面提到了spring在创建的过程中提供了一个扩展点,就是可以设置一个顶级的父容器,我们分析下如何来利用这扩展,其实非常简单,spring的ContextLoad中提供了一个方法可以让子类去重写,那么我只需要建立一个新的监听器,然后重写这个父类中的得到一个顶级的父类容器就可以了,所以我这里新建一个监听器:
```java
public class MyLinstener extends ContextLoaderListener {
@Override
protected ApplicationContext loadParentContext(ServletContext servletContext) {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext();
ac.register(AppConfig.class);com.dev201121.service
ac.refresh();
System.out.println("自定义的容器启动完毕,作为顶级父级容器来使用");
return ac;
}
}
在这个顶级的父容器中,我使用了注解的方式来扫描我指定的bean,上面的是通过web.xml配置的文件去扫描,而我这里使用的是注解方式,通过Appconfig来配置的:
@ComponentScan("com.dev201121.definitions")
public class AppConfig {
@Bean
public User1 user1(){
return new User1();
}
}
就是这个意思:
顶级容器:扫描的路径是:com.dev201121.definitions
第二级容器:扫描的路径:com.dev201121.service
controller容器扫描的路径:com.dev201121.web
所以这里我是把容器的每个层级需要存放的bean分类的,这样做的以后。我们来修改下web.xml的配置文件
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
<display-name>Archetype Created Web Application</display-name>
<listener>
<listener-class>com.dev201121.listeners.MyLinstener</listener-class>
<listener-class>com.dev201121.listeners.TestLinisten</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring.xml</param-value>
</context-param>
<servlet>
<servlet-name>dispartServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-servlet.xml</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>dispartServlet</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
</web-app>
把ContextLoadListener监听器换成我的MyLinstener就可以了,我们启动来看下
启动自定义的容器中肯定有我们创建的两个User类bean对象的。
子容器创建过程
知道了父容器的创建过程,那么子容器的创建过程也就非常简单了,和父容器的创建过程大同小异,我们上面说了其实子容器就是管理controller的,也就是web框架中的bean对象,也就是servlet上下文中的对象,而springmvc最重要的一个servlet就是DispatcherServlet,而这个servlet就是创建子容器的过程,我们大概看下这个servlet的类的继承关系:
其中右上边是servlet的,左上边到下面是spring实现的类,其中最重要的三个类是HttpServletBean->FrameworkServlet->DispatcherServlet,我们知道一个servlet调用的最核心有两个方法,一个是init方法,一个是service方法;service方法是只有在有请求过来的时候才会去调用的方法,而init方法如果不是懒加载的情况下启动servlet的过程中回去调用的,只要在servlet中设置了 load-on-startup 这个值只要是一个正数,也就是一个大于0的数字,那么servlet启动完成过后都会来调用,所以我们要看DispatcherServlet是如何创建的子容器,肯定是要来DispatcherServlet中来看init方法的,前面介绍过GerericServlet的接口,里面有两个init方法,一个是带参数的,一个是不带参数的,带参数的是GenericServlet实现的,就是拿到servlet配置的一些参数,而不带参数的init方法是提供给子类去实现的
所以在DispatcherServlet中,我们只需要去找init方法即可,而DispatcherServlet中的init方法是在父类HttpServletBean中实现的
HttpServletBean.init
public final void init() throws ServletException {
// Set bean properties from init parameters.
/**
* 下面这行代码就是得到这个servlet的初始化参数,就是init下面的初始化参数然后封装成一个PropertyValues
*/
PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
if (!pvs.isEmpty()) {
try {
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));
initBeanWrapper(bw);
bw.setPropertyValues(pvs, true);
}
catch (BeansException ex) {
if (logger.isErrorEnabled()) {
logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
}
throw ex;
}
}
// Let subclasses do whatever initialization they like.
// 这里就是创建子容器的过程,在子类中的FrameworkServlet中实现的
initServletBean();
}
这里的init的方法其实要完成的事情就是创建出子容器,然后将ContextLoadListener创建的父容器绑定到子容器中,然后去初始化spring mvc的核心组件,所以这里的initServletBean方法就是去创建容器和初始化spring mvc核心组件的过程,它是在FrameworkServlet中完成的
FrameworkServlet.initServletBean
/**
* Overridden method of {@link HttpServletBean}, invoked after any bean properties
* have been set. Creates this servlet's WebApplicationContext.
*
* 这个方法是创建子容器的,被看这个方法写了那么多,只有一行代码有用
* initWebApplicationContext,一看就知道是创建容器的,下面的
* initFrameworkServlet方法是一个空的实现,简单来说就是提供给子类去实现的
* 也就是说在当前容器加载完成过后,如果又需要加载点项目个性化的东西就可以重写这个父类中的方法
*/
@Override
protected final void initServletBean() throws ServletException {
getServletContext().log("Initializing Spring " + getClass().getSimpleName() + " '" + getServletName() + "'");
if (logger.isInfoEnabled()) {
logger.info("Initializing Servlet '" + getServletName() + "'");
}
long startTime = System.currentTimeMillis();
try {
//初始化创建一个子容器
this.webApplicationContext = initWebApplicationContext();
initFrameworkServlet();
}
catch (ServletException | RuntimeException ex) {
logger.error("Context initialization failed", ex);
throw ex;
}
if (logger.isDebugEnabled()) {
String value = this.enableLoggingRequestDetails ?
"shown which may lead to unsafe logging of potentially sensitive data" :
"masked to prevent unsafe logging of potentially sensitive data";
logger.debug("enableLoggingRequestDetails='" + this.enableLoggingRequestDetails +
"': request parameters and headers will be " + value);
}
if (logger.isInfoEnabled()) {
logger.info("Completed initialization in " + (System.currentTimeMillis() - startTime) + " ms");
}
}
initWebApplicationContext
protected WebApplicationContext initWebApplicationContext() {
/**
* 从servlet上下文中获取一个 WebApplicationContext.class.getName() + ".ROOT"为名字的容器,这个容器从名字就可以看出来是一个父容器
* 就是在监听器ContextLoadListener的回调方法中创建的一个父容器
*/
WebApplicationContext rootContext =
WebApplicationContextUtils.getWebApplicationContext(getServletContext());
WebApplicationContext wac = null;
//当前创建的子容器,如果到这里这个容器就已经不为空的,进入里面的逻辑
if (this.webApplicationContext != null) {
// A context instance was injected at construction time -> use it
wac = this.webApplicationContext;
if (wac instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
//如果容器不是活动的,也就是还没有启动,就进入下面的流程
if (!cwac.isActive()) {
// The context has not yet been refreshed -> provide services such as
// setting the parent context, setting the application context id, etc
//如果还没有父容器,就设置一个父容器到当前容器中
if (cwac.getParent() == null) {
// The context instance was injected without an explicit parent -> set
// the root application context (if any; may be null) as the parent
cwac.setParent(rootContext);
}
//最后启动容器
configureAndRefreshWebApplicationContext(cwac);
}
}
}
if (wac == null) {
// No context instance was injected at construction time -> see if one
// has been registered in the servlet context. If one exists, it is assumed
// that the parent context (if any) has already been set and that the
// user has performed any initialization such as setting the context id
//这里去找一个容器对象,如果是第一次肯定是空的
wac = findWebApplicationContext();
}
if (wac == null) {
// No context instance is defined for this servlet -> create a local one
//这里去创建子容器,然后绑定父容器
wac = createWebApplicationContext(rootContext);
}
if (!this.refreshEventReceived) {
// Either the context is not a ConfigurableApplicationContext with refresh
// support or the context injected at construction time had already been
// refreshed -> trigger initial onRefresh manually here.
synchronized (this.onRefreshMonitor) {
//调用了子类DispatcherServlet去初始化mvc的组件
onRefresh(wac);
}
}
if (this.publishContext) {
// Publish the context as a servlet context attribute.
String attrName = getServletContextAttributeName();
//最后将创建出来的容器对象放入到servlet上下文中
/**
* servlet上下文对象是在tomcat服务器中的context服务器中的
* 在contxt容器的生命周期内永远存活的,所以这样就将spring容器的生命周期交给了
* servlet上下文文容器中,而spring上下文也可以访问servlet上下文,servlet上下文也可以访问spring
* 上下文
*/
getServletContext().setAttribute(attrName, wac);
}
return wac;
}
createWebApplicationContext
protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) {
//得到当前(子容器)容器的class对象,这class对象肯定是ConfigurableWebApplicationContext的子类,这个class
//也是XmlWebApplicationContext
Class<?> contextClass = getContextClass();
if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
throw new ApplicationContextException(
"Fatal initialization error in servlet with name '" + getServletName() +
"': custom WebApplicationContext class [" + contextClass.getName() +
"] is not of type ConfigurableWebApplicationContext");
}
//得到一个可支配的web上下文容器对象
ConfigurableWebApplicationContext wac =
(ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
//设置环境变量对象
wac.setEnvironment(getEnvironment());
//将获取的父容器设置当前的容器的父容器,这样关系就绑定好了
wac.setParent(parent);
//将当前容器的spring配置文件路径获取到然后设置到当前容器的对象中,在容器refresh中回去AbstractRefreshableApplicationContext中加载xml中bean
String configLocation = getContextConfigLocation();
if (configLocation != null) {
wac.setConfigLocation(configLocation);
}
//和父容器创建的过程一样,就是配置和刷新web上下文工厂
configureAndRefreshWebApplicationContext(wac);
return wac;
}
configureAndRefreshWebApplicationContext
1.设置容器的ID;
2.添加servlet上下文到spring的上下文;
3.设置servlet的配置到spring上下文中;
4.添加命名空间;
5.添加一个监听器
6.提供了一个扩展
7.初始化监听器
8.启动容器
onRefresh
在创建子容器完成后,还要去初始化mvc的组件,就在onRefresh方法中去初始化的