# Spring IoC

Spring

# 1. IoC 是什么?

IoC 即 Inversion of Control,中文意思是控制反转。

举一个简单的例子,比如我们想要吃一个面包。在有面包店和无面包店两个情况我们会选择哪一种方式来获取面包呢?我们当然可以选择自己买面粉然后自己制作自己喜欢的口味的面包,但是流程就会比较复杂。其实,我们还可告诉面包店家,告知我们要的口味,由面包店来制作。这样,我们不但可以省去制作面包的过程,还可以吃到我们想要的口味。这就是控制反转。

当某个 Java 对象(如想吃面包的我)调用另外一个 Java 对象(被调用者,即被依赖对象,如面包)时,传统模式是用“new 被调用者”的代码方式来创建对象。当 Spring 框架出现后,对象的实现不再由调用者创建,而是由 Spring 容器(如面包店)来创建。Spring 容器会负责控制程序之间的关系(我和面包的关系),而不是由调用者(我)的代码程序直接控制。这样,控制权由调用者转移到 Spring 容器,控制权实现了反转,这就是 Spring 的控制反转。

# 2. IoC 底层原理

img

两种实现方式:

  • xml 配置文件
  • 注解

这里以 XML 配置文件来解释:xml 配置 + 工厂模式 + 反射

首先我们要将对象交给 IoC 容器进行统一管理,这些统一管理的对象我们称为 bean 对象。在 ApplicationContext.xml 文件中,我们用 <bean>标签来管理这些对象。然后我们定义一个工厂类,工厂类首先解析 XML 文件,<bean>标签有两个非常重要的属性,一个是 id,一个是 class,工厂类根据 id 找到这个对象,然后根据它的 class 来确定这个对象是什么类型的,最后利用反射,创建出这个对象。

# 3. IoC 操作 Bean 管理

# 3.1 概念

# 3.2 两种方式

# 3.2.1 基于 xml 配置

# 3.2.1.1 创建对象 —— 无参构造
  • bean 标签:每一个 bean 标签代表一个对象
  • 属性 id:每一个对象的唯一标识符
  • 属性 class:对象的类型
  • 属性 scope:对象的作用范围
  • init-method:指定类中的初始化方法名称
  • destroy-method:指定类中销毁方法名称

创建对象的时候默认调用无参构造创建。

<bean id="stu" class="com.hedon.Stu"></bean>
# 3.2.1.2 创建对象 —— 工厂静态方法
  • 写一个工厂类,里面有一个静态方法可以生成 UserDao 类型的对象

    public class StaticFatory {
    
        public static UserDao createUserDao(){
            return new UserDaoImpl();
        }
    
    }
    
  • 配置

    • Id :唯一标志
    • class:工厂类全限定类名
    • factory-method:指定生产对象的静态方法
        <!--静态工厂方法创建对象-->
    <bean id="userDao" class="com.factory.StaticFatory" factory-method="createUserDao"></bean>
    
# 3.2.1.3 创建对象 —— 实例工厂的方法
  • 写一个工厂类,里面的有一个方法可以生成对象,但是这个方法不是静态的

    public class InstanceFactory {
    
        public UserDao createUserDao(){
            return new UserDaoImpl();
        }
        
    }
    
  • 配置

    因为实例工厂类中的方法不是静态是,所以需要先创建对象,然后才可以来调用方法

        <!--需要先注入实例工厂-->
        <bean id="instanceFactory" class="com.factory.InstanceFactory"></bean>
    
        <!--通过实例工厂来调用它里面的方法来创建对象-->
        <bean id="userDao2" factory-bean="instanceFactory" factory-method="createUserDao"></bean>
    
# 3.2.1.4 注入基本属性
  • setter 注入
class Stu{
  private String name;
  
  //属性的 set 方法
  public void setName(String name){
    this.name = name;
  }
}
<bean id="stu" class="com.hedon.Stu">
	<property name="name" value="课程名"></property>
</bean>
  • 有参构造注入
class Stu{
  private String name;
  
