什么是spring容器和容器初始化

10 篇文章 1 订阅

目录

1.概念说明

(1)ServletContext

(2)BeanFactory

(3)控制反转(IOC)

(4)依赖注入(DI)

(5)Spring Bean作用域

2. 常见的Spring初始化容器的方式

(1)SSM

(2)ClassPathXmlApplicationContext

3.Spring boot如何进行初始化

(1)Spring boot初始化分为两个部分:

(2)@SpringBootApplication

(3)SpringApplication.run


相信做java开发的同学对“spring容器”一词并不陌生,但是spring容器到底是个什么东西,每个人的理解又不一样,这样对于初学者就比较尴尬了,总是糊里糊涂的。所以,接下来咱们从spring容器如何初始化角度来分析,这样能更好的帮助大家理解什么是spring容器,相信你通过这篇说明会对spring容器有更为清晰的认识。

在分析spring容器如何初始化之前,我们需要有几点说明,因为这将对初始化过程的理解更有帮助。

1.概念说明

(1)ServletContext

在servlet的规范当中,servlet容器或者叫web容器,如tomcat、jboss等中运行的每个应用都由一个ServletContext表示,在web容器中可以包含多个ServletContext,即可以有多个web应用在web容器中运行。如在tomcat的webapp目录下,每个war包都对应一个web应用,tomcat启动时会解压war包,并启动相关的应用。

(2)BeanFactory

BeanFactory是一个顶级接口,是一个最简单的spring容器,给依赖注入提供基础的支持。所以它是面向spring本身,属于基础设置。它最主要的方法就是 getBean(String beanName),该方法从容器中返回特定名称的 Bean。

ApplicationContext是由BeanFactory派生而来的,提供了更多面向实际应用的功能,所以它是面向使用spring框架的开发者。

WebApplicationContext 是专门为 Web 应用准备的,它允许从相对于 Web 根目录的路径中装载配置文件完成初始化工作。从 WebApplicationContext 中可以获得ServletContext 的引用,整个 Web应用上下文对象将作为属性放置到 ServletContext 中,以便 Web 应用环境可以访问 Spring 应用上下文。

他们之间的类继承关系,如下图:

(3)控制反转(IOC)

控制反转(IOC)只是一个概念,正常情况下,开发者都是通过new对象之后,才可以调用对象。但是如果对象太多,或者这些对象的每一个都需要被频繁的使用,那么就会出现对象频繁的被创建和销毁,这对资源的使用及性能的消耗上是极其恐怖的。为了解决这个问题,引入了IOC,即先把对象创建好,缓存在内存上(可以放在某个容器里,比如spring容器),使用的时候直接调用就好了,而且可以频繁被使用,当容器销毁的时候,再把对象销毁掉,这样既解决了资源使用问题,又提升了性能。

(4)依赖注入(DI)

基于IOC的概念,我们可以把一个个对象事先创建好,放在容器里。那么对象之间肯定会有依赖关系,怎么解决呢?

DI可以解决,DI会把符合依赖关系的对象通过JavaBean属性或者构造函数传递给需要的对象。通过JavaBean属性注射依赖关系的做法称为设值方法注入(Setter Injection);将依赖关系作为构造函数参数传入的做法称为构造器注入(Constructor Injection);还有一种是自动装配,比如使用@Autowired、@Resource。具体如何使用,不是这里的重点,可自行搜索。

(5)Spring Bean作用域

Spring Bean作用域的选择,决定了bean的创建和销毁方式。

1> 基本作用域:

Singleton(默认):单例模式(多线程下不安全),Spring IoC 容器(这里是针对一个ApplicationContext而言的,在一个web容器中(即ServletContext)中ApplicationContext可以有多个)中只会存在一个共享的 Bean 实例,无论有多少个Bean 引用它,始终指向同一对象。该模式在多线程下是不安全的。Singleton 作用域是Spring 中的缺省作用域,也可以显示的将 Bean 定义为 singleton 模式,配置为:<bean id="userDao" class="com.ioc.UserDaoImpl" scope="singleton"/>。

prototype:原型模式,每次通过 Spring 容器获取 prototype 定义的 bean 时,容器都将创建一个新的 Bean 实例,每个 Bean 实例都有自己的属性和状态,而 singleton 全局只有一个对象。根据经验,对有状态的bean使用prototype作用域,而对无状态的bean使用singleton作用域。

 2> Web应用中的作用域

Request:一次 request 一个实例。在一次 Http 请求中,容器会返回该 Bean 的同一实例。而对不同的 Http 请求则会产生新的 Bean,而且该 bean 仅在当前 Http Request 内有效,当前 Http 请求结束,该 bean实例也将会被销毁。配置为:<bean id="loginAction" class="com.cnblogs.Login" scope="request"/>

