传统的web项目最早,要从当年的JSP Web项目说起:不少Java开发,就是从这里开始起步的。
其项目结构目录十分简单,特点鲜明:
随后,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
说起来,集成spring也没有什么难度,一句话就可以说清楚:
OK了,就这么就可以完成spring集成了!
这里贴上,网上找的、随便一篇介绍spring集成时,写的配置内容:
<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>
<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>
其它配置什么的,我们暂时不用关心,到这里就够了。
现在我们来问自己几个问题:
其实这不好回答,因为说清楚,要讲很多关于spring底层的细节,能直接写一本书。
这里就暂时先不探讨其它深入的内容了,就只看spring web项目在启动时,大致是如何做到引入spring容器的。
在没有使用spring框架前,我们已经知道了web.xml的重要性;
在集成spring时,在web.xml中,又配置了spring包下的listener类,所以我们有理由相信:这个listener与spring的引入息息相关。
事实上,我们并没有猜错:来看一下这个类:
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); } }
从这个方法名称的字面意义上来理解,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的?
查看目标方法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
,表明这个属性文件在这个类的相对路径下。
我们找到这个属性文件,看下它的内容:
它里面的内容就一个映射,写明了该加载哪个 ApplicationContext 来作为上下文的实现类:
org.springframework.web.context.WebApplicationContext=\
org.springframework.web.context.support.XmlWebApplicationContext
所以,回到前面说的默认策略,整理下思路后,我们就清楚容器据说的默认就是什么了:
根据Context.properties
配置的org.springframework.web.context.WebApplicationContext
所映射的类名,来作为上下文类,并反射创建这个类,也就是XmlWebApplicationContext
。
绕了这么一大圈,终于看到我们熟悉的类了:XmlWebApplicationContext
所以,后面调用一系列的方法中,所使用到的 web上下文就是它了;
查看目标方法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上下文做了这些配置和操作:
其中,后面两个方法需要特别注意:
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容器操作相关的部分。
只是这部分的代码这么多,也相当核心,看懂也非常难,反正今天是没法说清楚这个事情,我们再在后面分章节继续读吧!