  //有参构造
  public Stu(String name){
    this.name = name;
  }
}
<bean id="stu" class="com.hedon.Stu">
	<constructor-arg name="name" value="课程名"></constructor-arg>
</bean>
  • p 名称空间注入

△ 重点:需要加入 p 名称空间

<?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:p="http://www.springframework.org/schema/p"

使用 set 注入

<bean id="stu" class="com.hedon.Stu" p:name="课程名"></bean>
# 3.2.1.5 注入空值
<property name="address">
   <!--null值-->
   <null></null>
</property>
# 3.2.1.6 注入特殊字符 —— 把带特殊符号的内容放到 CDATA 中
<property name="name">
	<value><![CDATA[<<南京>>]]></value>
</property>
# 3.2.1.7 注入引用类型属性 —— ref
public class UserService {

    //创建 UserDao 类型属性,生成 set 方法
    private UserDao userDao;

    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }


    public void add(){

        System.out.println("service add ....");
        userDao.update();
    }
    
}
    <bean id="userService" class="com.service.UserService">
        <!--注入属性
            name: 类中的属性名称
            value:属性值(基本类型)
            ref:对象值
        -->
        <property name="userDao" ref="userDao"></property>
    </bean>

    <bean id="userDao" class="com.dao.impl.UserDaoImpl"></bean>
# 3.2.1.8 注入属性的属性——内部bean
    <bean id="emp" class="com.bean.Emp">
        <property name="ename" value="小明"></property>
        <property name="gender" value=""></property>
      
      	<!-- 一个引用类型的属性,它也有自己的属性 -->
        <property name="dept">
            <bean id="dept" class="com.bean.Dept">
                <property name="name" value="安保部"></property>
            </bean>
        </property>
    </bean>
# 3.2.1.9 注入属性的属性——级联赋值2
    <bean id="emp" class="com.bean.Emp">
        <property name="ename" value="小红"></property>
        <property name="gender" value=""></property>
        <!-- 级联赋值 -->
        <property name="dept" ref="dept"></property>
    </bean>

    <bean id="dept" class="com.bean.Dept">
        <property name="name" value="后勤部"></property>
    </bean>
# 3.2.1.10 注入属性——级联赋值2

这个时候,这里的 “后勤部” 会被 dept.name 的 “保安部” 所覆盖。

这里为了能够使用 dept.name,需要在 Emp 类中写上属性 dept 的 getter 方法。

private Dept dept;

//getter
public Dept getDept(){
  return dept;
}

//setter
public void setDept(Dept dept){
  this.dept = dept;
}
    <bean id="emp" class="com.bean.Emp">
        <property name="ename" value="小红"></property>
        <property name="gender" value=""></property>
        <!-- 级联赋值 -->
        <property name="dept" ref="dept"></property>
      	<property name="dept.name" value="保安部"></property>
    </bean>

    <bean id="dept" class="com.bean.Dept">
        <property name="name" value="后勤部"></property>
    </bean>
# 3.2.1.11 注入集合类属性
  • 数组 array
<property name="sources">
  <array>
    <value>Java</value>
    <value>C++</value>
  </array>
</property>
  • 列表 list
<property name="list">
  <list>
    <value>小明</value>
    <value>小红</value>
  </list>
</property>

如果 list 中放的是对象:

<bean ........>
  
	<!--List中放对象-->
  <property name="courseList">
      <list>
          <ref bean="course1"></ref>
          <ref bean="course2"></ref>
      </list>
  </property>
  
</bean>

<bean id="course1" class="com.atguigu.spring5.Course">
  <property name="cname" value="Spring"></property>
</bean>


<bean id="course2" class="com.atguigu.spring5.Course">
  <property name="cname" value="Mybatis"></property>
</bean>
  • 映射 map
<property name="maps">
  <map>
    <entry key="JAVA" value="java"></entry>
    <entry key="CPlusPlus" value="C++"></entry>
  </map>
</property>
  • 集合 set
<property name="sets">
    <set>
        <value>MySQL</value>
        <value>Redis</value>
    </set>
</property>
# 3.2.1.12 提取集合注入 —— util 命名空间

需要加入 util 命名空间:

