一、什么是IOC?

IOC(Inversion of Control)即控制反转,要理解什么叫控制反转,我们就需要知道这里的控制是指对什么的控制?反转又是如何进行反转的?

直接上结论,控制指的是对资源获取方式的控制,反转指的是从主动获取资源变为被动接收资源。意思就是,控制反转指对资源的获取方式从主动获取变为被动接收。那么,为什么要做出这种改变?这种改变又是怎么实现的呢?

首先说说为什么要做出这种改变?传统模式中,资源都是new出来的,需要什么资源就自己创建(new)什么资源,这种方式就叫做主动获取。这种方式的缺点请参考下文:【我们到底为什么要使用IOC】

总结来说,传统模式主要有三大弊端:

  1. 创建了许多重复对象,造成大量资源浪费;
  2. 代码耦合度过高,更换实现类需要改动多个地方,不利于修改、维护和拓展;
  3. 创建和配置组件工作繁杂,给组件调用方带来极大不便。

在Spring之前,聪明的开发者尝试使用工厂模式来解决上述问题。具体什么是工厂模式请参考下文:【什么是工厂模式】

了解了什么是工厂模式之后,我们会发现,虽然工厂模式可以把对象的创建和使用过程分开,以此来降低代码的耦合度,但是不管是普通工厂模式还是抽象工厂模式,多多少少都有不可避免的弊端。简单工厂模式容错性较差,抽象工厂模式开发复杂度较高。。。

可能有人会说,那对象工厂和业务代码还是会耦合呀。emmm。。。是的,工厂模式只能降低代码耦合度,并不能去耦合。代码耦合度是必然存在的,我们要做的是通过某种方式尽可能的降低代码的耦合度,使代码更容易维护和拓展。Spring就很大程度解决了开发复杂和代码耦合度高的问题。

Spring是如何做到的呢?Spring做的就是将资源的获取方式从主动式变为被动式(控制反转)。被动式意思就是资源不是由我们来创建,而是交给一个容器来创建和设置,容器管理所有的组件(有功能的类)。

容器可以自动探查出哪些组件(类)需要用到另外的哪一些组件(类);这个组件运行的时候,需要另外一个组件,容器就通过反射的方式,将容器中准备好的对象注入(通过反射给属性赋值),这个过程就叫依赖注入(DI)。而且只要容器管理的组件,都可以使用容器强大的功能。

举个例子,下方代码中,假设UserInfoService受容器管理,UserInfoDao也受容器管理;容器帮我们创建好了UserInfoService对象和UserInfoDao对象,并且容器也能知道UserInfoService运行的时候,需要用到UserInfoDao,当UserInfoService运行的时候,容器就把准备好的UserInfoDao对象赋值过去,我们不需要new UserInfoDao(),只需要接收容器给我们的UserInfoDao,就可以直接使用UserInfoDao中的insert方法。这就是主动的new资源变为了被动接收资源。

  1. public class UserInfoService{
  2. private UserInfoDao dao;
  3. public void add(){
  4. dao.insert();
  5. }
  6. }
  7. public class UserInfoDao{
  8. public void insert(){}
  9. }

那么问题来了,Spring是怎么做到让我们不用new的呢?又是怎么自动探查出哪些组件需要用到另外的哪一些组件的呢?容器还有哪些强大的功能呢?

二、IOC的底层原理

1. IOC底层实现

先说Spring是怎么做到让我们不用new,就可以直接获取对象实例的。

Spring有一个xml配置文件,如果把Spring看做一个班级,这个配置文件就相当于一个花名册,花名册记载了这个班所有的学生信息。一个<bean>标签代表一个学生。把这种关系代入到代码中,就是一个<bean>标签代表一个类,Spring会去扫描这个配置文件,获取到所有在它这里注册了的类,然后给每个类都创建一个实例。

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <beans xmlns="http://www.springframework.org/schema/beans"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
  5. <bean id="zhangsan" class="com.ysy.learn.ZhangSan"></bean>
  6. <bean id="lisi" class="com.ysy.learn.LiSi"></bean>
  7. <bean id="wangwu" class="com.ysy.learn.WangWu"></bean>
  8. </beans>

