Spring 5源码阅读系列(2):从web项目的启动说起

Spring 5源码阅读系列(2):从web项目的启动说起

  • 1. web项目发展简述
  • 2. 集成spring
  • 3. spring web启动
    • 3.1. ContextLoaderListener
    • 3.2. web应用上下文的初始化:initWebApplicationContext()
    • 3.3. 上下文的创建:createWebApplicationContext()
    • 3.4. web上下文的配置和刷新:configureAndRefreshWebApplicationContext

1. web项目发展简述

传统的web项目最早,要从当年的JSP Web项目说起:不少Java开发,就是从这里开始起步的。

其项目结构目录十分简单,特点鲜明:

  • src下放源代码;
  • webRoot下放web相关资源:静态资源,jsp,配置文件等;
  • 源代码中,各种写servlet
  • web.xml里各种配置,写一个servlet,配置一个mapping
    Spring 5源码阅读系列(2):从web项目的启动说起_第1张图片

随后,struts框架出现了,简化了部分web开发,同时还增强了很多web功能,成为当时web开发的首选框架;但其明显的缺点是:很笨重;很多功能,需要在web.xml需要配置一大堆东西;增强的功能也需要配置对应的xml文件;

这种的情况甚至在struts1发展struts2时,都没有得到很好的改善,反面严重了不少:
struts1时,需要配置各种ActionForm,struts2需要配置各种Action、validation xml、message-conversion xml;直到注解出现后,这种情况才得以适当改善。

struts几乎同一时期出现的spring框架,因其超强的封装,约定大于配置的理念,容器概念的引入等等原因,与struts相比,极大简化了web开发,与Hibernate一起,迅速成为当年最流行的三大框架:SSH

在当时,SSH项目的集成,成为每个Java开发必备的基本功底,以至于当时有面试题需要手写集成SSH的xml配置;现在来看,简直令人哭笑不得!

随着spring一发不可收拾,当年红遍大江南北的SSH框架,也基本被SSM取而代之了,成为了在spring boot红起来之前,web开发框架的不二之选。

不过,就算是SSM框架,也逃不过一道关卡:web项目集成spring


2. 集成spring

说起来,集成spring也没有什么难度,一句话就可以说清楚:

  • 在web.xml配置spring的ContextLoaderListener,指定它的spring配置文件;
  • 在spring配置文件中,增加对应的配置,包括:component-scan,sessionFactory等创世bean定义;

OK了,就这么就可以完成spring集成了!
这里贴上,网上找的、随便一篇介绍spring集成时,写的配置内容:

  • web.xml


    
    <listener>
        
        <listener-class>
        	org.springframework.web.context.ContextLoaderListener
        listener-class>
    listener>
    <context-param>
        
        <param-name>contextConfigLocationparam-name>
        <param-value>classpath:applicationContext.xmlparam-value>
    context-param>
  • applicationContext.xml
	

    
    <context:property-placeholder location="classpath:database.properties"/>

    
    <bean class="org.springframework.jdbc.datasource.DriverManagerDataSource"
    	id="dataSource">
        
        <property name="driverClassName" value="${db_driver}"/>
        <property name="url" value="${db_url}"/>
        <property name="username" value="${db_username}"/>
        <property name="password" value="${db_password}"/>
    bean>

    
    <tx:annotation-driven/>

	
    <bean class="org.mybatis.spring.SqlSessionFactoryBean" id="sqlSessionFactory">
        
        <property name="dataSource" ref="dataSource"/>
        
        <property name="configLocation" value="classpath:myBatis/mybatis-config.xml"/>
        
    bean>
    

其它配置什么的,我们暂时不用关心,到这里就够了。


现在我们来问自己几个问题:

  • 1、为什么这样配置后,就集成了spring?或者说,我们总说集成spring,我们集成了啥?
  • 2、spring是如何解读我们的配置信息,从而完成spring容器的引入,最终完成整个集成工作的?

其实这不好回答,因为说清楚,要讲很多关于spring底层的细节,能直接写一本书。
这里就暂时先不探讨其它深入的内容了,就只看spring web项目在启动时,大致是如何做到引入spring容器的。


3. spring web启动

在没有使用spring框架前,我们已经知道了web.xml的重要性;
在集成spring时,在web.xml中,又配置了spring包下的listener类,所以我们有理由相信:这个listener与spring的引入息息相关。

3.1. ContextLoaderListener

事实上,我们并没有猜错:来看一下这个类:

