【springMVC源码解读系列】(一、项目启动:创建web应用容器,设置父容器)

本文深入剖析Spring MVC的启动过程,从配置DispatcherServlet到初始化WebApplicationContext,揭示了委派模式和父子容器的设计思想。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

源码注释码云地址:spring源码解读地址:https://gitee.com/oushiyou/spring-framework-5.2.8.RELEASE

这里首先附上一张整体流程图,以供解读的时候可以进行参考:
在这里插入图片描述
精华提取:java多态性、委派模式、父子容器

我们在使用springMVC的时候,通常的做法是在web.xml中配置DispatcherServlet, 然后传入springMVC的配置文件位置。比如:

	<servlet>
        <servlet-name>springmvc</servlet-name>
        <servlet-class>
            org.springframework.web.servlet.DispatcherServlet
        </servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:spring-mvc.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>springmvc</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

项目在启动的时候,会对我们配置的Servlet进行初始化,然后调用其init方法,并且将我们配置的init-param初始化参数传递给ServletConfig。

如上配置:我们配置的Servlet是org.springframework.web.servlet.DispatcherServlet,
那么就会调用其init方法进行初始化servlet。

由于DispatcherServlet没有init方法,根据java多态的特性,那么他们去调用父类的init方法。
–> DispatcherServlet的直接父类是:FrameworkServlet,FrameworkServlet类中也没有init方法 --> FrameworkServlet的直接父类是:HttpServletBean --> HttpServletBean类中有init方法,所以最终调用的是: HttpServletBean的init()。

此方法主要是设置一些配置参数到当前Servlet的属性上,以便后续获取,
比如我们配置的contextConfigLocation,他就会赋值到contextConfigLocationcontextConfigLocation属性上。

这里利用了委派模式,将初始化容器委派给了FrameworkServlet来实现,将其职责进行拆分,互不影响和干扰。

/**
	 * 二、将配置参数映射到该servlet的bean属性上,并调用子类初始化。
	 * 具体做初始化工作的是{@link HttpServletBean#initServletBean()}方法
	 * 而此类的initServletBean方法是一个空的方法,所有初始化bean的工作交给其子类做的:{@link FrameworkServlet#initServletBean()}
	 *
	 */
	@Override
	public final void init() throws ServletException {

		/**
		 * 从init参数设置bean属性。
		 * 将初始化参数设置进入{@link org.springframework.beans.MutablePropertyValues }中
		 */
		PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
		// 如果PropertyValues不为空:当前环境中有key-value的配置
		if (!pvs.isEmpty()) {
			try {
				// 获取HttpServletBean的BeanWrapper,以JavaBeans样式访问属性。
				BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
				// 资源加载器
				ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
				// 向HttpServletBean注册一个ResourceEditor
				// PropertyEditor:主要方法有四个
				// void setValue(Object value); 设置属性值
				// Object getValue(); 获取属性值
				// String getAsText(); 把属性值转换成 String
				// void setAsText(String text); 把 String 转换成属性值
				// 而 Java 也为我们提供了一个默认的实现类 java.beans.PropertyEditorSupport
				bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));
				// 目前此方法是空的方法,提供给子类进行扩展的,如果我们想要扩展,可以通过这里来进行扩展
				// todo 可以使用自定义编辑器初始化此HttpServletBean的BeanWrapper。
				initBeanWrapper(bw);
				// 设置初始化参数到configLocation中
				bw.setPropertyValues(pvs, true);
			}
			catch (BeansException ex) {
				if (logger.isErrorEnabled()) {
					logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
				}
				throw ex;
			}
		}

		/**
		 * 这里的具体实现是{@link org.springframework.web.servlet.FrameworkServlet#initServletBean()}
		 */
		initServletBean();
	}

然后我们看FrameworkServletinitServletBean方法:此方法是HttpServletBean 委派下来初始化springMVC容器的,此方法中,主要还是交给initWebApplicationContext方法。
这个方法主要就是创建当前的web容器,在创建的时候,尝试获取到父容器,也就是spring容器,如果存在,那么将其设置为parent父容器。