所以如果想要让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,它包含相关的扩展功能

2. IOC容器 - 图1

ApplicationContext接口是BeanFactory接口的子接口,提供更多强大的功能,一般由开发人员直接使用,使用ApplicationContext接口在加载配置文件的时候就会把在配置文件中配置了的对象创建

ApplicationContext需要关注的实现类有FileSystemXmlApplicationContextClassPathXmlApplicationContextFileSystemXmlApplicationContext取的是配置文件在系统盘中的路径(E:\MySelf\Data\User.xml);ClassPathXmlApplicationContext取的是配置文件的类路径(src下)

2. IOC容器 - 图2

为什么推荐使用**ApplicationContext**而不是**BeanFactory**

BeanFactory是在对象使用的时候才会创建对象,这样虽然可以节约资源,但是会降低程序运行效率。Spring一般和Web项目进行整合,需要tomcat启动,tomcat在启动的时候加载配置文件,使用ApplicationContext,就相当于tomcat启动的时候就会把对象创建完成,程序运行的时候,不需要再创建对象,可以提高程序的运行效率。把耗时耗资源的过程交给服务器,这样更合理,所以项目中使用ApplicationContext而不是BeanFactory

三、Bean管理

什么是Bean管理?Bean管理指的是两个操作:1.Spring创建对象;2.Spring注入属性。

那么具体怎么用Spring创建对象和注入属性呢?有两种方式,一种是基于xml配置文件方式实现,另一种是基于注解方式实现。

1. 基于xml配置文件方式

1.1 创建对象

<bean id="user" class="com.ysy.learn.UserInfoDao"></bean>

id属性:唯一标识
class属性:创建对象所在类的全路径(包+类名)
name属性:作用和id属性一致,id中不能加特殊符号,name中可以使用特殊符号

基于XML配置文件方式创建对象,就是在spring配置文件中,使用<bean>标签,标签里面添加对应属性,就完成了对象的注册。

Spring默认执行无参构造方法完成对象的创建。如果在类中声明一个有参构造函数,系统将不会默认生成无参构造函数,此时配置文件会因为找不到无参构造函数而报错

2. IOC容器 - 图3

1.2 注入属性

Spring提供了两种基于XML配置文件方式注入属性的方法,一种是属性的set()方法,一种是有参构造函数。

1.使用属性的set()方法示例:

2. IOC容器 - 图4

<property>是根据set方法来确定属性名的,如果将setName()改成setPname(),Spring默认得到的属性名就是pname,而不是name

2. IOC容器 - 图5

简化set()方法注入:P名称空间注入

什么是名称空间?名称空间是用来防止标签重复的。

<!--两个name区分不清,可以加个名称空间,b:name,a:name,就可以区分开了-->
<book>
    <name>西游记</name>
    <author>
        <name>吴承恩</name>  
    </author>
</book>

2. IOC容器 - 图6

2.使用有参构造函数示例:在使用有参构造注入属性的时候,Spring就不是默认使用无参构造函数创建对象了,而是使用有参构造函数创建对象

2. IOC容器 - 图7

<constructor-arg name="name"  index="0" value="花花"></constructor-arg>

value属性:属性值
name属性:类里面的属性名称
index属性:有参构造函数中参数的位置,0代表第一个参数
<!--可以只用value,那么就是根据顺序赋值,name=花花,age=11-->
<bean id="dog" class="com.ysy.learn.basic_02.Dog">
    <constructor-arg value="花花"></constructor-arg>
    <constructor-arg value="11"></constructor-arg>
</bean>
<!--可以用index和value,那么就是根据有参构造函数中参数的位置赋值,name=花花,age=11-->
<bean id="dog" class="com.ysy.learn.basic_02.Dog">
    <constructor-arg index="0" value="花花"></constructor-arg>
    <constructor-arg index="1" value="11"></constructor-arg>