public class ContextLoaderListener extends ContextLoader implements ServletContextListener {

	public ContextLoaderListener() {
	}

	public ContextLoaderListener(WebApplicationContext context) {
		super(context);
	}

	@Override
	public void contextInitialized(ServletContextEvent event) {
		initWebApplicationContext(event.getServletContext());
	}

	@Override
	public void contextDestroyed(ServletContextEvent event) {
		closeWebApplicationContext(event.getServletContext());
		ContextCleanupListener.cleanupAttributes(event.getServletContext());
	}
}

因为这个Listener实现了javax.servlet包下的ServletContextListener接口,那么只要Servlet容器启动时,这个Listener就会收到启动事件通知(这是Servlet API规范中的规定;这里就跳过了,以后有机会再讨论这个知识点),即调用contextInitialized(ServletContextEvent event)方法,传入一个ServletContextEvent对象,进而调用initWebApplicationContext(ServletContext servletContext)方法,传入一个ServletContext对象——这个方法肯定要跟进去,要看看到底做了什么事情。

Note:在深入了解initWebApplicationContext(ServletContext servletContext)方法前,其实应该先了解下,这个Listener是如何被创建的。

这里也先不过多讨论,只需要知道一点:这里由Servlet容器,比如Tomcat,来创建的,需要阅读tomcat启动源码;
其基本流程是,容器在解析web.xml时,读取到元素,根据其属性class获取到类,通过反射机制实例化出来它的对象。

@Override
public void addListener(String className) {
	// ContextLoaderListener的创建
   try {
       if (context.getInstanceManager() != null) {
           Object obj = context.getInstanceManager().newInstance(className);

           if (!(obj instanceof EventListener)) {
               throw new IllegalArgumentException(sm.getString(
                       "applicationContext.addListener.iae.wrongType",
                       className));
           }

           EventListener listener = (EventListener) obj;
           addListener(listener);
       }
   } catch (InvocationTargetException e) {
       ExceptionUtils.handleThrowable(e.getCause());
       throw new IllegalArgumentException(sm.getString(
               "applicationContext.addListener.iae.cnfe", className),
               e);
   } catch (ReflectiveOperationException| NamingException e) {
       throw new IllegalArgumentException(sm.getString(
               "applicationContext.addListener.iae.cnfe", className),
               e);
   }

}

3.2. web应用上下文的初始化:initWebApplicationContext()

从这个方法名称的字面意义上来理解,ContextLoaderListener监听到ServletContext启动后,会初始化WebApplicationContext

分析initWebApplicationContext()方法代码,除去一些异常检测、日志打印后,该方法的主要逻辑如下:

public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
	//首次创建时,context必然为空:因为listener创建时,调用的是无参构造,后续也没有调用对context的setter方法;
	if (this.context == null) {
		this.context = createWebApplicationContext(servletContext);//目标方法1
	}
	
	//如果context是可配置的web上下文,则配置它后刷新之;
	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
	        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);
	        }
	        configureAndRefreshWebApplicationContext(cwac, servletContext);//目标方法2
	    }
	}
	servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
	
	//检查当前类加载器,是否与加载ContextLoader的类加载器是同一个;如果不是,存入上下文线程map中
	ClassLoader ccl = Thread.currentThread().getContextClassLoader();
	if (ccl == ContextLoader.class.getClassLoader()) {
	    currentContext = this.context;
	} else if (ccl != null) {
	    currentContextPerThread.put(ccl, this.context);
	}
	
	return this.context;	
}

代码中,比较关键的两个方法,就是我们需要深入分析的目标方法;
针对目标方法1,我们要分析清楚:servlet容器,比如tomcat,是如何创建context的?


3.3. 上下文的创建:createWebApplicationContext()

查看目标方法1的代码:

protected WebApplicationContext createWebApplicationContext(ServletContext sc) {
	//1. 判断使用什么上下文类
	Class<?> contextClass = determineContextClass(sc);
	if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
		throw new ApplicationContextException("Custom context class [" + contextClass.getName() +
				"] is not of type [" + ConfigurableWebApplicationContext.class.getName() + "]");
	}
	
	//2. 调用反射创建上下文实例对象,并转换成ConfigurableWebApplicationContext
	return (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
}