session:在一次 Http Session 中,容器会返回该 Bean 的同一实例。而对不同的 Session 请求则会创建新的实例,该 bean 实例仅在当前 Session 内有效。同 Http 请求相同,每一次session 请求创建新的实例,而不同的实例之间不共享属性,且实例仅在自己的 session 请求内有效,请求结束,则实例将被销毁。配置为:<bean id="userPreference" class="com.ioc.UserPreference" scope="session"/>

global Session:在一个全局的 Http Session 中,容器会返回该 Bean 的同一个实例,仅在使用 portlet context 时有效。

2. 常见的Spring初始化容器的方式

首先,看下Spring 容器高层视图(如下图),Spring 启动时读取应用程序提供的Bean配置信息,并在Spring容器中生成一份相应的Bean配置注册表,然后根据这张注册表实例化Bean,装配号Bean之间的依赖关系,为上层应用提供准备就绪的运行环境。

(1)SSM

 在web容器启动的时候,会初始化web应用,即创建ServletContext对象,加载解析web.xml文件。web.xml中的加载顺序:context-param -> listener -> filter -> servlet。其中ContextLoaderListener是属于listener阶段。我们通常需要在项目的web.xml中配置一个DispatcherServlet,并配置拦截包含“/”路径的请求,即拦截所有请求。

ContextLoaderListener和DispatcherServlet两个都会创建各自的spring容器,一般情况下,ContextLoaderListener创建的是父容器,而DispatcherServlet创建的是子容器,在子容器中有个parent属性,指向的就是父容器,而父容器没有任何指向子容器的关系,所以只能子容器调用父容器中的方法,但是反过来不支持,下面以这两个说明下容器初始化的过程。

  • ContextLoaderListener

首先看下ContextLoaderListener主要代码,如下

public WebApplicationContext initWebApplicationContext(ServletContext 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!");
	} else {
		...
		try {
			if (this.context == null) {
				this.context = this.createWebApplicationContext(servletContext);
			}
			if (this.context instanceof ConfigurableWebApplicationContext) {
				ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext)this.context;
				//判断该新创建的容器是否refresh过,即是否初始化单例bean等,没有的话则进行refresh
				if (!cwac.isActive()) {
					//判断该新创建的容器有父容器,没有则set
					if (cwac.getParent() == null) {
						ApplicationContext parent = this.loadParentContext(servletContext);
						cwac.setParent(parent);
					}
					//对新创建的容器进行refresh等操作,涉及到单例bean的初始化等工作
					this.configureAndRefreshWebApplicationContext(cwac, servletContext);
				}
			}
			//把新创建的容器作为属性放到ServletContext中,以便web应用环境可以访问该spring 容器
			servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
			...
			return this.context;
		} catch (RuntimeException var8) {
			...
		} catch (Error var9) {
			...
		}
	}
}

上面重点的是configureAndRefreshWebApplicationContext(cwac, servletContext)这个操作,里面开始时读取xml配置相关信息,重点是之后的wac.refresh(),代码注解如下:

