前言
最近在编写Spring相关博文的时候,发现有不少小伙伴对口头上经常说到的Spring容器、父子容器等等概念,既熟悉,又默认。大体知道它是干啥的,但是却有不太能知道所以然
因此本文自己也本着一个学习的态度,主要介绍Spring容器(父子容器)的启动过程。由于我们有web.xml配置文件的方式以及这里讲到过的全注解驱动的方式,因此本文都分开来讲述。
备注:本文讲述不包括Spring Boot中容器初始化的过程,这个在后面专讲Spring Boot的时候会着重讲解,敬请关注
当ContextLoaderListener和DispatcherServlet一起使用时, ContextLoaderListener 先创建一个根applicationContext,然后DispatcherSerlvet创建一个子applicationContext并且绑定到根applicationContext
基于注解驱动方式
按照这篇博文搭建的项目环境 【小家Spring】Spring注解驱动开发—Servlet 3.0整合Spring MVC(不使用web.xml部署描述符,全注解驱动) debug启动项目:
可以发现,只有我自己一个实现类MyWebAppInitializer
来配置当前的web环境。从这句代码可以看出,它只处理实体类,接口和抽象类一概不管:
if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) && WebApplicationInitializer.class.isAssignableFrom(waiClass)) { try { initializers.add((WebApplicationInitializer) ReflectionUtils.accessibleConstructor(waiClass).newInstance()); } catch (Throwable ex) { throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex); } } ... //排序后,循环调用onStartup方法 进行初始化 AnnotationAwareOrderComparator.sort(initializers); for (WebApplicationInitializer initializer : initializers) { initializer.onStartup(servletContext); }
接下来看看onStart()方法的实现AbstractDispatcherServletInitializer#onStartup
:
@Override public void onStartup(ServletContext servletContext) throws ServletException { super.onStartup(servletContext); registerDispatcherServlet(servletContext); } super如下: @Override public void onStartup(ServletContext servletContext) throws ServletException { registerContextLoaderListener(servletContext); }
到此我们发现我们注解驱动,和我们的web.xml驱动可以说就一样了。分两步了:
1.registerContextLoaderListener(servletContext):注册ContextLoaderListener监听器,让它去初始化Spring父容器
2.registerDispatcherServlet(servletContext);注册DispatcherServlet,让它去初始化Spring MVC的子容器
protected void registerContextLoaderListener(ServletContext servletContext) { WebApplicationContext rootAppContext = createRootApplicationContext(); if (rootAppContext != null) { // 创建listener 并且把已经创建好的容器放进去 ContextLoaderListener listener = new ContextLoaderListener(rootAppContext); //放入监听器需要的一些上下文,此处木有。一般都为null即可~~~。若有需要(自己定制),子类复写此方法即可 listener.setContextInitializers(getRootApplicationContextInitializers()); // 把监听器加入进来 这样该监听器就能监听ServletContext了,并且执行contextInitialized方法 servletContext.addListener(listener); } }
createRootApplicationContext
:如下,创建了一个AnnotationConfigWebApplicationContext
并且把配置文件注册进去了
@Override @Nullable //Spring告诉我们,这个是允许返回null的,也就是说是允许我们返回null的,后面会专门针对这里如果返回null,后面会是怎么样的流程的一个说明 protected WebApplicationContext createRootApplicationContext() { Class<?>[] configClasses = getRootConfigClasses(); if (!ObjectUtils.isEmpty(configClasses)) { AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); //配置文件可以有多个 会以累加的形式添加进去 context.register(configClasses); return context; } else { return null; } }
继续往下走:执行registerDispatcherServlet
protected void registerDispatcherServlet(ServletContext servletContext) { //Servlet名称 一般用系统默认的即可,否则自己复写此方法也成 String servletName = getServletName(); Assert.hasLength(servletName, "getServletName() must not return null or empty"); //创建web的子容易。创建的代码和上面差不多,也是使用调用者提供的配置文件,创建AnnotationConfigWebApplicationContext. 备注:此处不可能为null哦 WebApplicationContext servletAppContext = createServletApplicationContext(); Assert.notNull(servletAppContext, "createServletApplicationContext() must not return null"); //创建DispatcherServlet,并且把子容器传进去了。其实就是new一个出来,最后加到容器里,就能够执行一些init初始化方法了~ FrameworkServlet dispatcherServlet = createDispatcherServlet(servletAppContext); Assert.notNull(dispatcherServlet, "createDispatcherServlet(WebApplicationContext) must not return null"); //同样的 getServletApplicationContextInitializers()一般也为null即可 dispatcherServlet.setContextInitializers(getServletApplicationContextInitializers()); //注册servlet到web容器里面,这样就可以接收请求了 ServletRegistration.Dynamic registration = servletContext.addServlet(servletName, dispatcherServlet); if (registration == null) { throw new IllegalStateException("Failed to register servlet with name '" + servletName + "'. " + "Check if there is another servlet registered under the same name."); } //1表示立马执行哦,没有第一次惩罚了 registration.setLoadOnStartup(1); registration.addMapping(getServletMappings()); //调用者必须实现 registration.setAsyncSupported(isAsyncSupported()); //默认就是开启了支持异步的 //处理自定义的Filter进来,一般我们Filter不这么加进来,而是自己@WebFilter,或者借助Spring, 备注:这里添加进来的Filter都仅仅只拦截过滤上面注册的dispatchServlet Filter[] filters = getServletFilters(); if (!ObjectUtils.isEmpty(filters)) { for (Filter filter : filters) { registerServletFilter(servletContext, filter); } } //这个很清楚:调用者若相对dispatcherServlet有自己更个性化的参数设置,复写此方法即可 customizeRegistration(registration); }
然后继续执行,就来到了ContextLoaderListener#contextInitialized执行此监听器的初始化方法(注意:到了此处,就和web.xml方式一模一样了)
但是不一样的是,注解驱动的此时候,我们的ContextLoaderListener对象已经持有WebApplicationContext的引用了(但是还没有放进ServletContext里面去,需要注意),所以会稍微有点不一样。 注意源码中,我删除掉了一些日志语句。。。
public WebApplicationContext initWebApplicationContext(ServletContext servletContext) { // 虽然注解驱动传进来的监听器对象持有WebApplicationContext的引用,但是并没有放进ServletContext容器哦 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!"); } long startTime = System.currentTimeMillis(); try { // 这句特别重要,兼容了web.xml的方式以及注解驱动的方式。本文中是注解驱动的方式,所以此处不会null。下面讲解web.xml的方式的时候,我再会去详细讲解createWebApplicationContext(servletContext)这个方法~~~ if (this.context == null) { this.context = createWebApplicationContext(servletContext); } //从上图可以看出:XmlWebApplicationContext(xml驱动)和AnnotationConfigWebApplicationContext(注解驱动)都是复合的,都会进来 if (this.context instanceof ConfigurableWebApplicationContext) { ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context; //一般来说刚创建的context并没有处于激活状态,所以会进来完善一些更多的容器信息。比如刷新容器等等 if (!cwac.isActive()) { if (cwac.getParent() == null) { //在web.xml中配置了<context-param>的parentContextKey才会指定父级应用(或者我们自己复写此方法) 绝大多数情况下,Spring容器不用再给设置父容器 ApplicationContext parent = loadParentContext(servletContext); cwac.setParent(parent); } //读取相应的配置并且刷新context对象 这一步就极其重要了,因为刷新容器做了太多的事,属于容器的最最最核心逻辑(详解且见下问分解) configureAndRefreshWebApplicationContext(cwac, servletContext); } } //放进ServletContext上下文,避免再次被初始化,也让我们能更加方便的获取到容器 servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context); //此处把容器和当前线程绑定,public static WebApplicationContext getCurrentWebApplicationContext()这样就可以更加方便得得到容器.类为:ContextLoader ClassLoader ccl = Thread.currentThread().getContextClassLoader(); if (ccl == ContextLoader.class.getClassLoader()) { currentContext = this.context; } else if (ccl != null) { currentContextPerThread.put(ccl, this.context); } }
下面介绍最重要的一个方法:ContextLoader#configureAndRefreshWebApplicationContext
protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc){ //一般此处为真,给ApplicationContext设置一个id if (ObjectUtils.identityToString(wac).equals(wac.getId())) { //获取servletContext中的contextId属性 contextId,可在web.xml里配置,一般也不用配置,采用else里的默认值即可 String idParam = sc.getInitParameter(CONTEXT_ID_PARAM); if (idParam != null) { //存在则设为指定的id名 wac.setId(idParam); } else { // 生成默认id... 一般为org.springframework.web.context.WebApplicationContext:${contextPath} wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX + ObjectUtils.getDisplayString(sc.getContextPath())); } } //让容器关联上servlet上下文 wac.setServletContext(sc); //读取contextConfigLocation属性(在web.xml配置,但是注解驱动里没有,因此为null) String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM); if (configLocationParam != null) { //设置指定的spring文件所在地,支持classpath前缀并多文件,以,;为分隔符 wac.setConfigLocation(configLocationParam); } //这里有一个注意的地方,ConfigurableEnvironment生成的地方 //====wac.setConfigLocation(configLocationParam); 时根据 configLocationParam设置配置参数路径时就会初始化StandardServletEnvironment(ConfigurableEnvironment的子类) //StandardServletEnvironment符合条件,因此会执行initPropertySources方法。只与此方法的作用,后面再有相关文章详解 ConfigurableEnvironment env = wac.getEnvironment(); if (env instanceof ConfigurableWebEnvironment) { ((ConfigurableWebEnvironment) env).initPropertySources(sc, null); } //检查web.xml是否有一些其余初始化类的配置,极大多数情况都不需要,所以粗暴理解为没什么卵用 customizeContext(sc, wac); //容器的核心方法,也是最难的一个方法,这个在Spring容器详解中,会继续降到此方法 //这里先理解为就是初始化容器,比如加载bean、拦截器、各种处理器的操作就够了~(也是最耗时的一步操作) wac.refresh(); }
该方法完成之后,看到控制台log日志:
Root WebApplicationContext: initialization completed in 75383 ms
就证明Spring根容器就初始化完成了。
初始化DispatcherServlet,web子容器
由于设置了registration.setLoadOnStartup(1); 在容器启动完成后就调用servlet的init() DispatcherServlet 继承FrameworkServlet继承HttpServletBean继承 HttpServlet。
在HttpServletBean实现了init():
这里先科普一下Servlet初始化的四大步骤:
1.Servlet容器加载Servlet类,把类的.class文件中的数据读到内存中;
2.Servlet容器中创建一个ServletConfig对象。该对象中包含了Servlet的初始化配置信息;
3.Servlet容器创建一个Servlet对象(我们也可以手动new,然后手动添加进去);
4.Servlet容器调用Servlet对象的init()方法进行初始化。
@Override public final void init() throws ServletException { // 把servlet的初始化参数封装进来... PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties); //这里面我们并没有给此Servlet初始化的一些参数,所以此处为空,为false //若进来了,可以看到里面会做一些处理:将这个DispatcherServlet转换成一个BeanWrapper对象,从而能够以spring的方式来对初始化参数的值进行注入。这些属性如contextConfigLocation、namespace等等。 //同时注册一个属性编辑器,一旦在属性注入的时候遇到Resource类型的属性就会使用ResourceEditor去解析。再留一个initBeanWrapper(bw)方法给子类覆盖,让子类处真正执行BeanWrapper的属性注入工作。 //但是HttpServletBean的子类FrameworkServlet和DispatcherServlet都没有覆盖其initBeanWrapper(bw)方法,所以创建的BeanWrapper对象没有任何作用。 //备注:此部分把当前Servlet封装成一个BeanWrapper在把它交给Spring管理部分非常重要,比如后续我们讲到SpringBoot源码的时候,会看出来这部分代码的重要性了。。。 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) { throw ex; } } // Let subclasses do whatever initialization they like. // 从官方注解也能读懂。它把这个init方法给final掉了,然后开了这个口,子类可以根据自己的需要,在初始化的的时候可以复写这个方法,而不再是init方法了~ initServletBean(); }
因此我们只需要再看看initServletBean()
方法的实现即可,它是由FrameworkServlet
去实现的:
@Override protected final void initServletBean() throws ServletException { long startTime = System.currentTimeMillis(); try { // 这是重点,开始初始化这个子容器了 this.webApplicationContext = initWebApplicationContext(); //继续留一个口,给子类去复写初始化所需要的操作 一般都为空实现即可,除非自己要复写DispatcherServlet,做自己需要做的事 initFrameworkServlet(); } //当我们看到这句日志,就能知道dispatcherServlet已经初始化完成,web子容器也就初始化完成了 if (this.logger.isInfoEnabled()) { long elapsedTime = System.currentTimeMillis() - startTime; this.logger.info("FrameworkServlet '" + getServletName() + "': initialization completed in " + elapsedTime + " ms"); } }