IOC即 Inversion of Control,译为控制反转。
IoC is also known as dependency injection (DI). It is a process whereby objects define their dependencies (that is, the other objects they work with) only through constructor arguments, arguments to a factory method, or properties that are set on the object instance after it is constructed or returned from a factory method. The container then injects those dependencies when it creates the bean. This process is fundamentally the inverse (hence the name, Inversion of Control) of the bean itself controlling the instantiation or location of its dependencies by using direct construction of classes or a mechanism such as the Service Locator pattern.
IOC与大家熟知的依赖注入同理,. 这是一个通过依赖注入对象的过程 也就是说,它们所使用的对象,是通过构造函数参数,工厂方法的参数或者是从工厂方法的构造函数或返回值的对象实例设置的属性,然后容器在创建bean时注入这些需要的依赖。 这个过程相对普通创建对象的过程是反向的(因此称之为IoC),bean本身通过直接构造类来控制依赖关系的实例化或位置,或提供诸如服务定位器模式之类的机制。
要想搞明白IOC,就要理解如下几个概念:
1、谁控制谁:第三方Spring IOC容器控制我们编程过程中所依赖的bean对象。
2、控制什么:我们编程实现功能所依赖的bean对象。
3、什么是反转:起初我们所依赖的所有bean对象都需要我们手动new创建,现在交由Spring IOC容器统一来管理。
4、哪些方面被反转:依赖对象的创建、管理、和维护。
总结:本来我们要创建对象的时候,都是要自己手动new出对象,但是如果使用springIOC容器的话,我们就不需要手动的去new对象,而是把new对象的过程交给springIOC容器来管理,即为控制反转。
如果这个过程比较难理解的话,那么可以想象自己找女朋友和婚介公司找女朋友的过程:本来找女朋友是需要先有这么个人,然后你去和她建立联系,然后看电影、吃饭培养感情,才可以结婚,这整个过程都是需要你主动去做的,这个过程称为正向;如果你工作比较忙,没有时间去找女朋友,这时可以找婚介公司,你提出找女朋友的要求:女的、活的。然后婚介公司就可以根据要求找人,你事先无需知道是否有这么一个人,婚介公司找到人之后,你只需要按照婚介公司的安排,什么时候请人吃饭,什么时候请人看电影,餐桌和电影票婚介公司都帮你准备好,整个复杂的安排过程都不需要你管,由婚介公司搞定,这个过程就称为反转。
很多人把IOC和DI(Dependency Injection:依赖注入)说成一个东西,笼统来说的话是没有问题的,但是本质上还是有所区别的,希望大家能够严谨一点,IOC和DI是从不同的角度描述的同一件事,IOC是从容器的角度描述,而DI是从应用程序的角度来描述,也可以这样说,IOC是设计思想,而DI是具体的实现方式。
使用XML配置文件的注入bean对象的时候,一定要将配置文件添加到类路径中,如果使用idea,要把XML配置文件放在resource目录下即可,否则会出现XML配置文件无法扫描到的问题。
注意:如果使用XML配置文件方式注入bean对象,当bean对象很多,XML文件可能长达几千行,一旦出错,排查起来要人命,所以现在生产环境中很少使用XML方式注入bean。实际生产环境中一般使用注解@autowired**来自动注入我们所需要依赖的对象。
使用XML注入bean对象都要使用bean标签,每一个bean标签对应着一个bean对象,该标签的常用参数:
id:bean对象的唯一标识,为了与其他bean对象区分。
注意:id标签的赋值是唯一的标识,不可以重复,否则注入对象的时候无法正常注入。
class:标识要创建的bean的完全限定名。
scope:设置此bean对象是单例还是多例。
注意:
如果是singleton的作用域的话,每次在IOC容器创建之后此对bean象就已经创建完成。
如果是prototype的作用域的话,每次都是需要用到此对象的时候才会创建
init-mothed:在对象创建完成之后,调用指定初始化方法,可以用作日志信息等
destory-mothed:在容器关闭的时候,才会调用指定销毁方法
通过Property标签赋值
给bean对象的属性赋值可以用property标签,也是使用XML方式的时候,最常用的属性赋值方式,较重要,要掌握。
该标签的属性有两个:
name:标识属性的名称。
value:标识要给属性赋予的属性值。
<bean id="person" class="com.mjt.Person">
<property name="id" value="1"></property>
<property name="name" value="老马"></property>
<property name="age" value="18"></property>
</bean>
注意:value的值我们练习的时候都是直接赋予,实际上应该是根据从数据库中查询的数据,或者从别的配置文件注入的数据,而不是自己手动赋值。
通过构造器赋值
实际工作中较少用,了解即可。
<bean id="person2" class="com.mjt.bean.Person" scope="prototype">
<constructor-arg name="id" value="2"></constructor-arg>
<constructor-arg name="name" value="老魏"></constructor-arg>
<constructor-arg name="age" value="18"></constructor-arg>
</bean>
使用构造器赋值的时候,细节点很多,没必要全部掌握,因为也比较少用,了解概念即可。比如当有多个相同参数的构造方法的时候,默认情况下,后面的构造方法会覆盖掉前面的构造方法,也就是说实际上创建的bean对象是根据后定义的构造方法赋值的,其他细节点还有很多。
通过P命名空间赋值
使用P命名空间,其实就是提供了一种更为简单的写法,仅此而已,很少使用,了解即可。
首先需要引入P命名空间
xmlns:p="http://www.springframework.org/schema/p"
然后使用p命名标签给属性赋值
<bean id="person4" class="com.mjt.Person"
p:id="3"
p:name="赵六"
p:age="18"
</bean>
为复杂类型的赋值
以上我们的实体类的属性都是基础类型,比如int、String等,进行赋值很好操作。如果实体类的属性比较复杂,如String[] 数组,Address(另一个实体类),对于这样的属性应该如何赋值?
给复杂类型的赋值都在property标签内进行,通过在propertity标签的内部进行各标签的嵌套使用,完成对复杂类型属性的赋值操作。
了解应该怎样为复杂类型赋值的思路即可,这里可以重点掌握如何引用外部bean对象,比如数据库连接的时候,会用的到。
<!-- address 的bean对象的赋值 -->
<bean id="address" class="com.mashibing.bean.Address">
<property name="province" value="河北"></property>
<property name="city" value="邯郸"></property>
<property name="town" value="武安"></property>
</bean>
<!--给复杂类型的赋值都在property标签内进行-->
<bean id="person" class="com.mashibing.bean.Person">
<!--通过ref引用其他对象,引用外部bean-->
<property name="address" ref="address"></property>
<!--引用内部bean-->
<!-- <property name="address">
<bean class="com.mashibing.bean.Address">
<property name="province" value="北京"></property>
<property name="city" value="北京"></property>
<property name="town" value="西城区"></property>
</bean>
</property>-->
<!--给数组赋值-->
<property name="hobbies">
<array>
<value>book</value>
<value>movie</value>
<value>game</value>
</array>
</property>
<!--给set赋值-->
<property name="sets">
<set>
<value>111</value>
<value>222</value>
<value>222</value>
</set>
</property>
<!--给其他复杂类型赋值同理,限于篇幅不做演示-->
</bean>
自己实现自己的FactoryBean,自己可以进行管理,这里只是简单创建对象,实际上可以进行灵活的扩展。
我们平时也很少用到这种方式,但是Spring源码很多都是通过FactoryBean这种方式来实现的,理解Factory(工厂设计模式)的思想很重要。
/**
* 通过FactoryBean创建bean对象
* 可以通过泛型指定此Factory只生产什么类型的实例
* 此Factory生产的bean对象我们可以自己灵活控制,也可以选择交给IOC容器管理
* 选择权在我们怎么处理,而不再完全由IOC容器主导,此方式是Spring创建bean对象的一种补充,可以按需创建对象。
*/
public class MyFactoryBean implements FactoryBean<Person> {
@Override
//获取bean对象
public Person getObject() throws Exception {
Person person = new Person(50,"老王",20);
return person;
}
@Override
public Class<?> getObjectType() {
return Person.class;//返回对象的类型
}
@Override
public boolean isSingleton() {
return true;//判断是否单例
}
}
也可以把FactoryBean交给Spring IOC容器管理
<!--把FactoryBean交给IOC容器来管理-->
<bean id="myFactoryBean" class="com.mjt.factory.MyFactoryBean"></bean>
获取bean对象
//可以自己获取Factory生产的对象,以往交由Spring管理的bean对象只能通过applicationContext获取
MyFactoryBean myFactoryBean = new MyFactoryBean();
Person person2 = myFactoryBean.getObject();
person2.setId(51);//可以自己灵活的修改bean的属性
System.out.println(person2);
//获取IOC容器管理的bean对象
Person person3 = context.getBean("myFactoryBean", Person.class);
System.out.println(person2 == person3);
我们开发过程中,必须要与数据库打交道,此时需要建立数据库连接,需要用到第三方数据库连接池对象,此对象一般是交由Spring管理,下面演示如何用XML文件的方式管理第三方数据库连接池Bean对象:
首先导入相关依赖
<!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.21</version>
</dependency>
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.14</version>
</dependency>
在XML文件中配置第三方bean
<!--Spring管理第三方bean-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="username" value="root"></property>
<property name="password" value="123456"></property>
<property name="url" value="jdbc:mysql://localhost:3306/demo?serverTimezone=UTC"></property>
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"></property>
</bean>
使用第三方bean
DruidDataSource dataSource = context.getBean("dataSource", DruidDataSource.class);
System.out.println(dataSource);
System.out.println(dataSource.getConnection());
我们在开发过程中经常需要引入外部配置文件,如数据库连接配置文件,引入第三方配置文件通常是通过XML文件进行配置
1、首先需要在XML配置文件中引入context命名空间:需要设置其中包含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.xsd">
2、编写dp.properties数据库连接配置文件
注意:由于获取配置文件的属性的时候,是通过${}的方式,此方式会查找全局变量,可能会造成获取username为计算机名称等意外情况,所以最好加上jdbc的前缀来做区分。
db.properties
jdbc.username=root
jdbc.password=123456
jdbc.url=jdbc:mysql://localhost:3306/demo?serverTimezone=UTC
jdbc.driverClassName=com.mysql.cj.jdbc.Driver
3、在XML配置文件中配置bean对象:value要和外部配置文件严格对应。
<!--引入外部配置文件:如数据库连接配置文件-->
<context:property-placeholder location="db.properties"></context:property-placeholder>
<bean id="dataSource2" class="com.alibaba.druid.pool.DruidDataSource">
<property name="username" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
<property name="url" value="${jdbc.url}"></property>
<property name="driverClassName" value="${jdbc.driverClassName}"></property>
</bean>
当一个bean对象需要引用另一个bean对象的时候,在之前的配置中我们可以通过引用外部bean对象或者通过properties进行手动配置,如下:
<!--给复杂类型的赋值都在property标签内进行-->
<bean id="person" class="com.mashibing.bean.Person">
<!--通过ref引用其他对象,引用外部bean-->
<property name="address" ref="address"></property>
<!--配置内部bean-->
<property name="address">
<bean class="com.mashibing.bean.Address">
<property name="province" value="北京"></property>
<property name="city" value="北京"></property>
<property name="town" value="西城区"></property>
</bean>
</property>
</bean>
其实在XML方式中还提供了一个非常强大的功能就是自动装配,可以按照我们指定的规则进行配置,通过在bean标签中的autowired参数进行配置,属性值有以下几种,我们可以灵活设置:
default/no:不自动装配
byName:按照名字进行装配,根据set()方法来进行装配。
byType:按照bean类型进行装配,如果一个实体类有多个bean对象,此时无法判断加载哪一个bean,就会报错:expected single matching bean but found 2。
constructor:按照构造器进行装配,先按照有参构造器参数的类型进行装配,没有就直接装配null;如果按照类型找到了多个,那么就使用参数名作为id继续匹配,找到就装配,找不到就装配null。了解即可,较少用。
ioc.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 id="address" class="com.mashibing.bean.Address">
<property name="province" value="河北"></property>
<property name="city" value="邯郸"></property>
<property name="town" value="武安"></property>
</bean>
<bean id="person" class="com.mashibing.bean.Person" autowire="byName"></bean>
<bean id="person2" class="com.mashibing.bean.Person" autowire="byType"></bean>
<bean id="person3" class="com.mashibing.bean.Person" autowire="constructor"></bean>
</beans>
ApplicationContext表示IOC容器的入口,如果想要获取IOC容器管理的bean对象的话,必须要使用该类,该类有两个读取配置文件的的实现子类:
注意:每次获取IOC管理的bean对象,都要使用这两个对象之一,常用ClassPathXmlApplicationContext对象,因为此对象是根据classpath寻找配置文件,在Windows和Linux都可用;而FileSystemXmlApplicationContext在Linux下未必可用,当把项目部署到Linux服务器的时候,还要更改文件目录,比较麻烦,不推荐使用。
根据bean标签的id,可以准确的获取到唯一的bean对象,获取bean对象通过context.getBean方法,此方法可以传入1个参数,也可以传入两个参数:
ApplicationContext context = new ClassPathXmlApplicationContext("ioc.xml");
//获取具体的bean实例对象,需要进行强制类型转换
// Person person = (Person) context.getBean("person");
//获取对象的时候不需要强制类型转换
Person person = context.getBean("person", Person.class);
通过bean的type类型也可以获得IOC容器中的bean对象:
//通过bean的类型获取容器中bean对象
Person bean = context.getBean(Person.class);
System.out.println(bean);
注意:通过bean的类型获取bean对象的时候,不能包含相同类型的多个对象,如果找到多个相同类型的对象,此时无法判断究竟使用哪一个对象,就会出错。错误信息:expected single matching bean but found 2。
如上Person类装配了两个bean对象person、person2,通过bean的type获取的时候报错信息如下:
//通过bean的type类型获取容器中bean对象
Person bean = context.getBean(Person.class);
System.out.println(bean);
Exception in thread "main" org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'com.mjt.bean.Person' available: expected single matching bean but found 2: person,person2
总结:获取bean对象有两种方法:1、通过bean的id获取;2、通过bean的type获取。其中通过id获取bean对象的时候,每个id唯一标识一个bean对象;通过type获取的时候,要保证此类型只有一个bean对象。推荐使用id一一对应的方式,一般不会出错。
bean对象在IOC容器中存储的时候默认情况下都是单例的。
ApplicationContext context = new ClassPathXmlApplicationContext("ioc.xml");
Person person = context.getBean("person", Person.class);
Person person2 = context.getBean("person", Person.class);
//IOC容器管理的bean对象默认都是单例的,如果需要多例需要修改属性
System.out.println(person == person2);//因为是单例的,结果为true
如果需要多例,则需要修改属性:通过修改scope属性为protopyte,设为非单例。
<bean id="person" class="com.mjt.bean.Person" scope="prototype">
<property name="id" value="100"></property>
<property name="name" value="老马"></property>
<property name="age" value="18"></property>
</bean>
ApplicationContext context = new ClassPathXmlApplicationContext("ioc.xml");
Person person = context.getBean("person", Person.class);
Person person2 = context.getBean("person", Person.class);
//IOC容器管理的bean对象默认都是单例的,如果需要多例需要修改属性
System.out.println(person == person2);//修改为多例,结果为false
bean对象是什么时候创建的?
单例的bean对象在容器创建完成的时候就已经把对象创建好了,并不是需要用到bean对象的时候才开始创建;如果修改作用域为prototype多例,则是需要用到bean对象的时候才会创建。
bean对象是什么时候销毁的?
通过destroy-mothed参数指定的方法的执行时机,我们可以判断bean对象是在容器关闭的时候才会被销毁。
所谓的生命周期就是bean对象从创建到IOC容器关闭。
创建对象给属性赋值的时候是通过setter方法实现的。如果没有setter方法,是不能够给属性赋值成功的。
Spring IOC容器的底层实现是通过反射,反射默认调用的是无参构造方法,如果实体类不包含默认无参构造函数,是无法正常创建bean对象的,所以一定要保留默认无参构造方法。
/**
* 当需要从容器中获取对象的时候,最好要保留无参构造方法,因为底层的实现是反射
*
* Class clazz = Person.class;
* Object obj = clazz.newInstance();反射默认调用无参的构造方法
*
*/
BeanFactory更多的功能是一个接口,定义了一个规范,里面提供了很多大家都要遵守的方法;
FactoryBean是用来获取我们唯一的bean对象的。我们使用普通的IOC容器管理的bean对象,在起始的时候我们进行赋值过后,如果在运行过程中想要对属性值修改,是不能实现的。而Factory提供了一种更加灵活的方式,能让自己管理自己创建的对象,也支持把自己创建的对象交给IOC容器来管理。
原来对象都是由Spring来控制,创建也好,销毁也好。现在我们自己new出来一个对象,然后可以把自己创建的对象交给IOC来管理。只是有这种比较灵活的方式而已,一般也很少用。
Spring中包含一个BeanPostProcessor的接口,该接口有两个方法,相当于配置了初始化方法的前置和后置处理器,在bean对象初始化前后进行调用,用来增强业务处理逻辑,比如用作日志。无论是否包含init初始化方法,都会进行调用。
MyBeanPostProcesser.java
public class MyBeanPostProcessor implements BeanPostProcessor {
/**
* 在初始化方法调用之前执行
*/
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
System.out.println("postProcessBeforeInitialization:"+beanName+"调用初始化前置方法");
return bean;
}
/**
* 在初始化方法调用之后执行
*/
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
System.out.println("postProcessAfterInitialization:"+beanName+"调用初始化后缀方法");
return bean;
}
}
ioc.xml
<bean id="myBeanPostProcessor" class="com.mashibing.bean.MyBeanPostProcessor"></bean>
像这样把增强处理bean配置在XML文件中,是全局生效,也就是说所有创建的bean对象在初始化前后都会执行PostProcessor的增强处理。
我们平时使用Spring中较少使用,但是在SpringBoot源码机制中广泛使用,这里做了解就行。