</bean>

<!--但是如果遇到构造器重载
    public User(String name,String sex,Integer age){}
    public User(String name,Integer age,String sex){}
-->
<bean id="user" class="com.ysy.learn.basic_02.User">
    <constructor-arg index="0" value="花花"></constructor-arg>
    <constructor-arg index="1" value="11"></constructor-arg>
    <constructor-arg index="2" value="123"></constructor-arg>
</bean>
<!--这种情况就会随机选择一个构造器,可能赋值错误,此时可以使用type属性,指定参数的类型-->
<bean id="user" class="com.ysy.learn.basic_02.User">
    <constructor-arg index="0" value="花花"></constructor-arg>
    <constructor-arg index="1" value="11" type="Integer"></constructor-arg>
    <constructor-arg index="2" value="123"></constructor-arg>
</bean>

<!--name和index也可以同时存在-->
<bean id="dog" class="com.ysy.learn.basic_02.Dog">
    <constructor-arg name="name" index="0" value="花花"></constructor-arg>
    <constructor-arg name="sex" index="1" value="男"></constructor-arg>
</bean>
<!--但是如果构造函数的顺序是name,sex,但是配置中sex的index写的0,会报错,因为sex的index=1-->
<bean id="dog" class="com.ysy.learn.basic_02.Dog">
    <constructor-arg name="sex" index="0" value="花花"></constructor-arg>
    <constructor-arg name="name" index="1" value="男"></constructor-arg>
</bean>

以上为最基本的基于XML配置文件方式注入属性,此外还有其他为各种类型的属性赋值。

1.往属性中设置空值

如果不给属性赋值,属性默认就是null。所以<null>标签的使用场景是如果属性原本有值,但是在操作中需要给这个属性赋值为null,才会用到这种写法。

2. IOC容器 - 图8

2.属性值中包含特殊符号

2. IOC容器 - 图9

3.注入属性—外部Bean

2. IOC容器 - 图10

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{}
<bean id="person" class="com.ysy.Person">
    <property name="car" ref="car"></property>
</bean>

<bean id="car" class="com.ysy.Car"></bean>

4.注入属性—内部Bean

2. IOC容器 - 图11

内部Bean写id其实没用,只能内部使用,不能被全局使用。意思就是内部bean不能通过context.getBean("car")获取到,只能用person.getCar()

2. IOC容器 - 图12

5.注入属性—级联赋值

什么是级联属性?属性的属性就是级联属性。内部Bean的配置方式也是一种级联赋值,此外,使用表达式方式的级联赋值需要在Emp类中生成属性dept的get()方法,因为需要先得到dept对象实例,才能通过对象实例点出对象的name属性

2. IOC容器 - 图13

6.注入属性—Properties类型

2. IOC容器 - 图14

7.注入属性—集合类型

数组类型属性注入的时候,可以用标签<array>,也可以用标签<list>

2. IOC容器 - 图15

如何注入对象集合?

2. IOC容器 - 图16

如何给多个类重用同一个集合(抽取出作为公共部分,供多个对象使用)?

2. IOC容器 - 图17

1.3 通过继承实现Bean配置信息的重用

2. IOC容器 - 图18

1.4 通过abstract属性创建一个模板Bean

2. IOC容器 - 图19

1.5 Bean之间的依赖

Bean的创建顺序是按照在配置文件中的顺序来创建的,可以用depends-on来标识类之间的依赖关系,改变类的创建顺序。只是改变类的创建顺序,并不是说没有book,person就不能用。若没有添加depends-on标识,类的创建顺序是Car、Person、Book,添加依赖之后,类的创建顺序是Car、Book、Person。另外在depends-on中写的顺序也会影响类的创建顺序。

2. IOC容器 - 图20

1.6 自动装配(自动赋值)

自动装配就是自动为属性赋值,这个概念是基于自定义类型,基本类型不存在自动装配。如何实现自动装配呢?在注册Bean时,使用autowire属性就可以为当前Bean配置自动装配。