<?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:p="http://www.springframework.org/schema/p"
       xmlns:util="http://www.springframework.org/schema/util"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd      
       http://www.springframework.org/schema/util
       http://www.springframework.org/schema/util/spring-util.xsd">
<util:list id="bookList">
    <value>易筋经</value>
    <value>九阳神功</value>
    <value>九阴真经</value>
</util:list>

<bean id="book2" class="com.bean.Book">
    <property name="bookList" ref="bookList"></property>
</bean>
# 3.2.1.13 引入外部属性文件
  • 创建外部属性文件 jdbc.properties

    prop.driverClassName = com.mysql.cj.jdbc.Driver
    prop.url = jdbc:mysql://localhost:3306/eesy
    prop.username = root
    prop.password = root
    
  • XML 配置文件中引入 context 命名空间

    <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">
    
  • 在 XML 配置文件中引入 jdbc.properties

    <context:property-placeholder location="classpath:jdbc.properties"/>
    
  • 配置 Druid 连接池(需要先导入 Druil 相关 jar 包或依赖)

    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
      <property name="driverClassName" value="${prop.driverClass}"></property>
    	<property name="url" value="${prop.url}"></property>
    	<property name="username" value="${prop.username}"></property> 
      <property name="password" value="${prop.password}"></property>
    </bean>
    

# 3.2.2 基于注解

# 3.2.2.1 创建对象
  • 5个相同功能的注解:表示将加上该注解的类扫描并注入到 IoC 容器中。

    • @Component
    • @Controller
    • @Service
    • @Repository
    • @Mapper
  • 1)引入 aop 的 jar 包

  • 2-1)开启组件扫描 —— XML 配置版本

    • base-package:指定扫描哪个包
    <context:component-scan base-package="com.spring5"></context:component-scan>
    
    • context:exclude-filter:在开启默认过滤器(即扫描所有的注解)的基础上,指定不扫描的注解。
    <context:component-scan base-package="com.spring5">
        <!--表示不扫描该包下的 @Controller 注解 -->
        <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
    </context:component-scan>
    
    • context:include-filter:关闭默认过滤器(即什么都不扫描),指定扫描的注解。
      																													<!--关闭默认过滤器-->
    <context:component-scan base-package="com.spring5" use-default-filters="false">
        <!--扫描 @Controller-->
        <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
    </context:component-scan>
    
  • 2-2)开启注解扫描 —— 配置类版本

    写一个配置类,加上两个注解:

    • @Configuration:指明这是一个配置类
    • @ComponentScan(basePackages = {指定要扫描的包})
    @Configuration
    @ComponentScan(basePackages = {"com.spring5.beans","com.spring5.services"})
    public class SpringConfig {
        
    }
    
  • 3)在类上加注解,这里用 @Component 来演示

    • value:即配置文件中 bean 标签中的 id 属性,对象的唯一标志。默认为类名首字母小写。
    @Component(value = "stu")
    public class Stu {
    
        private String name;
    
        public String getName() {
            return name;
        }
        
        public void setName(String name) {
            this.name = name;
        }
    
        @Override
        public String toString() {
            return "Stu{" +
                    "name='" + name + '\'' +
                    '}';
        }
    }
    
  • 4-1)测试 —— XML 版本 ClassPathXmlApplicationContext

        @Test
        public void test(){
            ApplicationContext context = new ClassPathXmlApplicationContext("bean1.xml");
            Stu stu = context.getBean("stu", Stu.class);
            System.out.println(stu);
        }
    
  • 4-2)测试 —— 注解版本 AnnotationConfigApplicationContext

        @Test
        public void test(){
          ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
            Stu stu = context.getBean("stu", Stu.class);
            System.out.println(stu);
        }
    
  • 5)测试结果

    Stu{name='null'}
    
# 3.2.2.2 注入属性 —— @Value 注入普通类型属性

在上面 Stu 实体类中的属性上加上 @Value 进行赋值:

@Component(value = "stu")
public class Stu {

  //① 可以加在属性声明上面
    @Value("注入属性")
    private String name;


    public String getName() {
        return name;
    }