@Override
public void refresh() throws BeansException, IllegalStateException {
   // 来个锁,不然 refresh() 还没结束,你又来个启动或销毁容器的操作,那不就乱套了嘛
   synchronized (this.startupShutdownMonitor) {
 
      // 准备工作,记录下容器的启动时间、标记“已启动”状态、处理配置文件中的占位符
      prepareRefresh();
 
      // 这步比较关键,这步完成后,配置文件就会解析成一个个 Bean 定义,注册到 BeanFactory 中,
      // 当然,这里说的 Bean 还没有初始化,只是配置信息都提取出来了,
      // 注册也只是将这些信息都保存到了注册中心(说到底核心是一个 beanName-> beanDefinition 的 Map)
      ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
 
      // 设置 BeanFactory 的类加载器,添加几个 BeanPostProcessor,手动注册几个特殊的 bean
      // 这块待会会展开说
      prepareBeanFactory(beanFactory);
 
      try {
         // 【这里需要知道 BeanFactoryPostProcessor 这个知识点,Bean 如果实现了此接口,
         // 那么在容器初始化以后,Spring 会负责调用里面的 postProcessBeanFactory 方法。】
 
         // 这里是提供给子类的扩展点,到这里的时候,所有的 Bean 都加载、注册完成了,但是都还没有初始化
         // 具体的子类可以在这步的时候添加一些特殊的 BeanFactoryPostProcessor 的实现类或做点什么事
         postProcessBeanFactory(beanFactory);
         // 调用 BeanFactoryPostProcessor 各个实现类的 postProcessBeanFactory(factory) 方法
         invokeBeanFactoryPostProcessors(beanFactory);
 
         // 注册 BeanPostProcessor 的实现类,注意看和 BeanFactoryPostProcessor 的区别
         // 此接口两个方法: postProcessBeforeInitialization 和 postProcessAfterInitialization
         // 两个方法分别在 Bean 初始化之前和初始化之后得到执行。注意,到这里 Bean 还没初始化
         registerBeanPostProcessors(beanFactory);
 
         // 初始化当前 ApplicationContext 的 MessageSource,国际化这里就不展开说了,不然没完没了了
         initMessageSource();
 
         // 初始化当前 ApplicationContext 的事件广播器,这里也不展开了
         initApplicationEventMulticaster();
 
         // 从方法名就可以知道,典型的模板方法(钩子方法),
         // 具体的子类可以在这里初始化一些特殊的 Bean(在初始化 singleton beans 之前)
         onRefresh();
 
         // 注册事件监听器,监听器需要实现 ApplicationListener 接口。这也不是我们的重点,过
         registerListeners();
 
         // 重点,重点,重点
         // 初始化所有的 singleton beans,并将它们放入Spring容器的缓存中(缓存底层为ConcurrentHashMap结构)
         //(lazy-init 的除外)
         finishBeanFactoryInitialization(beanFactory);
 
         // 最后,广播事件,ApplicationContext 初始化完成
         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.
         // 销毁已经初始化的 singleton 的 Beans,以免有些 bean 会一直占用资源
         destroyBeans();
 
         // Reset 'active' flag.
         cancelRefresh(ex);
 
         // 把异常往外抛
         throw ex;
      }
 
      finally {
         // Reset common introspection caches in Spring's core, since we
         // might not ever need metadata for singleton beans anymore...
         resetCommonCaches();
      }
   }
}

  • DispatcherServlet

在DispatcherServlet类中并没有找到init方法,继续从其父类中搜索,DispatcherServlet -> FrameworkServlet -> HttpServletBean,最终在其父类HttpServletBean中找到了init方法。整体方法链路如下:

public final void init() throws ServletException {
   ...
   // Let subclasses do whatever initialization they like.
   initServletBean();
}
protected final void initServletBean() throws ServletException {
   ...
   try {
      this.webApplicationContext = initWebApplicationContext();
      initFrameworkServlet();
   }
   ...
}
protected WebApplicationContext initWebApplicationContext() {
   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);
         }
      }
   }
   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
      wac = findWebApplicationContext();
   }
   if (wac == null) {
      // No context instance is defined for this servlet -> create a local one
      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;
}

相比较ContextLoaderListener而言,DispatcherServlet主要加了一个onRefresh(wac)操作,主要是初始化 HandlerMapping & HandlerAdapter 等spring mvc九大组件工作。具体代码如下:

@Override
protected void onRefresh(ApplicationContext context) {
    initStrategies(context);
}

/**
 * 初始化策略
 * Initialize the strategy objects that this servlet uses.
 * <p>May be overridden in subclasses in order to initialize further strategy objects.
 */
protected void initStrategies(ApplicationContext context) {
    // 多文件上传组件
    initMultipartResolver(context);
    // 初始化本地语言环境
    initLocaleResolver(context);
    // 初始化模板处理器
    initThemeResolver(context);
    // 初始化url映射
    initHandlerMappings(context);
    // 初始化参数适配器
    initHandlerAdapters(context);
    // 初始化异常拦截器
    initHandlerExceptionResolvers(context);
    // 初始化视图预处理器
    initRequestToViewNameTranslator(context);
    // 初始化视图转化器
    initViewResolvers(context);
    // 初始化FlashMap管理器
    initFlashMapManager(context);
}

(2)ClassPathXmlApplicationContext

使用代码示例如下:

private static ApplicationContext ac = new ClassPathXmlApplicationContext(
        "classpath*:conf/spring-impl-res.xml",
        "classpath*:conf/spring-elasticsearch-sql-impl.xml",
        "classpath*:conf/spring/spring-impl-dao.xml",
        "classpath*:conf/spring/spring-impl-service.xml"
);

其底层代码实现为:

public ClassPathXmlApplicationContext(String[] configLocations, boolean refresh, ApplicationContext parent) throws BeansException {
    super(parent);
    this.setConfigLocations(configLocations);
    if (refresh) {
        this.refresh();
    }

}

而refresh()与ContextLoaderListener中的refresh()是同一方法,所以具体细节看上述即可。

3.Spring boot如何进行初始化

(1)Spring boot初始化分为两个部分:

@SpringBootApplication和执行SpringApplication.run静态方法。

