Spring IOC

小龙 640 2020-04-15

一、Spring IOC理解

1.1 IoC是什么?

IOC(Inversion of Control),既“控制反转”,不是什么技术,而是一种设计思想。在程序之中,Ioc就意味着将你设计好的对象交由容器控制,而不是传统的直接在对象内部控制。控制是什么?谁控制了谁?为何是反转?哪些地方反转了?要想很好的理解IOC就需要了解上面的集合问题。

谁控制谁,控制什么

  • 传统的Java SE程序设计,我们都是直接在对象内部通过关键字new进行对象创建,是程序主动去创建依赖对象;而IoC是有一个专门的容器来创建这些对象,既由IoC容器来控制对象的创建;
  • 谁控制谁:IoC容器控制了对象
  • 控制什么:主要控制了外部资源获取(不只是对象,还包括比如文件等资源)
    为何是反转,哪些方面反转了
  • 有反转就有正转,传统的开发是由我们自己在对象中主动去直接获取依赖对象,也就是正转;反转则是由容器来帮忙创建及注入依赖对象;
  • 为何是反转:因为容器帮我们查找及注入依赖对象,对象只是被动的接受依赖对象,所以是反转;
  • 哪些方面反转了:依赖对象的获取被反转了,原来是主动的创建,现在被动的接受注入;
    传统应用程序
    ioc1.png

当有了IoC/DI的容器之后,在客户端类中不在需要去主动创建那些对象了

ioc2.png

1.2 IoC能做什么

IoC不是一种技术,只是一种思想,一个重要的面向对象编程的法则,它指导我们如何设计出松耦合、更优良的程序。传统应用程序都是由我们在类内部使用关键值(new)主动去创建依赖对象,从而导致类与类之间高耦合,难于测试;有了IoC容器之后,把创建和查找依赖对象的控制权交给了容器,由容器进行注入组合对象,所以对象与对象之间是松耦合的,这样也方便测试,更利于功能的复用,更重要的是使得程序的整个体系结构变得更加的灵活。
其实IoC对编程带来的最大的改变不是从代码上,而是从思想上,发生了“主从换为”的变化。应用程序原本是老大,想要获取什么资源都是主动出击,但是在IoC/DI思想中,应用程序就变成了被动了,被动的等待容器来创建并注入它所需要的资源。
IoC很好的体现了面向对象设计法则之一——好莱坞法则:“别找我们,我们找你”;既由IoC容器帮对象找相应的依赖对象并注入,而不是对象主动去找。

1.3、IoC和DI

DI(Dependency Injection,既“依赖注入”):组件之间的依赖关系由容器在运行期间决定,形象的说既由容器动态的将某个依赖关系注入到组件之中。依赖注入的目的并非为软件系统带来更多的功能,而是为了提升组件重用的频率,并为系统搭建一个灵活的、可扩展的平台。通过依赖注入机制,我们只需要通过简单的配置,而无需任何代码就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心资源具体来自何处,由谁实现。

理解DI的关键是:“谁依赖谁,为什么需要依赖,,谁注入谁,注入了什么”;

  • 谁依赖谁:当然是应用程序依赖于IoC容器
  • 为什么需要依赖:应用程序需要IoC容器来提供对象需要的外部资源
  • 谁注入谁:很明显是IoC容器注入应用程序某个对象,应用程序依赖的对象
  • 注入了什么:就是注入某个对象所需要的外部资源(包括对象,资源,常亮数据)

1.4、依赖注入的两种方式:设置注入和构造注入

依赖注入是指程序运行过程中,如果需要另一个对象协助完成时,无需在代码中创建被调用者对象,而是依赖外部的注入
通过下面代码来观察两种注入方式
定义ILanguage接口

package com.yootk.ioc;
public interface ILanguage {
    String kind();
}

定义两个实现子类

package com.yootk.ioc.impl;
import com.yootk.ioc.ILanguage;
public class Chineselan implements ILanguage {
    @Override
    public String kind() {
        return "我讲汉语";
    }
}
-------------------------------------------------
package com.yootk.ioc.impl;
import com.yootk.ioc.ILanguage;
public class English implements ILanguage {
    @Override
    public String kind() {
        return "I Speak English";
    }
}

定义IPerson接口

package com.yootk.ioc;
public interface IPerson {
    void speak();
}

定义实现子类

package com.yootk.ioc.impl;
import com.yootk.ioc.ILanguage;
import com.yootk.ioc.IPerson;
public class Chinese implements IPerson {
    /** 需要注入的对象,由IoC容器创建对象根据配置文件进行注入 */
    private ILanguage language;
    @Override
    public void speak() {
        System.err.println(this.language.kind());
    }
    /**
     * 设置注入
     * @param language 根据配置文件注入的对象
     */
    public void setLanguage(ILanguage language) {
        this.language = language;
    }
}
package com.yootk.ioc.impl;
import com.yootk.ioc.ILanguage;
import com.yootk.ioc.IPerson;
public class American implements IPerson {
    /** 需要注入的对象,由IoC容器创建对象根据配置文件进行注入 */
    private ILanguage language;
    @Override
    public void speak() {
	System.err.println(this.language.kind());
    }
    /**
     * 构造注入
     * @param language 根据配置文件注入的对象
     */
    public American(ILanguage language){
        this.language = language;
    }
}