  //② 也可以加在属性的 set 方法上
    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Stu{" +
                "name='" + name + '\'' +
                '}';
    }
}

再次测试:

Stu{name='注入属性'}

Process finished with exit code 0
# 3.2.2.3 注入属性 —— @Autowired 根据属性类型进行自动装配
@Service
public class Book {


    @Value(value = "11.1")
    private Double price;

    //这里会根据 Stu 类型自动注入 stu 对象
    @Autowired
    private Stu stu;


    @Override
    public String toString() {
        return "Book{" +
                "price=" + price +
                ", stu=" + stu +
                '}';
    }
}

再测试一下:

@Test
public void test1(){
  ApplicationContext context = new ClassPathXmlApplicationContext("bean1.xml");
  Book book = context.getBean("book", Book.class);
  System.out.println(book);
}

测试结果:

Book{price=11.1, stu=Stu{name='Stu注入自己的普通属性'}}

Process finished with exit code 0
# 3.2.2.4 注入属性 —— @Qualifier 根据属性名称进行注入
  • 这个注解需要跟 @Autowired 进行搭配使用。
  • 如果一个接口有多个实现类对象的话,那就可以指定名称来进行指定注入。
@Controller
public class Book {

    @Value(value = "11.1")
    private Double price;

    //在 @Autowired 下面加上 @Qualifier 就可以根据名称进行注入
    @Autowired
    @Qualifier(value = "stu")
    private Stu stu;

    @Override
    public String toString() {
        return "Book{" +
                "price=" + price +
                ", stu=" + stu +
                '}';
    }
}
# 3.2.2.5 注入属性 —— @Resource 可以根据类型注入,可以根据名称注入
//@Resource 根据类型注入
@Resource(name="stu")  //根据名称进行注入
private Stu stu;

# 3.3 两种 bean

# 3.3.1 普通 bean

在配置文件中定义 bean 的 class 类型就是返回类型。

# 3.3.2 工厂 bean(FactoryBean)

在配种文件中定义 bean 的 class 类型可以跟返回类型不一样。

  • 创建类 MyBean,把这个类作为工厂 bean,让它实现接口 FactoryBean

    //泛型中写这个工厂 bean 要产生的 bean 对象的类型
    public class MyBean implements FactoryBean<Course> {
    
    		//返回的对象
        @Override
        public Course getObject() throws Exception {
            Course course = new Course();
            course.setCname("课程名");
            return course;
        }
    
        @Override
        public Class<?> getObjectType() {
            return null;
        }
    
        @Override
        public boolean isSingleton() {
            return false;
        }
    }
    
  • 注入到 IoC 容器中

    <bean id="myBean" class="com.atguigu.spring5.MyBean"></bean>
    
  • 测试

        @Test
        public void testFactoryBean(){
            ApplicationContext context = new ClassPathXmlApplicationContext("bean2.xml");
            Course myBean = context.getBean("myBean", Course.class);
            System.out.println(myBean);
        }
    

    这样一来,我们配置的 bean 类型是 MyBean,但是生成的对象类型是 Course。

# 3.4 bean 作用域

# 3.4.1 XML 配置单例与多例 —— 默认单例
  • <bean>标签中有scope 属性

    • singleton:单例

      设置 scope 值是 singleton 时候,加载 spring 配置文件时候就会创建单实例对象。

    • prototype:多例

      设置 scope 值是 prototype 时候,不是在加载 spring 配置文件时候创建 对象,在调用 getBean 方法时候创建多实例对象。

    • request:WEB 项目中,Spring 创建一个 Bean 的对象,将对象存入到 request 域中。

    • session:WEB 项目中,Spring 创建一个 Bean 的对象,将对象存入到 session 域中。

    • global session:WEB 项目中,应用在 Portlet 环境.如果 没有Portlet 环境那么 globalSession 相当于 session。

# 3.4.2 工厂 bean 配置单例与多例 —— 默认多例

之前讲工厂 Bean 的时候实现接口 FactoryBean 中有一个方法叫做 isSingleton(),所以很显然可以通过这个方法来配置单例与多例。

    @Override
    public boolean isSingleton() {
        return false;
    }

