【小家Spring】Spring容器(含父子容器)的启动过程源码级别分析(含web.xml启动以及全注解驱动,和ContextLoader源码分析)(上)

本文涉及的产品
容器镜像服务 ACR,镜像仓库100个 不限时长
简介: 【小家Spring】Spring容器(含父子容器)的启动过程源码级别分析(含web.xml启动以及全注解驱动,和ContextLoader源码分析)(上)

前言


最近在编写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启动项目:


image.png


可以发现,只有我自己一个实现类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里面去,需要注意),所以会稍微有点不一样。 注意源码中,我删除掉了一些日志语句。。。

image.png


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():


image.png





这里先科普一下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");
    }
  }



相关文章
|
3月前
|
Java 测试技术 数据库
说一说 SpringBoot 整合 Junit5 常用注解
我是小假 期待与你的下一次相遇 ~
|
6月前
|
Java Spring
Spring Boot的核心注解是哪个?他由哪几个注解组成的?
Spring Boot的核心注解是@SpringBootApplication , 他由几个注解组成 : ● @SpringBootConfiguration: 组合了- @Configuration注解,实现配置文件的功能; ● @EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选项 ● @ComponentScan:Spring组件扫描
|
5月前
|
人工智能 缓存 自然语言处理
保姆级Spring AI 注解式开发教程,你肯定想不到还能这么玩!
这是一份详尽的 Spring AI 注解式开发教程,涵盖从环境配置到高级功能的全流程。Spring AI 是 Spring 框架中的一个模块,支持 NLP、CV 等 AI 任务。通过注解(如自定义 `@AiPrompt`)与 AOP 切面技术,简化了 AI 服务集成,实现业务逻辑与 AI 基础设施解耦。教程包含创建项目、配置文件、流式响应处理、缓存优化及多任务并行执行等内容,助你快速构建高效、可维护的 AI 应用。
|
6月前
|
XML Java 数据库连接
微服务——SpringBoot使用归纳——Spring Boot集成MyBatis——基于注解的整合
本文介绍了Spring Boot集成MyBatis的两种方式:基于XML和注解的形式。重点讲解了注解方式,包括@Select、@Insert、@Update、@Delete等常用注解的使用方法,以及多参数时@Param注解的应用。同时,针对字段映射不一致的问题,提供了@Results和@ResultMap的解决方案。文章还提到实际项目中常结合XML与注解的优点,灵活使用两者以提高开发效率,并附带课程源码供下载学习。
493 0
|
4月前
|
Web App开发 前端开发 JavaScript
鸿蒙5开发宝藏案例分享---Web适配一多开发实践
这是一份实用的鸿蒙Web多设备适配开发指南,针对开发者在不同屏幕尺寸下的布局难题提供了解决方案。文章通过三大法宝(相对单位、媒体查询和窗口监听)详细介绍如何实现智能适配,并提供了多个实战案例,如宫格布局、对话框变形和自适应轮播图等。此外,还分享了调试技巧及工具推荐,帮助开发者快速上手并优化性能。最后鼓励读者实践探索,并提示更多官方资源等待发现。
|
11月前
|
XML JSON API
ServiceStack:不仅仅是一个高性能Web API和微服务框架,更是一站式解决方案——深入解析其多协议支持及简便开发流程,带您体验前所未有的.NET开发效率革命
【10月更文挑战第9天】ServiceStack 是一个高性能的 Web API 和微服务框架,支持 JSON、XML、CSV 等多种数据格式。它简化了 .NET 应用的开发流程,提供了直观的 RESTful 服务构建方式。ServiceStack 支持高并发请求和复杂业务逻辑,安装简单,通过 NuGet 包管理器即可快速集成。示例代码展示了如何创建一个返回当前日期的简单服务,包括定义请求和响应 DTO、实现服务逻辑、配置路由和宿主。ServiceStack 还支持 WebSocket、SignalR 等实时通信协议,具备自动验证、自动过滤器等丰富功能,适合快速搭建高性能、可扩展的服务端应用。
609 3
|
6月前
|
关系型数据库 MySQL 数据库
基于Flink CDC 开发,支持Web-UI的实时KingBase 连接器,三大模式无缝切换,效率翻倍!
TIS 是一款基于Web-UI的开源大数据集成工具,通过与人大金仓Kingbase的深度整合,提供高效、灵活的实时数据集成方案。它支持增量数据监听和实时写入,兼容MySQL、PostgreSQL和Oracle模式,无需编写复杂脚本,操作简单直观,特别适合非专业开发人员使用。TIS率先实现了Kingbase CDC连接器的整合,成为业界首个开箱即用的Kingbase CDC数据同步解决方案,助力企业数字化转型。
1119 5
基于Flink CDC 开发,支持Web-UI的实时KingBase 连接器,三大模式无缝切换,效率翻倍!
|
6月前
|
机器学习/深度学习 开发框架 API
Python 高级编程与实战:深入理解 Web 开发与 API 设计
在前几篇文章中,我们探讨了 Python 的基础语法、面向对象编程、函数式编程、元编程、性能优化、调试技巧以及数据科学和机器学习。本文将深入探讨 Python 在 Web 开发和 API 设计中的应用,并通过实战项目帮助你掌握这些技术。
|
10月前
|
开发框架 搜索推荐 数据可视化
Django框架适合开发哪种类型的Web应用程序?
Django 框架凭借其强大的功能、稳定性和可扩展性,几乎可以适应各种类型的 Web 应用程序开发需求。无论是简单的网站还是复杂的企业级系统,Django 都能提供可靠的支持,帮助开发者快速构建高质量的应用。同时,其活跃的社区和丰富的资源也为开发者在项目实施过程中提供了有力的保障。
372 67
|
10月前
|
存储 前端开发 JavaScript
如何在项目中高效地进行 Web 组件化开发
高效地进行 Web 组件化开发需要从多个方面入手,通过明确目标、合理规划、规范开发、加强测试等一系列措施,实现组件的高效管理和利用,从而提高项目的整体开发效率和质量,为用户提供更好的体验。
299 63