编写测试类

package com.yootk.ioc.test;
import com.yootk.ioc.IPerson;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.FileSystemXmlApplicationContext;
public class Test {
    public static void main(String[] args) {
        // resource目录下
        ApplicationContext context = new ClassPathXmlApplicationContext("spring/spring-base.xml");
        IPerson cperson = (IPerson) context.getBean("Chinese");
        cperson.speak();

        IPerson aperson = (IPerson) context.getBean("American");
        aperson.speak();
    }
}

spring-base.xml配置文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <!-- 配置Bean对象,注入Chinese对象 -->
    <bean id="Chinese" class="com.yootk.ioc.impl.Chinese">
        <!-- 设置注入,property是需要容器注入的属性,name是属性名称,ref是注入的内容,是bean id,
        这里注入的“汉语”,该对象实例由Chineselan类生成 -->
        <property name="language" ref="Chineselan"/>
    </bean>
    <bean id="Chineselan" class="com.yootk.ioc.impl.Chineselan"></bean>

    <!-- 配置Bean对象,注入American对象 -->
    <bean id="American" class="com.yootk.ioc.impl.American">
        <!-- 构造注入,constructor-arg是需要注入的属性,name是属性名称 -->
        <constructor-arg name="language" ref="English"/>
    </bean>
    <bean id="English" class="com.yootk.ioc.impl.English"></bean>
</beans>

执行结果
ioc3.png

通过配置文件管理Bean的好处是个Bean之间的依赖关系被放在在配置文件中实现,而不是在代码中实现,通过配置文件,Spring能很好的为每个Bean注入属性。

当调用者对象需要其他对象协助时,IoC容器就会根据配置文件把需要的对象实例化并通过设置注入和依赖注入的方式注入到当前调用者的对象中去,作为成员变量;

优点

  • 开发人员不需要关注对象是如何被创建的,由IoC容器根据配置文件创建;
  • 增加新类也非常的方便,只需要修改配置文件即可;
  • IoC容器通过配置文件来确定需要注入的实例化对象。非常便于单元测试

二、Spring IoC原理分析

要想更加深入的理解IoC就需要对其实现原理进行分析,通过从基本概念和源码角度着手分析一下三个问题。

  • 什么是IoC容器
  • 如果创建IoC容器
  • IoC容器如何初始化Bean实例的

2.1、什么是IoC容器

2.1.1、什么是IoC容器

所谓的IoC容器就是指的Spring中Bean工厂里面的Map存储结构(存储了Bean的实例);

2.1.2、Spring中的工厂有哪些

(1) ApplicationContext接口:

  • 实现了BeanFactory接口
  • 实现ApplicationContext接口的工厂,可以获取到容器中具体的Bean对象
    (2) BeanFactory工厂(是Spring框架早期的创建Bean对象的工厂接口)
  • 实现BeanFactory接口的工厂,也可以获取到容器中具体的Bean对象

通过查看源码,不管是BeanFactory还是ApplicationContext,其实最终的底层BeanFactory都是DefaultListableBeanFactory

2.1.3、ApplicationContext和BeanFactory的区别