# 3.5 bean 生命周期

st=>start: 开始
op1=>operation: 通过构造器创建 bean 实例(默认无参数构造)
op2=>operation: 为 bean 的属性设置值和对其他 bean 引用(调用 set 方法)
op3=>operation: 把 bean 实例传递 bean 后置处理器的方法 postProcessBeforeInitialization
op4=>operation: 调用 bean 的初始化的方法(需要进行配置初始化的方法)
op5=>operation: 把 bean 实例传递 bean 后置处理器的方法 postProcessAfterInitialization
op6=>operation: bean 可以使用了(对象获取到了)
op7=>operation: 当容器关闭时候,调用 bean 的销毁的方法(需要进行配置销毁的方法)
e=>end: 结束
st->op1->op2->op3->op4->op5->op6->op7->e
  • 写一个实体类 BeanLife

    public class BeanLife {
    
        private String name;
    
        public void setName(String name) {
            System.out.println("第2步:为 bean 的属性设置值和对其他 bean 引用(调用 set 方法)");
            this.name = name;
        }
    
        public BeanLife() {
            System.out.println("第1步:通过构造器创建 bean 实例(默认无参数构造)");
        }
    
        public void initMethod(){
            System.out.println("第4步:调用 bean 的初始化的方法(需要进行配置初始化的方法)");
        }
    
    
        public void destroyMethod(){
            System.out.println("第7步:当容器关闭时候,调用 bean 的销毁的方法(需要进行配置销毁的方法)");
        }
    }
    
  • 配置 BeanLife 及其初始化和销毁方法

        <bean id="beanLife" class="com.atguigu.spring5.BeanLife" 
              init-method="initMethod" destroy-method="destroyMethod">
            <property name="name" value="生命周期"></property>
        </bean>
    
  • 加入 Bean 后置处理器,写一个实体类 MyBeanPost 实现接口BeanPostProcessor

    public class MyBeanPost implements BeanPostProcessor {
    
        @Override
        public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
            System.out.println("第3步:把 bean 实例传递 bean 后置处理器的方法 postProcessBeforeInitialization");
            return bean;
        }
    
        @Override
        public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
            System.out.println("第5步:把 bean 实例传递 bean 后置处理器的方法 postProcessAfterInitialization");
            return bean;
        }
    }
    
  • 配置 Bean 后置处理器

    <bean id="myBeanPost" class="com.atguigu.spring5.MyBeanPost"></bean>
    
  • 测试代码

        @Test
        public void testBeanLift(){
            ApplicationContext context = new ClassPathXmlApplicationContext("bean4.xml");
            BeanLife beanLift = context.getBean("beanLife", BeanLife.class);
            System.out.println("第6步:开始使用 bean =》"+beanLift);
            //需要手动销毁
            ((ClassPathXmlApplicationContext)context).close();
        }
    
  • 测试结果

    第1步:通过构造器创建 bean 实例(默认无参数构造)
    第2步:为 bean 的属性设置值和对其他 bean 引用(调用 set 方法)
    第3步:把 bean 实例传递 bean 后置处理器的方法 postProcessBeforeInitialization
    第4步:调用 bean 的初始化的方法(需要进行配置初始化的方法)
    第5步:把 bean 实例传递 bean 后置处理器的方法 postProcessAfterInitialization
    第6步:开始使用 bean =》com.atguigu.spring5.BeanLife@8e24743
    第7步:当容器关闭时候,调用 bean 的销毁的方法(需要进行配置销毁的方法)
    
    Process finished with exit code 0
    

# 3.6 XML 自动装配

自动装配指的是 Spring 会根据指定装配规则(属性名称或者属性类型)自动将匹配的属性值进行注入。

# 3.6.1 根据属性名称自动注入

image-20200907235305024

# 3.6.2 根据属性类型自动注入

image-20200907235613273

这里需要注意如果存在两个 bean 是 Dept 类型的,那么就会报错:

image-20200907235730020
上次更新: 8/29/2022, 12:17:25 AM