<bean id="person" class="com.ysy.learn.bean.Person" autowire="byName"></bean>

autowire属性有以下几个值:

default/no:不进行自动装配

byName:以属性名作为id到容器中找到这个组件,为它赋值,如果找不到就装配null

2. IOC容器 - 图21

byType:以属性类型作为查找依据去容器中找到这个组件,如果容器中有多个就报错,没有就装配null

2. IOC容器 - 图22

constructor:按照构造器进行赋值;1. 先找该类型的有参构造器,没有的话,直接装配null;2. 存在有参构造器就按照有参构造器的参数类型进行装配,如果没有该参数类型的组件,就直接装配null;3. 如果该类型的组件存在多个,就用参数名作为id继续匹配,找到就装配,找不到就装配null。这种用构造器装配的方式不会报错。

2. IOC容器 - 图23

如果属性是一个Lsit,容器会把容器中所有的Book组件封装成list赋值给这个属性。

2. IOC容器 - 图24

1.7 外部属性文件

我们一般习惯于将数据库连接信息抽取到配置文件中,然后直接获取配置文件中的值,便于修改和维护。假设我们有一个jdbc.properties文件,里面包含数据库连接信息

jdbc.username=root
jdbc.password=123456
jdbc.jdbcUrl=jdbc:mysql://localhost:3306/test
jdbc.driverClass=com.mysql.jdbc.Driver

那么Spring怎么获取外部的属性文件呢?

<?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">

    <!--数据库连接池作为单实例是最好的,一个项目就一个连接池,
        连接池里管理很多连接,连接直接从连接池中获取
        可以让Spring帮我们创建连接池对象,管理连接池
        若不使用外部的属性文件,我们可以这样配置,然后获取bean对象
     -->
    <bean id="testDB" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="user" value="root"></property>
        <property name="password" value="123456"></property>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/test"></property>
        <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
    </bean>

    <!--将数据库的连接信息抽取到jdbc.properties文件中,然后引用外部属性文件(需要使用context名称空间)-->
    <!--加载外部配置文件 classpath:固定写法,表示引用类路径下的一个资源-->
    <context:property-placeholder location="classpath:jdbc.properties"></context:property-placeholder>
    <bean id="testDB1" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <!--${key}:动态取出配置文件中某个key对应的值
            如果在配置文件中直接配置username,获取不到数据库的连接,因为username是Spring的key中的一个关键字
            所以我们一般加一个前缀:jdbc.username来加以区分
        -->
        <property name="user" value="${jdbc.username}"></property>
        <property name="password" value="${jdbc.password}"></property>
        <property name="jdbcUrl" value="${jdbc.jdbcUrl}"></property>
        <property name="driverClass" value="${jdbc.driverClass}"></property>
    </bean>
</beans>

上面提到如果在配置文件中直接配置username,获取不到数据库的连接,因为username是Spring的key中的一个关键字,那么username在Spring中到底代表什么呢?

personname属性赋值${username},打印发现,获取到的username是计算机用户名

2. IOC容器 - 图25

补充知识:源码文件夹和普通文件夹

源码文件夹中的内容在编译之后会被放在编译文件的类路径下,普通文件夹中的内容不会放在类路径下,所以放在源码文件夹中的内容可以 直接使用classpath:来取,但是放在普通文件夹中的内容使用这种方式是取不到的

2. IOC容器 - 图26

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;
    }
}
<!--用静态工厂的方式创建对象(不需要new AirPlaneStaticFactory)
    class:静态工厂全类名
    factory-method:指定工厂方法
    constructor-arg:为工厂方法传参数
-->
<bean id="airPlane" class="com.ysy.learn.factory.AirPlaneStaticFactory" factory-method="getAirPlane">
    <constructor-arg value="李四"></constructor-arg>
</bean>

