Web IOC容器
从大家熟悉的DispatcherServlet开始,最先执行的是init()方法。但是经过探索,往上追索在其父类HttpServletBean找到init()方法
/** * Map config parameters onto bean properties of this servlet, and * invoke subclass initialization. * @throws ServletException if bean properties are invalid (or required * properties are missing), or if subclass initialization fails. */@Overridepublic final void init() throws ServletException { if (logger.isDebugEnabled()) { logger.debug("Initializing servlet '" getServletName() "'"); } // Set bean properties from init parameters. PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties); 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) { if (logger.isErrorEnabled()) { logger.error("Failed to set bean properties on servlet '" getServletName() "'", ex); } throw ex; } } // Let subclasses do whatever initialization they like. initServletBean(); if (logger.isDebugEnabled()) { logger.debug("Servlet '" getServletName() "' configured successfully"); }}
在init()方法中,真正完成初始化容器的逻辑在initServletBean()方法中,找到代码在FrameworkServlet类中:
/** * Overridden method of {@link HttpServletBean}, invoked after any bean properties * have been set. Creates this servlet's WebApplicationContext. */@Overrideprotected final void initServletBean() throws ServletException { getServletContext().log("Initializing Spring FrameworkServlet '" getServletName() "'"); if (logger.isInfoEnabled()) { logger.info("FrameworkServlet '" getServletName() "': initialization started"); } long startTime = System.currentTimeMillis(); try { this.webApplicationContext = initWebApplicationContext(); initFrameworkServlet(); } catch (ServletException | RuntimeException ex) { logger.error("Context initialization failed", ex); throw ex; } if (logger.isInfoEnabled()) { long elapsedTime = System.currentTimeMillis() - startTime; logger.info("FrameworkServlet '" getServletName() "': initialization completed in " elapsedTime " ms"); }}
终于看到了initWebApplicationContext()方法:
/** * Initialize and publish the WebApplicationContext for this servlet. * <p>Delegates to {@link #createWebApplicationContext} for actual creation * of the context. Can be overridden in subclasses. * @return the WebApplicationContext instance * @see #FrameworkServlet(WebApplicationContext) * @see #setContextClass * @see #setContextConfigLocation */protected WebApplicationContext initWebApplicationContext() { // 先从ServletContext中获得父容器WebAppliationContext 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); } } } // 先去ServletContext中查找Web容器的引用是否存在,并创建好默认的空IOC容器 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(); } // 给上一步创建好的IOC容器赋值 if (wac == null) { // No context instance is defined for this servlet -> create a local one wac = createWebApplicationContext(rootContext); } // 触发onRefresh方法 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); if (this.logger.isDebugEnabled()) { this.logger.debug("Published WebApplicationContext of servlet '" getServletName() "' as ServletContext attribute with name [" attrName "]"); } } return wac;}/** * Retrieve a {@code WebApplicationContext} from the {@code ServletContext} * attribute with the {@link #setContextAttribute configured name}. The * {@code WebApplicationContext} must have already been loaded and stored in the * {@code ServletContext} before this servlet gets initialized (or invoked). * <p>Subclasses may override this method to provide a different * {@code WebApplicationContext} retrieval strategy. * @return the WebApplicationContext for this servlet, or {@code null} if not found * @see #getContextAttribute() */@Nullableprotected WebApplicationContext findWebApplicationContext() { String attrName = getContextAttribute(); if (attrName == null) { return null; } WebApplicationContext wac = WebApplicationContextUtils.getWebApplicationContext(getServletContext(), attrName); if (wac == null) { throw new IllegalStateException("No WebApplicationContext found: initializer not registered?"); } return wac;}/** * Instantiate the WebApplicationContext for this servlet, either a default * {@link org.springframework.web.context.support.XmlWebApplicationContext} * or a {@link #setContextClass custom context class}, if set. * <p>This implementation expects custom contexts to implement the * {@link org.springframework.web.context.ConfigurableWebApplicationContext} * interface. Can be overridden in subclasses. * <p>Do not forget to register this servlet instance as application listener on the * created context (for triggering its {@link #onRefresh callback}, and to call * {@link org.springframework.context.ConfigurableApplicationContext#refresh()} * before returning the context instance. * @param parent the parent ApplicationContext to use, or {@code null} if none * @return the WebApplicationContext for this servlet * @see org.springframework.web.context.support.XmlWebApplicationContext */protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) { Class<?> contextClass = getContextClass(); if (this.logger.isDebugEnabled()) { this.logger.debug("Servlet with name '" getServletName() "' will try to create custom WebApplicationContext context of class '" contextClass.getName() "'" ", using parent context [" parent "]"); } if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) { throw new ApplicationContextException( "Fatal initialization error in servlet with name '" getServletName() "': custom WebApplicationContext class [" contextClass.getName() "] is not of type ConfigurableWebApplicationContext"); } ConfigurableWebApplicationContext wac = (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass); wac.setEnvironment(getEnvironment()); wac.setParent(parent); String configLocation = getContextConfigLocation(); if (configLocation != null) { wac.setConfigLocation(configLocation); } configureAndRefreshWebApplicationContext(wac); return wac;}protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) { if (ObjectUtils.identityToString(wac).equals(wac.getId())) { // The application context id is still set to its original default value // -> assign a more useful id based on available information if (this.contextId != null) { wac.setId(this.contextId); } else { // Generate default id... wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX ObjectUtils.getDisplayString(getServletContext().getContextPath()) '/' getServletName()); } } wac.setServletContext(getServletContext()); wac.setServletConfig(getServletConfig()); wac.setNamespace(getNamespace()); wac.addApplicationListener(new SourceFilteringListener(wac, new ContextRefreshListener())); // The wac environment's #initPropertySources will be called in any case when the context // is refreshed; do it eagerly here to ensure servlet property sources are in place for // use in any post-processing or initialization that occurs below prior to #refresh ConfigurableEnvironment env = wac.getEnvironment(); if (env instanceof ConfigurableWebEnvironment) { ((ConfigurableWebEnvironment) env).initPropertySources(getServletContext(), getServletConfig()); } postProcessWebApplicationContext(wac); applyInitializers(wac); wac.refresh();}
从上面的代码中可以看出,在configAndRefreshWebApplicationContext0方法中,调用refresh()方法,这个是真正启动IOC容器的入口,后面会详细介绍。IOC容器初始化以后,最后调用了DispatcherServlet的 onRefresh()方法,在onRefresh()方法中又是直接调用initStrategies()方法初始化SpringMVC的九大组件:
/** * This implementation calls {@link #initStrategies}. */@Overrideprotected 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); // handlerMappeing initHandlerMappings(context); // 初始哈参数适配器 initHandlerAdapters(context); // 初始化异常拦截器 initHandlerExceptionResolvers(context); // 初始化视图预处理器 initRequestToViewNameTranslator(context); // 初始化视图转换器 initViewResolvers(context); initFlashMapManager(context);}基于XML的IOC容器初始化
IOC容器的初始化包括BeanDefinition的Resource定位、加载和注册这三个基本的过程。我们以ApplicationContext为例讲解,ApplicationContext 系列容器也许是我们最熟悉的,因为Web项目中使用的XmlWebApplicationContext 就属于这个继承体系还有ClasspathXmlApplicationContext等,其继承体系如下图所示:
ApplicationContext 允许上下文嵌套,通过保持父上下文可以维持一个上下文体系。对于Bean的查找可以在这个上下文体系中发生,首先检查当前上下文,其次是父上下文,逐级向上,这样为不同的Spring应用提供了一个共享的Bean定义环境。
1.寻找入口还有一个我们用的比较多的ClassPathXmlApplicationContext,通过main()方法启动:
ApplicationContext app = new ClassPathxmlApplicationContext("application.xml");
先看其构造函数的调用:
public ClassPathxmlApplicationContext(String configlocation)throws BeansException{ this(new String[]{configlocation}, true, (ApplicationContext) nul1); }
其实际调用的构造函数为:
public ClassPathxmlApplicationContext(String[] configlocations,boolean refresh,@Nullable ApplicationContext parent) throws BeansException { super(parent); this.setconfigLocations(configlocations); if (refresh) { this.refresh(); }}
还有像AnnotationConfigApplicationContext、FileSystemXmlApplicationContext、 XmlWebApplicationContext等都继承自父容器AbstractApplicationContext主要用到了装饰器模式 和策略模式,最终都是调用refresh()方法。
2.获得配置路径通过分析ClassPathXmlApplicationContext的源代码可以知道,在创建ClassPathXmlApplicationContext 容器时,构造方法做以下两项重要工作:
首先,调用父类容器的构造方法(super(parent)方法)为容器设置好Bean 资源加载器。然后,再调用父类AbstractRefreshableConfigApplicationContext 的setConfigLocations(configLocations)方法设置Bean配置信息的定位路径。 通过追踪ClassPathXmlApplicationContext的继承体系,发现其父类的父类AbstractApplicationContext中初始化IOC容器所做的主要源码如下:
public abstract class AbstractApplicationContext extends DefaultResourceLoader implements ConfigurableApplicationContext { static { // Eagerly load the ContextClosedEvent class to avoid weird classloader issues // on application shutdown in WebLogic 8.1. (Reported by Dustin Woods.) // 为了避免应用程序在Weblogic8.1关闭是出现类加载异常加载问题,加载IOC容器关闭事件(ContextClosedEvent)类 ContextClosedEvent.class.getName(); } /** * Create a new AbstractApplicationContext with no parent. */ public AbstractApplicationContext() { this.resourcePatternResolver = getResourcePatternResolver(); } /** * Create a new AbstractApplicationContext with the given parent context. * @param parent the parent context */ public AbstractApplicationContext(@Nullable ApplicationContext parent) { this(); setParent(parent); } // 获取一个Spring Source的加载器用于读入SpringBean配置信息 // AbstractApplicationContext默认构造方法中有调用 PathMatchingResourcePatternResolver 构造方法 // 创建Spring资源加载器 protected ResourcePatternResolver getResourcePatternResolver() { return new PathMatchingResourcePatternResolver(this); }}
在设置容器的资源加载器之后,接下来ClassPathXmlApplicationContext 执行 setConfigLocations()方法通过调用其父类AbstractRefreshableConfigApplicationContext的方法进行对Bean配置信息的定位,该方法的源码如下:
/** * Set the config locations for this application context in init-param style, * i.e. with distinct locations separated by commas, semicolons or whitespace. * <p>If not set, the implementation may use a default as appropriate. */// 处理单个资源文件路径为一个字符串情况public void setConfigLocation(String location) { // 多个资源文件路径之间可以用 ",; \t\n" 分割 setConfigLocations(StringUtils.tokenizeToStringArray(location, CONFIG_LOCATION_DELIMITERS));}/** * 保存需要加载的本地资源 例如:application.xml * Set the config locations for this application context. * <p>If not set, the implementation may use a default as appropriate. */public void setConfigLocations(@Nullable String... locations) { if (locations != null) { Assert.noNullElements(locations, "Config locations must not be null"); this.configLocations = new String[locations.length]; for (int i = 0; i < locations.length; i ) { // resolvePath 为同一个类中将字符串解析为路径的方法 this.configLocations[i] = resolvePath(locations[i]).trim(); } } else { this.configLocations = null; }}
通过这两个方法的源码我们可以看出,我们既可以使用一个字符串来配置多个Spring Bean配置信息,也可以使用字符串数组,即下面两种方式都是可以的:
ClassPathResource res=new ClassPathResource("a.xml,b.xml");
多个资源文件路径之间可以是用",;\t\r"等分隔。
ClassPathResource res =new ClassPathResource(new String[]{"a.xml","b.xml"});
至此,SpringlOC容器在初始化时将配置的Bean 配置信息定位为Spring封装的Resource。