/**
	 * 三、{@link HttpServletBean9}的重写方法,在设置任何bean属性后调用。创建此Servlet的WebApplicationContext。
	 * 在此处设置父容器的(如果父容器存在的话),
	 */
	@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 {
			// 初始化并发布此Servlet的WebApplicationContext(这里的容器为子容器)。
			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
获取父容器 --> 创建当前容器 --> 判断我们是否配置了springMVC容器的全限定类名,如果没有指定,那么使用默认的:XmlWebApplicationContext --> 反射实例化容器对象 --> 设置父容器 --> 刷新上下文

protected WebApplicationContext initWebApplicationContext() {
		// 获取到父容器,父容器是springContextLoaderListener实现ServletContextListener监听的时候创建的,显然,ServletContextListener监听是在此之前的。
		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);
				}
			}
		}
		// 如果为null,则尝试查找一下当前容器
		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
			// 在构建时没有注入上下文实例
			// 看看是否已经在servlet上下文中注册了一个。如果存在,则假定父上下文(如果有的话)已经设置,
			// 并且用户已经执行了任何初始化,比如设置上下文id
			wac = findWebApplicationContext();
		}
		// 如果查找不到,那么就进行创建:很显然,初始化的时候得从这里创建的
		if (wac == null) {
			// 没有为这个servlet定义上下文实例 -> 创建一个本地上下文, 然后进行刷新
			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) {
				onRefresh(wac);
			}
		}

		if (this.publishContext) {
			// Publish the context as a servlet context attribute.
			String attrName = getServletContextAttributeName();
			getServletContext().setAttribute(attrName, wac);
		}

		return wac;
	}
protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) {
		// 获取上下文类型,默认是:org.springframework.web.context.support.XmlWebApplicationContext
		Class<?> contextClass = getContextClass();
		// 这里判断传入的上下文类是否实现了ConfigurableWebApplicationContext接口,如果没有实现,那么就会报错
		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");
		}
		// 实例化
		ConfigurableWebApplicationContext wac =
				(ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
		wac.setEnvironment(getEnvironment());
		// 设置父容器
		wac.setParent(parent);
		// 设置本地配置文件的位置
		String configLocation = getContextConfigLocation();
		if (configLocation != null) {
			wac.setConfigLocation(configLocation);
		}
		// 配置并且刷新当前容器上下文
		configureAndRefreshWebApplicationContext(wac);

		return wac;
	}
protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) {
		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
			if (this.contextId != null) {
				wac.setId(this.contextId);
			}
			else {
				// Generate default id...
				wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
						ObjectUtils.getDisplayString(getServletContext().getContextPath()) + '/' + getServletName());
			}
		}

		wac.setServletContext(getServletContext());
		wac.setServletConfig(getServletConfig());
		wac.setNamespace(getNamespace());
		wac.addApplicationListener(new SourceFilteringListener(wac, new ContextRefreshListener()));

		// 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
		ConfigurableEnvironment env = wac.getEnvironment();
		if (env instanceof ConfigurableWebEnvironment) {
			((ConfigurableWebEnvironment) env).initPropertySources(getServletContext(), getServletConfig());
		}

		postProcessWebApplicationContext(wac);
		// 应用初始化器:调用自定义初始化器
		applyInitializers(wac);
		// 刷新上下文
		wac.refresh();
	}

从整个启动流程来看,虽然我们配置的是DispatcherServlet,但是在启动的时候,他什么都没有做,而是交个其两个父类来进行实现的,这种委派这是模式的思想很值得我们进行学习。而他是作为请求中央调度器的,后面我会深度剖析DispatcherServlet以及其使用到的一些设计模式。

其中讲到了将spring的容器作为父容器,这里面蕴含着很高的设计思想,后面我也会深度讲解其父子容器的作用,以及做此设计解决的问题和对项目有着什么样的扩展性。

如果你也喜欢源码,不防点赞评论关注一套流程走完,相互学习吧!

一个相信努力就会有结果的程序员,以兴趣驱动技术!         ------ CoderOu

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值