最终控制台输出的结果如下图。虽然配置文件中的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;
    }
}
<!--实例工厂需要对工厂本身创建对象-->
<bean id="airPlaneInstanceFactory" class="com.ysy.learn.factory.AirPlaneInstanceFactory"></bean>

<!--1. 先创建出实例工厂对象本身
    2. 配置我们要创建的AirPlane要使用哪一个工厂创建
         factory-bean:指定使用哪一个工厂实例
         factory-method:指定使用哪一个工厂方法
    3. constructor-arg为工厂方法传参
-->
<bean id="airPlane1" class="com.ysy.learn.bean.AirPlane" factory-bean="airPlaneInstanceFactory"
      factory-method="getAirPlane">
    <constructor-arg value="李四"></constructor-arg>
</bean>

最终控制台输出的结果如下图。先去创建了AirPlaneInstanceFactory实例,之后再创建AirPlane实例。虽然xml文件中配置了AirPlane<bean>标签,但是AirPlane不是容器通过反射的方式创建出来的,而是通过实例工厂的工厂方法创建出来的。

2. IOC容器 - 图27

**FactoryBean**:Spring规定的一个接口,只要是这个接口的实现类,Spring都认为是一个工厂

public class MyAirPlaneFactoryBean implements FactoryBean<AirPlane> {