(2)@SpringBootApplication

  • @SpringBootConfiguration同@Configuration,表示这是一个JavaConfig配置类,无其他作用。
  • @EnableAutoConfiguration
    1. 类似于提供一个组件,起作用就是可以将所有符合自动配置条件的bean定义加载到IOC容器,比如根据spring-boot-starter-web来判断是否添加webmvc和tomcat,其他spring-boot-starter开头的包均通过该方式自动化配置,借助@import的帮助。
    2. 注意这里并没有执行自动化配置,真正执行是在SpringApplication.run方法里的refreshContext方法里。
    3. 该注解的作用是启动自动配置机制,而支持的自动配置类名列表在spring-boot-autoconfigure包下的META-INF/spring.factories文件中的EnableAutoConfiguration中。
  • @ComponentScan
    1. Spring的自动扫描注解,可定义扫描范围,加载到IOC容器,默认扫描SpringApplication.run启动类所在的同级目录下的所有文件,所以最好将该启动类放到跟路径下。
    2. 注意该注解仅提供扫描配置支持,并未进行扫描,真正执行的是在SpringApplication.run方法里的refreshContext方法里。
    3. 扫描所有@Component注解,如@Configuration、@Controller、@Service、@Repository底层均是@Component实现。

(3)SpringApplication.run

  •   SpringApplication这个类主要做一下四件事情:
  1. 推断应用的类型是普通的项目还是Web项目;
  2. 查找并加载所有可用初始化器 , 设置到initializers属性中;
  3. 找出所有的应用程序监听器,设置到listeners属性中;
  4. 推断并设置main方法的定义类,找到运行的主类;

构造器方法如下:

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
    ...
    this.webApplicationType = WebApplicationType.deduceFromClasspath();
    this.setInitializers(this.getSpringFactoriesInstances(ApplicationContextInitializer.class));
    this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class));
    this.mainApplicationClass = this.deduceMainApplicationClass();
}
  • run方法
/**
  * Run the Spring application, creating and refreshing a new
  * {@link ApplicationContext}.
  *
  * @param args the application arguments (usually passed from a Java main method)
  * @return a running {@link ApplicationContext}
  *
  * 运行spring应用,并刷新一个新的 ApplicationContext(Spring的上下文)
  * ConfigurableApplicationContext 是 ApplicationContext 接口的子接口。在 ApplicationContext
  * 基础上增加了配置上下文的工具。 ConfigurableApplicationContext是容器的高级接口
  */
public ConfigurableApplicationContext run(String... args) {
     //记录程序运行时间
     StopWatch stopWatch = new StopWatch();
     stopWatch.start();
     // ConfigurableApplicationContext Spring 的上下文
     ConfigurableApplicationContext context = null;
     Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
     configureHeadlessProperty();
     //从META-INF/spring.factories中获取监听器
     //1、获取并启动监听器
     SpringApplicationRunListeners listeners = getRunListeners(args);
     listeners.starting();
     try {
         ApplicationArguments applicationArguments = new DefaultApplicationArguments(
                 args);
         //2、构造应用上下文环境(读取配置文件,如application.yml等,加载配置文件,构建容器环境)
         ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
         //处理需要忽略的Bean
         configureIgnoreBeanInfo(environment);
         //打印banner
         Banner printedBanner = printBanner(environment);
         ///3、初始化应用上下文(创建spring容器)
         context = createApplicationContext();
         //实例化SpringBootExceptionReporter.class,用来支持报告关于启动的错误
         exceptionReporters = getSpringFactoriesInstances(
                 SpringBootExceptionReporter.class,
                 new Class[]{ConfigurableApplicationContext.class}, context);
         //4、刷新应用上下文前的准备阶段(为后面扫描bean、加载做准备)
         prepareContext(context, environment, listeners, applicationArguments, printedBanner);
         //5、刷新应用上下文(底层跟ContextLoaderListener中的refresh()是同一方法)
         refreshContext(context);
         //刷新应用上下文后的扩展接口
         afterRefresh(context, applicationArguments);
         //时间记录停止
         stopWatch.stop();
         if (this.logStartupInfo) {
             new StartupInfoLogger(this.mainApplicationClass)
                     .logStarted(getApplicationLog(), stopWatch);
         }
         //发布容器启动完成事件
         listeners.started(context);
         callRunners(context, applicationArguments);
     } catch (Throwable ex) {
         handleRunFailure(context, ex, exceptionReporters, listeners);
         throw new IllegalStateException(ex);
     }
 
     try {
         listeners.running(context);
     } catch (Throwable ex) {
         handleRunFailure(context, ex, exceptionReporters, null);
         throw new IllegalStateException(ex);
     }
     return context;
}

用一张图更为直观,如下:

以上就是本片所有内容了,其实spring初始化重要的两个过程,

第一,初始化配置,并创建容器;

第二,实例化bean,并放到缓存,重点是refresh方法。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值