创建Bean对象的时机不同
(1) BeanFactory采用延迟加载,第一次调用getBean()方法时才会初始化Bean。
(2) ApplicationContext是加载完applicationContext.xml时,就创建具体的Bean对象实例。(只对BeanDefition中描述为单例的Bean,才进行饿汉式加载
BeanFactory关系图
ioc4.png

2.2、如何创建IoC容器

###2.2.1、创建方式
(1) ApplicationContext接口常用实现类

  • ClassPathXmlApplicationContext:它是从类的根路径下加载配置文件(推荐使用)
  • FileSystemXmlApplicationContext:它是从磁盘路径上加载配置文件,配置文件可以在磁盘的任意位置
  • AnnotationConfigApplicationContext:当我们使用注解配置容器对象时,需要使用此类来创建Spring容器。它用来读取注解

2.2.2、在不同应用下创建IoC容器

(1) 在Java应用中创建IoC容器

ApplicationContext context = new ClassPathXmlApplicationContext("spring/spring-base.xml");
ApplicationContext context = new FileSystemXmlApplicationContext("具体路径");

(2) 在Web应用中创建IoC容器

  • web.xml中配置ContextLoaderListener接口,并配置ContextConfigLocation参数
 <!-- 1、SpringMVC本身是运行在Spring容器之中,所以需要定义一个Spring容器的基本配置文件路径 -->
    <context-param> <!-- 配置全局的初始化参数,这个参数依靠ServletContext.getInitParameter()获取 -->
        <param-name>contextConfigLocation</param-name>  <!-- 属性标准名称 -->
        <!-- 所有的Spring配置文件只允许加载一次,不要重复加载 -->
        <param-value>classpath:spring/spring-base.xml</param-value>
    </context-param>
    <!-- 2、通过WEB容器启动的时候实现Spring容器的启动操作 -->
    <listener>
        <listener-class>
            org.springframework.web.context.ContextLoaderListener
        </listener-class>
    </listener>
  • web容器启动之后加载web.xml,此时加载ContextLoaderListener监听器。(实现了ServletContextListener接口)
  • ContextLoaderListener监听器会在web容器启动的时候,触发ContextInitialize()方法(ServletContext域或对象创建的时候触发该方法)。
	/**
	* 初始化根web应用程序上下文。
	 * Initialize the root web application context.
	 */
	@Override
	public void contextInitialized(ServletContextEvent event) {
		initWebApplicationContext(event.getServletContext());
	}
  • ContextInitialize()方法会调用initWebApplicationContext()方法,该方法负责创建Spring容器(DefaultListableBeanFactory)
	/**
	 * 为给定的servlet上下文初始化Spring的web应用程序上下文,使用在构建时提供的应用程序上下文,或者创建一个新的应用程序上下文
	 */
	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!");
		}

		servletContext.log("Initializing Spring root WebApplicationContext");
		Log logger = LogFactory.getLog(ContextLoader.class);
		if (logger.isInfoEnabled()) {
			logger.info("Root WebApplicationContext: initialization started");
		}
		long startTime = System.currentTimeMillis();

		try {
			// Store context in local instance variable, to guarantee that
			// it is available on ServletContext shutdown.
			if (this.context == null) {
				this.context = createWebApplicationContext(servletContext);
			}
			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) {
						// 创建 Spring 容器
						ApplicationContext parent = loadParentContext(servletContext);
						cwac.setParent(parent);
					}
					// Spring 容器初始化单例 Bean 的实例
					configureAndRefreshWebApplicationContext(cwac, servletContext);
				}
			}
			servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);

			ClassLoader ccl = Thread.currentThread().getContextClassLoader();
			if (ccl == ContextLoader.class.getClassLoader()) {
				currentContext = this.context;
			}
			else if (ccl != null) {
				currentContextPerThread.put(ccl, this.context);
			}

			if (logger.isInfoEnabled()) {
				long elapsedTime = System.currentTimeMillis() - startTime;
				logger.info("Root WebApplicationContext initialized in " + elapsedTime + " ms");
			}

			return this.context;
		}
		catch (RuntimeException | Error ex) {
			logger.error("Context initialization failed", ex);
			servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex);
			throw ex;
		}
	}

此方法中创建Spring容器就在createWebApplicationContext()方法中完成。Spring容器初始化单例Bean的实例由configureAndRefreshWebApplicationContext()完成。

  • configureAndRefreshWebApplicationContext()方法中调用最终初始化Bean的refresh()方法
	protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
		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
			String idParam = sc.getInitParameter(CONTEXT_ID_PARAM);
			if (idParam != null) {
				wac.setId(idParam);
			}
			else {
				// Generate default id...
				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);
		}

		// 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(sc, null);
		}
		customizeContext(sc, wac);
		// Spring 容器真正初始化的方法就是 refresh() 方法,不管是 Web 环境还是 Java 环境,都是使用此方法
		wac.refresh();
	}

Spring容器真正的初始化方法就是refresh(),不管是Web环境还是Java环境,都是使用此方法。

注意:ServletContextListener在服务器启动时创建,在服务器正常关闭时销毁,SpringContextLoaderListener(服务器启动时负责加载Spring配置文件)
(3) 下图主要是分析上面三部中【创建Spring容器】的图示
ioc5.png

2.2.3、IoC容器如何创建Bean对象

上一段中我们讲述了创建IoC容器的过程,下面我们接着上面的refresh()方法来继续分析
refresh()方法是由“AbstractApplicationContext”类实现的。

	@Override
	public void refresh() throws BeansException, IllegalStateException {
		synchronized (this.startupShutdownMonitor) {
			// 准备刷新此上下文
			prepareRefresh();

			// 1、创建真正的Spring容器(DefaultListableBeanFactory)
			// 2、加载BeanDefition(描述要初始化的Bean的信息)
			// 3、将BeanDefition注册到BeanDefitionRegistry
			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);
	
				// 执行实现了BeanFactoryPostProcessor接口的Bean
				// 比如PropertyPlaceHolderConfigurer(context:property-placeholer)就是此处被调用的,替换掉BeanDefition中的占位符(${})中的内容
				// Invoke factory processors registered as beans in the context.
				invokeBeanFactoryPostProcessors(beanFactory);

				// 注册BeanPostProcessor(后置处理器)
				// 比如容器自动装载一个AutowireAnnotationBeanPostPercessor后置处理器(实现@Autowired注解的功能)
				// 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();

				// 初始化非懒加载方式的单例Bean实例
				// 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();
			}
		}
	}