发现容器会先尝试判断使用什么上下文类来实例化上下文对象;跟进去,看看容器是如何判断的:

	protected Class<?> determineContextClass(ServletContext servletContext) {
		// 1. 如果容器配置了init-param: contextClass,则尝试使用该属性值代表的类
		String contextClassName = servletContext.getInitParameter(CONTEXT_CLASS_PARAM);
		if (contextClassName != null) {
			try {
				return ClassUtils.forName(contextClassName, ClassUtils.getDefaultClassLoader());
			}
			catch (ClassNotFoundException ex) {
				throw new ApplicationContextException(
						"Failed to load custom context class [" + contextClassName + "]", ex);
			}
		}
		
		//2. 如果没有配置init-param: contextClass,则取默认策略中定义的类
		else {
			contextClassName = defaultStrategies.getProperty(WebApplicationContext.class.getName());
			try {
				return ClassUtils.forName(contextClassName, ContextLoader.class.getClassLoader());
			}
			catch (ClassNotFoundException ex) {
				throw new ApplicationContextException(
						"Failed to load default context class [" + contextClassName + "]", ex);
			}
		}
	}

可以看出,容器的判断依据分两步:
1)有没有配置init-param: context;如果用户在web.xml中配置了启动参数,contextClass,容器会使用这个配置的类,来作为web容器的上下文:
这种情况,一般是用户自定义context实现其特殊功能;下面是一个配置示例:

<context-param>
	<param-name>contextClassparam-name>
	<param-value>com.springstudy.webproject.context.MyApplicationContextparam-value>
context-param>

但多数情况下,用户不会自己提供一个上下文类,所以走第2步:

2)使用默认策略定义的类,作为上下文类;
但是,默认策略是什么呢?我们追踪下defaultStrategies,发现它在这个时候写入属性值的:

	static {
		//看一下,属性文件的路径是什么: DEFAULT_STRATEGIES_PATH = ”ContextLoader.properties"
		ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, ContextLoader.class);
		defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);
	}

通过上面代码可知,原来默认策略是读取 ContextLoader 类路径下的 Context.properties 文件,根据里面的内容来决定的:

该文件通过ClassPathResource来加载,且指定了加载时的类——ContextLoader,表明这个属性文件在这个类的相对路径下。
我们找到这个属性文件,看下它的内容:
Spring 5源码阅读系列(2):从web项目的启动说起_第2张图片

它里面的内容就一个映射,写明了该加载哪个 ApplicationContext 来作为上下文的实现类:

org.springframework.web.context.WebApplicationContext=\
org.springframework.web.context.support.XmlWebApplicationContext

所以,回到前面说的默认策略,整理下思路后,我们就清楚容器据说的默认就是什么了:
根据Context.properties配置的org.springframework.web.context.WebApplicationContext所映射的类名,来作为上下文类,并反射创建这个类,也就是XmlWebApplicationContext

绕了这么一大圈,终于看到我们熟悉的类了:XmlWebApplicationContext
所以,后面调用一系列的方法中,所使用到的 web上下文就是它了;


3.4. web上下文的配置和刷新:configureAndRefreshWebApplicationContext

查看目标方法2的代码,看看web上下文在配置和刷新时,到底做了哪些事情:

protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
		if (ObjectUtils.identityToString(wac).equals(wac.getId())) {

			String idParam = sc.getInitParameter(CONTEXT_ID_PARAM);
			if (idParam != null) {
				wac.setId(idParam);
			}
			else {
				wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
						ObjectUtils.getDisplayString(sc.getContextPath()));
			}
		}

		wac.setServletContext(sc);
		String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM);
		if (configLocationParam != null) {
			wac.setConfigLocation(configLocationParam);
		}

		ConfigurableEnvironment env = wac.getEnvironment();
		if (env instanceof ConfigurableWebEnvironment) {
			((ConfigurableWebEnvironment) env).initPropertySources(sc, null);
		}

		customizeContext(sc, wac);
		wac.refresh();
	}

可见,在上述方法中,会对web上下文做了这些配置和操作:

  • setId()
  • setServletContext()
  • setConfigLocation()
  • customizeContext()
  • refresh()

其中,后面两个方法需要特别注意:
1)customizeContext(): 是否需要启动应用上下文initializer

