一、什么是IOC?
IOC(Inversion of Control)
即控制反转,要理解什么叫控制反转,我们就需要知道这里的控制是指对什么的控制?反转又是如何进行反转的?
直接上结论,控制指的是对资源获取方式的控制,反转指的是从主动获取资源变为被动接收资源。 意思就是,控制反转指对资源的获取方式从主动获取变为被动接收。那么,为什么要做出这种改变?这种改变又是怎么实现的呢?
首先说说为什么要做出这种改变?传统模式中,资源都是new出来的,需要什么资源就自己创建(new)什么资源,这种方式就叫做主动获取。这种方式的缺点请参考下文:【我们到底为什么要使用IOC】
总结来说,传统模式主要有三大弊端:
创建了许多重复对象,造成大量资源浪费;
代码耦合度过高,更换实现类需要改动多个地方,不利于修改、维护和拓展;
创建和配置组件工作繁杂,给组件调用方带来极大不便。
在Spring之前,聪明的开发者尝试使用工厂模式来解决上述问题。具体什么是工厂模式请参考下文:【什么是工厂模式】
了解了什么是工厂模式之后,我们会发现,虽然工厂模式可以把对象的创建和使用过程分开,以此来降低代码的耦合度,但是不管是普通工厂模式还是抽象工厂模式,多多少少都有不可避免的弊端。简单工厂模式容错性较差,抽象工厂模式开发复杂度较高。。。
可能有人会说,那对象工厂和业务代码还是会耦合呀。emmm。。。是的,工厂模式只能降低代码耦合度,并不能去耦合。代码耦合度是必然存在的,我们要做的是通过某种方式尽可能的降低代码的耦合度,使代码更容易维护和拓展。Spring就很大程度解决了开发复杂和代码耦合度高的问题。
Spring是如何做到的呢?Spring做的就是将资源的获取方式从主动式变为被动式(控制反转)。被动式意思就是资源不是由我们来创建,而是交给一个容器来创建和设置,容器管理所有的组件(有功能的类)。
容器可以自动探查出哪些组件(类)需要用到另外的哪一些组件(类);这个组件运行的时候,需要另外一个组件,容器就通过反射的方式,将容器中准备好的对象注入(通过反射给属性赋值),这个过程就叫依赖注入(DI)。而且只要容器管理的组件,都可以使用容器强大的功能。
举个例子,下方代码中,假设UserInfoService
受容器管理,UserInfoDao
也受容器管理;容器帮我们创建好了UserInfoService
对象和UserInfoDao
对象,并且容器也能知道UserInfoService
运行的时候,需要用到UserInfoDao
,当UserInfoService
运行的时候,容器就把准备好的UserInfoDao
对象赋值过去,我们不需要new UserInfoDao()
,只需要接收容器给我们的UserInfoDao
,就可以直接使用UserInfoDao
中的insert
方法。这就是主动的new
资源变为了被动接收资源。
public class UserInfoService{
private UserInfoDao dao;
public void add(){
dao.insert();
}
}
public class UserInfoDao{
public void insert(){}
}
那么问题来了,Spring是怎么做到让我们不用new
的呢?又是怎么自动探查出哪些组件需要用到另外的哪一些组件的呢?容器还有哪些强大的功能呢?
二、IOC的底层原理
1. IOC底层实现
先说Spring是怎么做到让我们不用new
,就可以直接获取对象实例的。
Spring有一个xml配置文件,如果把Spring看做一个班级,这个配置文件就相当于一个花名册,花名册记载了这个班所有的学生信息。一个标签代表一个学生。把这种关系代入到代码中,就是一个
标签代表一个类,Spring会去扫描这个配置文件,获取到所有在它这里注册了的类,然后给每个类都创建一个实例。
所以如果想要让Spring来管理这个类,第一步就需要把这个类配置到Spring的配置文件当中,否则Spring不知道这个类是它班级的学生,他也就无权管理这个类。知道了这个类需要被管理,那Spring又是如何创建实例的呢?这里就用到了XML解析+工厂模式+反射,IOC思想基于IOC容器完成,IOC容器底层就是对象工厂。
// 1. 创建一个工厂类
public class UserFactory{
public static ZhangSan getZhangSan(){
// 2. 解析XML文件,得到class的属性值
String value = "解析XML"; // com.ysy.learn.ZhangSan
// 3. 通过反射的方式获取ZhangSan的字节码文件
Class clazz = Class.forName(value);
// 4. 创建ZhangSan
return (ZhangSan)clazz.newInstance();
}
}
容器已经帮我们创建好了对象,那我们如何从容器中获取出这个组件呢?要从容器中拿对象,当然需要先得到容器,ApplicationContext
就是IOC容器的实现
public class TestDemo{
public void testZhangSan(){
// 1. 根据Spring配置文件,得到IOC容器对象
ApplicationContext context = new ClassPathXmlApplicationContext("user.xml");
// 2. 获取对象实例
// Zhangsan zhangsan1 = (HelloSpring)context.getBean("zhangsan");
// Zhangsan zhangsan2 = context.getBean(Zhangsan.class);
Zhangsan zhangsan3 = context.getBean("zhangsan",Zhangsan.class);
}
}
2. IOC容器的实现方式
Spring提供了两种IOC容器的实现方式,一种是BeanFactory
接口,另一种是ApplicationContext
接口。两种方式都可以加载配置文件,创建IOC容器对象。
BeanFactory
接口是IOC容器的基本实现,是Spring内部的使用接口,不提供给开发人员使用(Spring自己用的),使用BeanFactory
接口在加载配置文件的时候不会创建对象,获取(使用)对象的时候才会创建对象
BeanFactory
需要关注的实现类有ConfigurableApplicationContext
,它包含相关的扩展功能
ApplicationContext
接口是BeanFactory
接口的子接口,提供更多强大的功能,一般由开发人员直接使用,使用ApplicationContext
接口在加载配置文件的时候就会把在配置文件中配置了的对象创建
ApplicationContext
需要关注的实现类有FileSystemXmlApplicationContext
和ClassPathXmlApplicationContext
。FileSystemXmlApplicationContext
取的是配置文件在系统盘中的路径(E:MySelfDataUser.xml);ClassPathXmlApplicationContext
取的是配置文件的类路径(src下)
为什么推荐使用ApplicationContext
而不是BeanFactory
?
BeanFactory
是在对象使用的时候才会创建对象,这样虽然可以节约资源,但是会降低程序运行效率。Spring一般和Web项目进行整合,需要tomcat启动,tomcat在启动的时候加载配置文件,使用ApplicationContext
,就相当于tomcat启动的时候就会把对象创建完成,程序运行的时候,不需要再创建对象,可以提高程序的运行效率。把耗时耗资源的过程交给服务器,这样更合理,所以项目中使用ApplicationContext
而不是BeanFactory
三、Bean管理
什么是Bean管理?Bean管理指的是两个操作:1.Spring创建对象;2.Spring注入属性。
那么具体怎么用Spring创建对象和注入属性呢?有两种方式,一种是基于xml配置文件方式实现,另一种是基于注解方式实现。
1. 基于xml配置文件方式
1.1 创建对象
id属性:唯一标识
class属性:创建对象所在类的全路径(包+类名)
name属性:作用和id属性一致,id中不能加特殊符号,name中可以使用特殊符号
基于XML配置文件方式创建对象,就是在spring配置文件中,使用标签,标签里面添加对应属性,就完成了对象的注册。
Spring默认执行无参构造方法完成对象的创建。如果在类中声明一个有参构造函数,系统将不会默认生成无参构造函数,此时配置文件会因为找不到无参构造函数而报错
1.2 注入属性
Spring提供了两种基于XML配置文件方式注入属性的方法,一种是属性的set()方法,一种是有参构造函数。
1.使用属性的set()方法示例:
是根据set方法来确定属性名的,如果将setName()改成setPname(),Spring默认得到的属性名就是pname,而不是name
简化set()方法注入:P名称空间注入
什么是名称空间?名称空间是用来防止标签重复的。
西游记
吴承恩
2.使用有参构造函数示例: 在使用有参构造注入属性的时候,Spring就不是默认使用无参构造函数创建对象了,而是使用有参构造函数创建对象
value属性:属性值
name属性:类里面的属性名称
index属性:有参构造函数中参数的位置,0代表第一个参数
以上为最基本的基于XML配置文件方式注入属性,此外还有其他为各种类型的属性赋值。
1.往属性中设置空值
如果不给属性赋值,属性默认就是null。所以标签的使用场景是如果属性原本有值,但是在操作中需要给这个属性赋值为null,才会用到这种写法。
2.属性值中包含特殊符号
3.注入属性—外部Bean
ref是一个严格的引用,它引用了外部的bean。如果有如下代码和配置,从IOC容器获取Car car1 = context.getBean("car")
,从Person中获取Car car2 = person.getCar()
,此时car1和car2是同一个对象。如果先Car car = person.getCar()
,之后修改car
的属性,再获取一次car
,获取到的对象属性是被修改过的属性。
public class Person{
private Car car;
public void setCar(String car) {
this.car = car;
}
public String getCar() {
return car;
}
}
public class Car{}
4.注入属性—内部Bean
内部Bean写id其实没用,只能内部使用,不能被全局使用。意思就是内部bean不能通过context.getBean("car")
获取到,只能用person.getCar()
5.注入属性—级联赋值
什么是级联属性?属性的属性就是级联属性。内部Bean的配置方式也是一种级联赋值,此外,使用表达式方式的级联赋值需要在Emp类中生成属性dept的get()方法,因为需要先得到dept对象实例,才能通过对象实例点出对象的name属性
6.注入属性—Properties类型
7.注入属性—集合类型
数组类型属性注入的时候,可以用标签,也可以用标签
如何注入对象集合?
如何给多个类重用同一个集合(抽取出作为公共部分,供多个对象使用)?
1.3 通过继承实现Bean配置信息的重用
1.4 通过abstract属性创建一个模板Bean
1.5 Bean之间的依赖
Bean的创建顺序是按照在配置文件中的顺序来创建的,可以用depends-on来标识类之间的依赖关系,改变类的创建顺序。只是改变类的创建顺序,并不是说没有book,person就不能用。若没有添加depends-on标识,类的创建顺序是Car、Person、Book,添加依赖之后,类的创建顺序是Car、Book、Person。另外在depends-on中写的顺序也会影响类的创建顺序。
1.6 自动装配(自动赋值)
自动装配就是自动为属性赋值,这个概念是基于自定义类型,基本类型不存在自动装配。如何实现自动装配呢?在注册Bean时,使用autowire
属性就可以为当前Bean配置自动装配。
autowire
属性有以下几个值:
default/no: 不进行自动装配
byName: 以属性名作为id到容器中找到这个组件,为它赋值,如果找不到就装配null
byType: 以属性类型作为查找依据去容器中找到这个组件,如果容器中有多个就报错,没有就装配null
constructor: 按照构造器进行赋值;1. 先找该类型的有参构造器,没有的话,直接装配null;2. 存在有参构造器就按照有参构造器的参数类型进行装配,如果没有该参数类型的组件,就直接装配null;3. 如果该类型的组件存在多个,就用参数名作为id继续匹配,找到就装配,找不到就装配null。这种用构造器装配的方式不会报错。
如果属性是一个Lsit,容器会把容器中所有的Book组件封装成list赋值给这个属性。
1.7 外部属性文件
我们一般习惯于将数据库连接信息抽取到配置文件中,然后直接获取配置文件中的值,便于修改和维护。假设我们有一个jdbc.properties文件,里面包含数据库连接信息
jdbc.username=root
jdbc.password=123456
jdbc.jdbcUrl=jdbc:mysql://localhost:3306/test
jdbc.driverClass=com.mysql.jdbc.Driver
那么Spring怎么获取外部的属性文件呢?
上面提到如果在配置文件中直接配置username
,获取不到数据库的连接,因为username
是Spring的key中的一个关键字,那么username
在Spring中到底代表什么呢?
给person
的name
属性赋值${username}
,打印发现,获取到的username
是计算机用户名
补充知识: 源码文件夹和普通文件夹
源码文件夹中的内容在编译之后会被放在编译文件的类路径下,普通文件夹中的内容不会放在类路径下,所以放在源码文件夹中的内容可以 直接使用classpath:
来取,但是放在普通文件夹中的内容使用这种方式是取不到的
2. 工厂Bean
有一个AirPlane
类,我们分别用静态工厂、实例工厂和FactoryBean
的方式创建AirPlane
实例
public class AirPlane {
public AirPlane() {、
System.out.println("AirPlane...的构造器");
}
private String fdj; // 发动机
private Double airPlaneHeight; // 飞机重量
private String jzName; // 机长名称
private String fjName; // 副驾名称
private Integer peopleNum; // 载人数
}
静态工厂: 工厂本身不用创建对象,通过静态方法调用,对象 = 工厂类.方法名()
public class AirPlaneStaticFactory {
public AirPlaneStaticFactory() {
System.out.println("静态工厂。。。。的构造器。");
}
public static AirPlane getAirPlane(String jzName){
AirPlane airPlane = new AirPlane();
airPlane.setAirPlaneHeight(1500.00);
airPlane.setFdj("发动机");
airPlane.setFjName("张三");
airPlane.setJzName(jzName);
airPlane.setPeopleNum(200);
return airPlane;
}
}
最终控制台输出的结果如下图。虽然配置文件中的class配置的是AirPlaneStaticFactory
,但是没有创建AirPlaneStaticFactory
的实例,因为没有打印AirPlaneStaticFactory
构造器中的语句,而是打印了AirPlane
构造器中的语句,说明创建了一个AirPlane
的实例,并且为AirPlane
实例中机长名称赋值为传入的参数李四。
实例工厂: 要先new
一个工厂类的实例,再调用工厂类的方法
public class AirPlaneInstanceFactory {
public AirPlaneInstanceFactory() {
System.out.println("AirPlaneInstanceFactory...的构造器");
}
public AirPlane getAirPlane(String jzName){
AirPlane airPlane = new AirPlane();
airPlane.setAirPlaneHeight(1500.00);
airPlane.setFdj("发动机");
airPlane.setFjName("张三");
airPlane.setJzName(jzName);
airPlane.setPeopleNum(200);
return airPlane;
}
}
最终控制台输出的结果如下图。先去创建了AirPlaneInstanceFactory
实例,之后再创建AirPlane
实例。虽然xml文件中配置了AirPlane
的标签,但是
AirPlane
不是容器通过反射的方式创建出来的,而是通过实例工厂的工厂方法创建出来的。
FactoryBean
:Spring规定的一个接口,只要是这个接口的实现类,Spring都认为是一个工厂
public class MyAirPlaneFactoryBean implements FactoryBean {
/**
* 工厂方法,返回创建的对象
* @return
* @throws Exception
*/
@Override
public AirPlane getObject() throws Exception {
AirPlane airPlane= new AirPlane();
airPlane.setJzName("王五");
return airPlane;
}
/**
* 返回创建的对象是什么类型
* Spring会自动调用方法来确认返回的对象是什么类型
* @return
*/
@Override
public Class getObjectType() {
return null;
}
/**
* 是否是单例
* false:不是单例
* true:是单例
* @return
*/
@Override
public boolean isSingleton() {
return true;
}
}
最终控制台输出结果如下图。我们可以发现,Spring先创建了MyAirPlaneFactoryBean
的实例,然后再去创建了AirPlane
的实例。那么MyAirPlaneFactoryBean
的实例和AirPlane
的实例分别是在什么时候创建的呢?
isSingleton()
方法return true
:代表创建单实例对象,然后我们只创建容器,不获取对象。
@Test
public void testBean2(){
ApplicationContext context = new ClassPathXmlApplicationContext("bean2.xml");
// AirPlane air1 = context.getBean("airPlane2", AirPlane.class);
// System.out.println(air1);
}
输出结果如下。只打印了调用MyAirPlaneFactoryBean
的构造器,说明只创建了MyAirPlaneFactoryBean
的实例,没有创建AirPlane
对象。
isSingleton()
方法return false
:代表创建多实例对象,然后我们只创建容器,不获取对象。输出结果如下。也是只打印了调用MyAirPlaneFactoryBean
的构造器。所以我们可以得出结论,我们通过FactoryBean
创建的对象,不管是单实例还是多实例,都是在获取实例的时候创建的,IOC容器启动的时候不会创建。
3. Bean的作用域
作用域限定了Spring Bean的作用范围,可以通过在配置文件中配置scope属性配置Bean的作用域。
目前Spring支持五种作用域:
1.singleton
singleton是默认的作用域,当定义Bean时,如果没有指定scope配置项,Bean的作用域被默认为singleton。singleton属于单例模式,在整个系统上下文环境中,仅有一个Bean实例。也就是说,在整个系统上下文环境中,你通过Spring IOC获取的都是同一个实例。
singleton作用域 示例:
在XML配置文件中未指定Bean的作用域(scope属性),之后在testBean()中两次获取book对象,打印对象地址,两次获取对象的地址是一致的,说明两次获取的是同一个对象实例。
2.prototype
当一个bean的作用域为Prototype,表示一个bean定义对应多个对象实例。这意味着每次对该bean请求(将其注入到另一个bean中,或者以程序的方式调用容器的getBean()方法)时都会创建一个新的bean实例。如果Bean的作用域配置为Prototype,那么在加载Spring配置文件的时候不会创建对象,而是在调用getBean()方法的时候创建多实例对象。根据经验,对有状态的bean应该使用prototype作用域,而对无状态的bean则应该使用singleton作用域。
prototype作用域示例:
在XML配置文件中指定Bean的作用域为Prototype,之后在testBean()中两次获取book对象,打印对象地址,两次获取对象的地址不一致,说明两次获取的不是同一个对象实例。
3.request
在web环境下,同一次请求创建一个Bean
4.session
在web环境下,同一次会话创建一个Bean
5.global-session
4. Bean的生命周期
从对象创建到对象销毁的过程就是对象的生命周期。
Bean的生命周期:
容器关闭,Bean销毁(需要在Spring配置文件中配置销毁方法)
自定义后置处理器,只需要实现BeanPostProcessor接口,之后在xml文件中配置自定义的类,Spring会自动识别这个后置处理器。postProcessBeforeInitialization
是在初始化方法之前调用的方法,可以在Bean被初始化之前做操作,postProcessAfterInitialization
是在初始化方法之后调用的方法,可以在Bean初始化之后,继续对Bean做其他操作。
public class MyBeanPostProcessor implements BeanPostProcessor {
/**
* 初始化方法之前调用的方法
* @param bean
* @param beanName
* @return
* @throws BeansException
*/
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
System.out.println("postProcessBeforeInitialization" + bean + "beanName:" + beanName);
return bean;
}
/**
* 初始化方法之后调用的方法
* @param bean
* @param beanName
* @return
* @throws BeansException
*/
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
System.out.println("postProcessAfterInitialization" + bean + "beanName:" + beanName);
return bean;
}
}
Bean的生命周期示例:
单实例对象定义后置处理器之前
单实例对象定义后置处理器之后
如果配置了后置处理器,就算Bean没有定义初始化方法,还是会执行后置处理器的方法
多实例对象定义后置处理器之后
多实例对象,Spring是不负责销毁的,就是说如果Bean的作用域配置的是prototype,那么容器关闭时,不会调用销毁方法。
5. 基于注解方式
什么是注解?注解是代码中特殊的标记,格式是@(属性名称=属性值,属性名称=属性值)
注解可以在什么地方用?注解可以作用在类、方法、属性上
为什么需要注解?为了让配置更加简洁,简化XML配置,用更优雅更简洁的方式实现XML配置
5.1 创建对象
Spring针对创建对象提供了4个注解,他们的功能是一样的,都可以快速的将组件加入到IOC容器的管理中。
@Controller:推荐给控制器组件添加(Servlet)
@Service:推荐给业务逻辑层组件添加(Service)
@Repository:推荐给数据库层的组件添加(Dao)
@Component:推荐给不属于已上几种的组件添加
Spring底层不会去验证这个组件是否如添加的注解描述是一个Dao
层还是一个Service
层,推荐不同的层添加不同的注解,是为了让开发者更明确而已。
所以具体如何使用注解创建一个对象呢?
第一步,创建一个类,给类上添加上述4个之中任意一个注解;
第二步,配置组件扫描,就是要告诉Spring,我要用注解创建对象,你帮我创建一下。这个过程就是去Spring的配置文件中,开启组件扫描。
第三步,组件扫描开启之后,运行程序,会报错,所以需要注意,要支持注解开发,一定要导入AOP的jar包。导入jar包之后,Spring就会通过注解的方式帮我们创建对象。
5.2 组件扫描配置
上面提到开启组件扫描,那么如何开启组件扫描,需要注意些什么呢?
5.3 注入属性
使用@Autowired
注解实现根据类型实现自动装配
/**
* 属性的自动注入
* @Autowired Spring会自动为这个属性赋值,去容器中找到这个属性对应的组件
* 用注解不需要为属性生成get、set方法
*/
@Controller
public class BookController {
// 自动装配,自动为这个属性赋值
@Autowired
private BookService bookService;
public void insert(){
bookService.insert();
}
}
@Service
public class BookService {
@Autowired
private BookDao bookDao;
public void insert(){
System.out.println("BookService insert....");
bookDao.insert();
}
}
@Repository
public class BookDao {
public void insert(){
System.out.println("BookDao insert.....");
}
}
1. @Autowired
的原理:
2. 方法上有@Autowired
1. 这个方法也会在Bean创建的时候自动运行
2. 这个方法的每一个参数都会注入值
3. 也可以在参数上加[@Qualifier]()
@Autowired
和@Resource
和@Inject
的区别:
1. @Autowired和@Qualifier是一起使用的
2. @Autowired是Spring自己的,功能更强大,但是离开了Spring就用不了了
3. @Resource是JavaEE的,Java的标准,扩展性更强,如果切换成另外的框架,@Autowired就用不了了
5.4 完全注解开发
完全注解开发就是我们不需要Spring的XML文件,而是利用一个类来实现Spring的XML文件的功能。