    /**
     * 工厂方法,返回创建的对象
     * @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;
    }
}
<!--FactoryBean是Spring规定的一个接口,只要是这个接口的实现类,Spring都认为是一个工厂
    1. FactoryBean创建的实例,不管是单实例还是多实例,都是在获取实例的时候创建的,IOC容器启动的时候不会创建
-->
<bean id="airPlane2" class="com.ysy.learn.factory.MyAirPlaneFactoryBean"></bean>

最终控制台输出结果如下图。我们可以发现,Spring先创建了MyAirPlaneFactoryBean的实例,然后再去创建了AirPlane的实例。那么MyAirPlaneFactoryBean的实例和AirPlane的实例分别是在什么时候创建的呢?

2. IOC容器 - 图28

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对象。

2. IOC容器 - 图29

isSingleton()方法return false:代表创建多实例对象,然后我们只创建容器,不获取对象。输出结果如下。也是只打印了调用MyAirPlaneFactoryBean的构造器。所以我们可以得出结论,我们通过**FactoryBean**创建的对象,不管是单实例还是多实例,都是在获取实例的时候创建的,IOC容器启动的时候不会创建。

2. IOC容器 - 图30

3. Bean的作用域

作用域限定了Spring Bean的作用范围,可以通过在配置文件中配置scope属性配置Bean的作用域。

2. IOC容器 - 图31

目前Spring支持五种作用域:

1.singleton

singleton是默认的作用域,当定义Bean时,如果没有指定scope配置项,Bean的作用域被默认为singleton。singleton属于单例模式,在整个系统上下文环境中,仅有一个Bean实例。也就是说,在整个系统上下文环境中,你通过Spring IOC获取的都是同一个实例。

singleton作用域 示例:

在XML配置文件中未指定Bean的作用域(scope属性),之后在testBean()中两次获取book对象,打印对象地址,两次获取对象的地址是一致的,说明两次获取的是同一个对象实例。

2. IOC容器 - 图32

2.prototype

当一个bean的作用域为Prototype,表示一个bean定义对应多个对象实例。这意味着每次对该bean请求(将其注入到另一个bean中,或者以程序的方式调用容器的getBean()方法)时都会创建一个新的bean实例。如果Bean的作用域配置为Prototype,那么在加载Spring配置文件的时候不会创建对象,而是在调用getBean()方法的时候创建多实例对象。根据经验,对有状态的bean应该使用prototype作用域,而对无状态的bean则应该使用singleton作用域。

prototype作用域示例:

在XML配置文件中指定Bean的作用域为Prototype,之后在testBean()中两次获取book对象,打印对象地址,两次获取对象的地址不一致,说明两次获取的不是同一个对象实例。

2. IOC容器 - 图33

3.request

在web环境下,同一次请求创建一个Bean

4.session

在web环境下,同一次会话创建一个Bean

5.global-session

4. Bean的生命周期

从对象创建到对象销毁的过程就是对象的生命周期。

Bean的生命周期:

  1. 通过构造器创建Bean实例(无参构造):单实例Bean是在容器启动的时候;多实例是在获取对象的时候
  2. 为Bean的属性设置值和对其他Bean引用(调用set()方法)
  3. 初始化之前执行的方法(后置处理器Before)
  4. 调用Bean的初始化方法(需要在Spring配置文件中配置初始化方法)
  5. 初始化之后执行的方法(后置处理器After)
  6. 使用Bean对象
  7. 容器关闭,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 id="myBeanPostProcessor" class="com.ysy.learn.common.post.MyBeanPostProcessor"> </bean>

Bean的生命周期示例:

单实例对象定义后置处理器之前

2. IOC容器 - 图34

单实例对象定义后置处理器之后

2. IOC容器 - 图35

如果配置了后置处理器,就算Bean没有定义初始化方法,还是会执行后置处理器的方法

2. IOC容器 - 图36

多实例对象定义后置处理器之后

多实例对象,Spring是不负责销毁的,就是说如果Bean的作用域配置的是prototype,那么容器关闭时,不会调用销毁方法。

2. IOC容器 - 图37

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 组件扫描配置

上面提到开启组件扫描,那么如何开启组件扫描,需要注意些什么呢?

<!--自动扫描组件
    base-package:指定扫描的基础包,把基础包及下面所有包中加了注解的类,自动扫描进IOC容器
                  所有加了注解的组件,id默认是类名首字母小写
    1. 如果要扫描多个包,多个包之间用逗号隔开
    2. 或者扫描包的上层目录
-->
<context:component-scan base-package="com.ysy" use-default-filters="false">
    <!--context:exclude-filter:扫描时排除不要的组件
            annotation:按照注解进行排除,标注了指定注解的组件不要;expression=""注解的全类名
            assignable:指定排除某个具体的类,按照类排除;expression=""类的全类名
            aspectj:aspectj表达式
            custom:自定义一个类,继承TypeFilter,自己写代码决定排除规则
            regex:正则表达式
    -->
    <!-- <context:exclude-filter type="assignable" expression="com.ysy.learn.bean.Car"/>-->

    <!--context:exclude-filter:指定扫描包时要包含的类
            指定扫描时要包含的类的时候,需要把use-default-filters属性置为false,否则使用默认的过滤规则,就是全扫描
    -->
    <context:include-filter type="assignable" expression="com.ysy.learn.bean.Car"/>
</context:component-scan>

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.....");
    }
}

@Autowired的原理:

  1. 先按照类型去容器中找对应的组件
    1. 找到,找到一个,就赋值
    2. 没找到,报错
    3. 找到多个,假设BookService1和BookService2都继承了BookService
      1. 按照变量名作为id继续匹配BookService1(bookService1)、BookService2(bookService2)
        1. 匹配上,直接装配
        2. 没有匹配上,抛异常(按照变量名作为id匹配)
          @Qualifier()指定一个新id
        3. 找到,装配
        4. 找不到,报错
          1. 可以使用required设置是否必须装配上,required=false,能装配上就装,装不上就是null,不报错
  1. 方法上有@Autowired

    1. 这个方法也会在Bean创建的时候自动运行
    2. 这个方法的每一个参数都会注入值
    3. 也可以在参数上加@Qualifier


    @Autowired@Qualifier是一起使用的

@Autowired@Resource@Inject的区别

@AutowiredSpring自己的,功能更强大,但是离开了Spring就用不了了

@ResourceJavaEE的,Java的标准,扩展性更强,如果切换成另外的框架,@Autowired就用不了了

5.4 完全注解开发

完全注解开发就是我们不需要Spring的XML文件,而是利用一个类来实现Spring的XML文件的功能。

四、SPEL表达式

五、Spring的单元测试

六、泛型依赖注入