protected void customizeContext(ServletContext sc, ConfigurableWebApplicationContext wac) {
		//1. 如果配置了init-param: globalInitializerClasses 或者 contextInitializerClasses,则加载这些initializer类
		List<Class<ApplicationContextInitializer<ConfigurableApplicationContext>>> initializerClasses =
				determineContextInitializerClasses(sc);

		for (Class<ApplicationContextInitializer<ConfigurableApplicationContext>> initializerClass : initializerClasses) {
			Class<?> initializerContextClass =
					GenericTypeResolver.resolveTypeArgument(initializerClass, ApplicationContextInitializer.class);
			if (initializerContextClass != null && !initializerContextClass.isInstance(wac)) {
				throw new ApplicationContextException(String.format(
						"Could not apply context initializer [%s] since its generic parameter [%s] " +
						"is not assignable from the type of application context used by this " +
						"context loader: [%s]", initializerClass.getName(), initializerContextClass.getName(),
						wac.getClass().getName()));
			}
			this.contextInitializers.add(BeanUtils.instantiateClass(initializerClass));
		}

		//2. 如果存在initializer类,则初始化之
		AnnotationAwareOrderComparator.sort(this.contextInitializers);
		for (ApplicationContextInitializer<ConfigurableApplicationContext> initializer : this.contextInitializers) {
			initializer.initialize(wac);
		}
	}

2)refresh():
这个方法是个接口方法,需要找到对应的实现类;但因为我们之前的分析已知,wac对象实例是XmlWebApplicationContext,所以直接看XmlWebApplicationContext.refresh()即可:

public class XmlWebApplicationContext extends AbstractRefreshableWebApplicationContext {
	...//省略属性
	
	protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException {
	...//省略方法内容
	}

	protected void initBeanDefinitionReader(XmlBeanDefinitionReader beanDefinitionReader) {
	}

	protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws IOException {
		String[] configLocations = getConfigLocations();
		if (configLocations != null) {
			for (String configLocation : configLocations) {
				reader.loadBeanDefinitions(configLocation);
			}
		}
	}

	protected String[] getDefaultConfigLocations() {
		if (getNamespace() != null) {
			return new String[] {DEFAULT_CONFIG_LOCATION_PREFIX + getNamespace() + DEFAULT_CONFIG_LOCATION_SUFFIX};
		}
		else {
			return new String[] {DEFAULT_CONFIG_LOCATION};
		}
	}
}

但是,XmlWebApplicationContext并没有实现refresh方法,所以需要看其父类的实现。

结果它的父类:AbstractRefreshableWebApplicationContext:也没有实现这个方法,只能再向上找它的父类;如果还没有,继续再向上找。

这样一直找,直到找到``AbstractApplicationContext才实现了refresh方法:

public void refresh() throws BeansException, IllegalStateException {
		synchronized (this.startupShutdownMonitor) {
			// Prepare this context for refreshing.
			prepareRefresh();

			// Tell the subclass to refresh the internal bean factory.
			ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

			// Prepare the bean factory for use in this context.
			prepareBeanFactory(beanFactory);

			try {
				// Allows post-processing of the bean factory in context subclasses.
				postProcessBeanFactory(beanFactory);

				// Invoke factory processors registered as beans in the context.
				invokeBeanFactoryPostProcessors(beanFactory);

				// Register bean processors that intercept bean creation.
				registerBeanPostProcessors(beanFactory);

				// Initialize message source for this context.
				initMessageSource();

				// Initialize event multicaster for this context.
				initApplicationEventMulticaster();

				// Initialize other special beans in specific context subclasses.
				onRefresh();

				// Check for listener beans and register them.
				registerListeners();

				// Instantiate all remaining (non-lazy-init) singletons.
				finishBeanFactoryInitialization(beanFactory);

				// Last step: publish corresponding event.
				finishRefresh();
			}

			catch (BeansException ex) {
				if (logger.isWarnEnabled()) {
					logger.warn("Exception encountered during context initialization - " +
							"cancelling refresh attempt: " + ex);
				}

				// Destroy already created singletons to avoid dangling resources.
				destroyBeans();

				// Reset 'active' flag.
				cancelRefresh(ex);

				// Propagate exception to caller.
				throw ex;
			}

			finally {
				// Reset common introspection caches in Spring's core, since we
				// might not ever need metadata for singleton beans anymore...
				resetCommonCaches();
			}
		}
	}

这个refresh方法就不得了,做了相当多的事情啊,也就是它完成了spring容器的初始化、bean扫描等等;
源码追到这里,终于看到与spring容器操作相关的部分。

只是这部分的代码这么多,也相当核心,看懂也非常难,反正今天是没法说清楚这个事情,我们再在后面分章节继续读吧!

你可能感兴趣的:(源码阅读)