Spring的工作原理(二)IOC-DI
由上一篇文章其实借着打印HelloWorld的这个例子已经简单理解了什么是控制反转。这里再一次详细讲解一下。
一、什么是IOC,什么是DI?
先把定义记住,后面才能有序的进行,否则就容易看不懂。
1.1 IOC(Inversion of Control)-控制反转。
控制反转与其定义为Spring中的一种技术,不如说是一种设计思想。在之前我们使用new的方式来进行创建对象,现在创建对象的权利交给了Spring来统一管理:即对对象的定义、作用域、生命周期、后置处理器、定义集成。
1.2 DI(Dependency Injection)-依赖注入。
懂了控制反转之后,依赖注入就容易理解了,依赖注入的原理就是控制反转,因为管理权在Spring,如果对象与对象之间存在一定的联系,那么由Spring容器(或者叫IOC容器)动态的将某个依赖关系注入到组件当中,这个过程就是依赖注入。组件之间的依赖关系由容器运行期决定。
举个例子
在Spring框架下,当Bean实例 A运行过程中需要引用另外一个Bean实例B时,Spring框架会创建Bean的实例B,并将实例B通过实例A的构造函数、set方法、自动装配和注解方式注入到实例A,这种注入实例Bean到另外一个实例Bean的过程称为依赖注入。
二、IOC控制反转基础知识
本节就是讲解IOC怎么进行控制反转的,并且通过哪一个类、或者哪一个接口实现的。它是怎么进行管理Bean对象的?管理了对象的哪一些地方?怎么进行创建?销毁时间是什么?下面就详细介绍一下:
2.1 IOC对bean的获取
首先,被Spring管理的类使用哪一个接口创建?那就需要了解一下Spring中的Spring BeanFactory容器和Sring ApplicationContext容器。
2.1.1 Spring BeanFactory容器
这个容器接口在org.springframework.beans.factory.BeanFactor中被定义。BeanFactory中有大量的接口被实现,最常用的就是XmlBeanFactory类。它可以从一个XML文件中读取元数据,获取Bean。
例如我们声明了一个Spring的xml配置文件为Beans.xml,实体类为HelloWorld.java
<bean id="helloWorld" class="com.tutorialspoint.HelloWorld"> <property name="message" value="Hello World!"/> </bean>
我们使用XmlBeanFactory进行读取Beans.xml,并创建HelloWorld的方式为:
XmlBeanFactory factory = new XmlBeanFactory(new ClassPathResource("Beans.xml")); HelloWorld obj = (HelloWorld) factory.getBean("helloWorld"); //HelloWorld被Bean设置的Id为"helloWorld"
分析一下:
第一步就是利用Spring框架提供的XmlBeanFactory()API生成工厂Bean并利用 ClassPathResource() API 去加载在路径 CLASSPATH 下可用的 bean 配置文件。在此时XmlBeanFactory()初始化所有的Bean对象。
第二步就是利用第一步生成Bean工厂对象,利用getBean()方法获取所需要的Bean。该方法是通过配置文件中的Bean Id来返回一个真正的对象。
2.1.2 Spring ApplicationContext 容器
说到ApplicationContext容器,其实它是BeanFactory 的子接口。称为Spring上下文。
接口:org.springframework.context.ApplicationContext interface中被定义。
最常被使用的 ApplicationContext 接口实现:
- FileSystemXmlApplicationContext:该容器从 XML 文件中加载已被定义的 bean。在这里,你需要提供给构造器 XML 文件的完整路径。
- ClassPathXmlApplicationContext:该容器从 XML 文件中加载已被定义的 bean。在这里,你不需要提供 XML 文件的完整路径,只需正确配置 CLASSPATH 环境变量即可,因为,容器会从 CLASSPATH 中搜索 bean 配置文件。
- WebXmlApplicationContext:该容器会在一个 web 应用程序的范围内加载在 XML 文件中已被定义的 bean。
例如我们声明了一个Spring的xml配置文件为Beans.xml,实体类为HelloWorld.java
<bean id="helloWorld" class="com.tutorialspoint.HelloWorld"> <property name="message" value="Hello World!"/> </bean>
我们使用FileSystemXmlApplicationContext进行读取Beans.xml,并创建HelloWorld的方式为:
ApplicationContext context = new FileSystemXmlApplicationContext("C:/Users/ZARA/workspace/HelloSpring/src/Beans.xml"); HelloWorld obj = (HelloWorld) context.getBean("helloWorld");
其他两个同理。不过是路径不同。
分析一下:
第一步生成工厂对象。加载完指定路径下 bean 配置文件后,利用框架提供的 FileSystemXmlApplicationContext API 去生成工厂 bean。FileSystemXmlApplicationContext 负责生成和初始化所有的对象,比如,所有在 XML bean 配置文件中的 bean。
第二步用第一步生成的上下文中的 getBean() 方法得到所需要的 bean。 这个方法通过配置文件中的 bean ID 来返回一个真正的对象。一旦得到这个对象,就可以利用这个对象来调用任何方法。
2.2 IOC容器对Bean的管理
由2.1 我们可以知道在Bean中定义了一个HelloWorld对象。就来详细研究一下,Bean的定义、作用域、生命周期。
2.2.1 SpringBean定义
属性 | 描述 |
class | 这个属性是强制性的,并且指定用来创建 bean 的 bean 类。 |
name | 这个属性指定唯一的 bean 标识符。在基于 XML 的配置元数据中,你可以使用 ID 和/或 name 属性来指定 bean 标识符。 |
scope | 这个属性指定由特定的 bean 定义创建的对象的作用域。 |
constructor-arg | 基于构造函数的依赖注入。 |
properties | 基于设置函数的依赖注入。 |
autowiring mode | 自动装配实现 |
lazy-initialization mode | 延迟初始化的 bean 告诉 IoC 容器在它第一次被请求时,而不是在启动时去创建一个 bean 实例。 |
initialization 方法 | 在 bean 的所有必需的属性被容器设置之后,调用回调方法。 |
destruction 方法 | 当包含该 bean 的容器被销毁时,使用回调方法。 |
Spring和Bean之间的关系图:
2.2.2 配置数据元供给Spring
(1)基于 XML 的配置文件
(2)基于注解的配置
(3)基于 Java 的配置
让我们一一来看一下:
(1)基于 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-3.0.xsd"> <!-- 基础的Bean对象加载方式 --> <bean id="..." class="..."> <!-- collaborators and configuration for this bean go here --> </bean> <!-- Bean的懒加载方法 --> <bean id="..." class="..." lazy-init="true"> <!-- collaborators and configuration for this bean go here --> </bean> <!-- Bean初始化方法 --> <bean id="..." class="..." init-method="..."> <!-- collaborators and configuration for this bean go here --> </bean> <!-- Bean的销毁方法 --> <bean id="..." class="..." destroy-method="..."> <!-- collaborators and configuration for this bean go here --> </bean> <!-- more bean definitions go here --> </beans>
对于Spring Bean的定义,属于默认空间命名,没有空间名。
xmlns="http://www.springframework.org/schema/beans"
对于xsi来说,它是一个标准命名空间。这个命名空间为每个文档中指定相应的Schema文件,这是标准组织定义的标准命名空间。
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
(2)基于注解的配置
自从Spring2.5之后,Spring就可以用配置注解的方式进行依赖注入。该种方式无需在xml中配置Bean引用。
注解配置默认在Spring中是关闭的,需要在配置文件中进行打开。(引入context空间)
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd"> <context:annotation-config/> </beans>
下面就可以使用注解,解释几个常用的重要注解:
@Required
注解应用于bean属性的setter方法,用来检查是否已经设置了所有必需的属性。@Autowired
它可以对类成员变量、方法及构造函数进行标注,完成自动装配的工作。@Qualifier
,通过指定确切的将被引用的bean,@Autowired
和@Qualifier
注解可以用来删除混乱- SR-250 Annotations,Spring支持JSR-250的基础的注解,其中包括了
@Resource
,@PostContruct
和@PreDestory
注解
简介项目:
pom.xml文件中引入了
spring-core spring-beans spring-context junit
package com.mcb.spring.vo; public class HelloWorld { private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } public void sayHello(){ System.out.println(name+"说: Hello World"); } }
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <context:annotation-config /> <bean id="hello" class="com.mcb.spring.vo.HelloWorld"> <property name="name" value="Sum"/> </bean> </beans>
import com.mcb.spring.vo.HelloWorld; import org.junit.Test; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class TestSpring { @Test public void sayHello(){ ApplicationContext context = new ClassPathXmlApplicationContext("Beans.xml"); HelloWorld hello = (HelloWorld)context.getBean("hello"); hello.sayHello(); } }
A. @Required
将该注解加到setter 方法中:
@Required public void setName(String name) { this.name = name; }
此时如果我们将Beans.xml中name的<property>属性注释掉,我们会发现class报错。并提示"必须的参数缺失"。
如果我们将注解@Required去掉,并且同样注释参数,运行程序。运行成功,但是参数为属性name的值为 null。
B.@Autowired
1,setter方法中的@Autowired
我们稍微改造一下上面的工程,添加Person类
Person类:
package com.mcb.spring.vo; public class Person { private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } }
定义Beans.xml。将Person放在Spring进行管理。
<context:annotation-config /> <bean id="hello" class="com.mcb.spring.vo.HelloWorld"> <!--<property name="name" value="Sum"/>--> </bean> <bean id="person" class="com.mcb.spring.vo.Person"> <property name="name" value="Tom"/> </bean>
将Person对象通过setter方法进行注入。
package com.mcb.spring.vo; import org.springframework.beans.factory.annotation.Autowired; public class HelloWorld { private String name; public String getName() { return name; } @Autowired // 将Person类注入进来 public void setName(Person person) { this.name = person.getName(); } public void sayHello(){ System.out.println(name+"说: Hello World"); } }
运行程序,让我们看一下结果:
2,同理在属性中和构造方法中@Autowired ,原理一致。都可以将Person对象注入到HelloWorld中,进行name输出。
@Autowired // 将Person类注入进来 private Person person;
@Autowired public HelloWorld(Person person) { this.name = person.getName(); }
C.@Qualifier
结合使用@Qualifier
和@Autowired
注解通过指定哪一个真正的bean将会被装配来消除混乱。
还是以上图工程为例:配置同一个Preson,两个不同对象Bean实例。
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <context:annotation-config /> <bean id="hello" class="com.mcb.spring.vo.HelloWorld"> <!--<property name="name" value="Sum"/>--> </bean> <bean id="person1" class="com.mcb.spring.vo.Person"> <property name="name" value="Tom"/> </bean> <bean id="person2" class="com.mcb.spring.vo.Person"> <property name="name" value="Jim"/> </bean> </beans>
在HelloWorld中进行注入
package com.mcb.spring.vo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; public class HelloWorld { @Autowired @Qualifier("person2") private Person person; private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } public void sayHello(){ System.out.println(person.getName()+"说: Hello World"); } }
运行测试类,进行打印:
D. SR-250 Annotations
在此我们了解一下一个注解@Resource。
上图使用了 @Autowired 和Qualifier来进行了注入。
@Autowired @Qualifier("person2") private Person person;
我们还可以使用@Resource进行注入。
@Resource(name = "person2") private Person person;
可以达到同样的效果。
他们的区别如下:
- @Autowired注解为Spring提供的注解,只按照byType方式注入,默认情况下,它要求依赖对象必须存在,如果允许为null,可以设置它的required属性为false,如果我们想按照byName方式来装配,可以结合@Qualifier注解一起使用;
- @Resource为J2EE提供的注解,它有两个重要的属性:name和type。而默认情况下,@Resource注解按照byName的方式来装配。@Resource的装配顺序是这样的: 如果同时指定了name和type,则从Spring上下文中找到唯一匹配的bean进行装配,找不到则抛出异常。如果指定了name,则从上下文中查找名称(id)匹配的bean进行装配,找不到则抛出异常。如果指定了type,则从上下文中找到类型匹配的唯一bean进行装配,找不到或是找到多个,都会抛出异常。如果既没有指定name,又没有指定type,则自动按照byName方式进行装配;如果没有匹配,则回退为一个原始类型进行匹配,如果匹配则自动装配。
(3)基于 Java 的配置
Spring中为了减少XML配置,可以声明一个配置类类对bean进行配置,主要用到两个注解@Configuration和@bean
第一步:在Spring中开启包扫描。
<beans> <context:component-scan base-package="com.mcb.spring.vo"/> </beans>
第二步:建立SpringConfig类。
package com.mcb.spring.config; import com.mcb.spring.vo.HelloWorld; import com.mcb.spring.vo.Person; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class SpringConfig { @Bean public HelloWorld helloWorld(Person person){ HelloWorld helloWorld = new HelloWorld(); helloWorld.setName(person.getName()); return helloWorld; } @Bean public Person person() { Person person = new Person(); person.setName("Jim"); return person; } }
第三步:测试类使用AnnotationConfigApplicationContext获取ApplicationContext类。
import com.mcb.spring.config.SpringConfig; import com.mcb.spring.vo.HelloWorld; import org.junit.Test; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; public class TestSpring { @Test public void sayHello(){ ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class); HelloWorld hello = (HelloWorld)context.getBean(HelloWorld.class); hello.sayHello(); } }
运行正常。
在第一步中,也可以用@ComponentScan注解代替xml的方式进行。
@Configuration @ComponentScan(basePackages = "com.mcb.spring.config") public class SpringConfig { ...... }
温馨提示:什么时候下使用什么样的注解呢?
2.2.3 Spring Bean的作用域
当Spring定义一个Bean时,必须声明该作用域的选项。目前支持五个作用域,分别是:singleton、prototype、request、session、global session。
用例: bean属性:scope
<bean id="..." class="..." scope="singleton"> <!-- collaborators and configuration for this bean go here --> </bean>
2.2.4 Spring Bean 生命周期
Bean的生命周期可以表达为: Bean的定义--> Bean的初始化--> Bean的使用 --> Bean的销毁
A。Bean的初始化回调函数。(实现InitializingBean)
import org.springframework.beans.factory.InitializingBean;
public void afterPropertiesSet() throws Exception { System.out.println("我是Init回调函数"); }
B。Bean的销毁回调函数。(DisposableBean)
import org.springframework.beans.factory.DisposableBean;
public void destroy() throws Exception { System.out.println("我是destroy回调函数"); }
C。Bean的初始化方法、销毁方法自定义。
public void destoryMy() { System.out.println("我是销毁方法"); }
public void init() { System.out.println("我是初始化方法"); }
<bean id="helloWorld" class="com.mcb.spring.vo.HelloWorld" init-method="init" destroy-method="destoryMy"> </bean>
将A、B、C同时配置后,结果如下:
我是Init回调函数 我是初始化方法 Jim说: Hello World 我是destroy回调函数 我是销毁方法
温馨提示:InitializingBean和DisposableBean的回调方法尽量不要使用,因为XML配置在命名方法上提供了极大的灵活性。
调用destory的时候,需要关闭hook的registerShutdownHook();
ApplicationContext context = new ClassPathXmlApplicationContext("Beans.xml"); HelloWorld hello = (HelloWorld)context.getBean("helloWorld"); hello.sayHello(); ((ClassPathXmlApplicationContext) context).registerShutdownHook();
默认初始化和销毁方法(Beans 标签下)