# SpringIOC 概述

🚀本篇章代码 Demo 在日常开发中,创建对象的操作随处可见以至于对其十分熟悉又感觉很繁琐,每次需要对象都需要亲手 new 一个对象出来,甚至某些情况下由于不好的编程习惯可能还会造成对象无法被回收,就会非常糟糕,我们一直提倡的松耦合,少入侵原则,这种情况下变得一无是。前辈们开始谋求改变这种编程陋习,考虑如何使编码更加解耦合,由此而来的解决方法是面向接口编程,未使用接口和使用接口后的代码如下:

# 1️⃣ 使用接口前的做法 - before

SystemDaoImpl.java
package top.rem.rain.demo1.before;
import java.util.ArrayList;
import java.util.List;
/**
 * @Author: LightRain
 * @Description: 系统类
 * @DateTime: 2023-12-30 22:27
 * @Version:1.0
 **/
public class SystemDaoImpl {
    /**
     * 打印系统信息
     */
    public void systemInfo() {
        List<String> list = new ArrayList<String>();
        list.add(String.format("Java运行版本:%s",System.getProperty("java.version")));
        list.add(String.format("Java供应商:%s",System.getProperty("java.vendor")));
        list.add(String.format("Java安装目录:%s",System.getProperty("java.home")));
        list.add(String.format("操作系统名称:%s",System.getProperty("os.name")));
        list.add(String.format("操作系统架构:%s",System.getProperty("os.arch")));
        list.add(String.format("操作系统版本:%s",System.getProperty("os.version")));
        list.add(String.format("用户名称:%s",System.getProperty("user.name")));
        list.add(String.format("用户主目录:%s",System.getProperty("user.home")));
        list.forEach(System.out::println);
    }
}
SystemServiceImpl.java
package top.rem.rain.demo1.before;
/**
 * @Author: LightRain
 * @Description: 系统业务层
 * @DateTime: 2023-12-30 22:28
 * @Version:1.0
 **/
public class SystemServiceImpl {
    /**
     * 实体类对象
     */
    private SystemDaoImpl system;
    public void getSystemInfo(){
        // 未使用接口的做法
        system = new SystemDaoImpl();
        system.systemInfo();
    }
}

# 2️⃣ 使用接口后的做法 - after

SystemDao.java
package top.rem.rain.demo1.after;
/**
 * @author LightRain
 */
public interface SystemDao {
    /**
     * 打印系统信息
     */
    void systemInfo();
}
SystemDaoImpl.java
package top.rem.rain.demo1.after;
import java.util.ArrayList;
import java.util.List;
/**
 * @Author: LightRain
 * @Description: 系统实现类
 * @DateTime: 2023-12-30 22:43
 * @Version:1.0
 **/
public class SystemDaoImpl implements SystemDao {
    /**
     * 打印系统信息
     */
    @Override
    public void systemInfo() {
        List<String> list = new ArrayList<String>();
        list.add(String.format("Java运行版本:%s", System.getProperty("java.version")));
        list.add(String.format("Java供应商:%s", System.getProperty("java.vendor")));
        list.add(String.format("Java安装目录:%s", System.getProperty("java.home")));
        list.add(String.format("操作系统名称:%s", System.getProperty("os.name")));
        list.add(String.format("操作系统架构:%s", System.getProperty("os.arch")));
        list.add(String.format("操作系统版本:%s", System.getProperty("os.version")));
        list.add(String.format("用户名称:%s", System.getProperty("user.name")));
        list.add(String.format("用户主目录:%s", System.getProperty("user.home")));
        list.forEach(System.out::println);
    }
}
SystemServiceImpl.java
package top.rem.rain.demo1.after;
/**
 * @Author: LightRain
 * @Description: 系统业务层
 * @DateTime: 2023-12-30 22:45
 * @Version:1.0
 **/
public class SystemServiceImpl {
    /**
     * 使用接口的做法
     */
    private SystemDao systemDao;
    public void systemInfo() {
        // 使用接口来创建具体实现类对象
        systemDao = new SystemDaoImpl();
        systemDao.systemInfo();
    }
}

SystemServiceImpl 类中由原来直接与 SystemDaoImpl 打交道变为了 SystemDao , 即使 SystemDao 最终实现依然是 SystemDaoImpl ,这样做的好处是所有调用都通过接口 SystemDao 来完成,而接口真正实现类和最终执行者就是 SystemDaoImpl 类,当替换 SystemDaoImpl 类时也只需要修改 SystemDao 指向新实现类即可。

pibcnmD.png

在上述代码中很大程度上降低了代码耦合度,但是代码依旧存在入侵性和一定程度的耦合性,如:在修改 SystemDao 实现类时,扔然需要修改 SystemServiceImpl 内部代码,当依赖的类过多时,查找和修改的过程会非常麻烦,因此我们扔需要寻找一种方式,它可以令开发者无需触及 SystemServiceImpl 内容代码的情况下实现修改 SystemDao 的实现类,以便达到最低的耦合度和最少入侵目的。

# 3️⃣ 通过配置文件降低耦合度

实际上存在一种称为反射的编程技术可以协助解决此问题,反射是一种根据给出完整类名来动态生成对象,这种编程方式可以让对象在生成时决定到底是哪一种对象,因此可以这样来做,在某个配置文件,该文件已写好 SystemDaoImpl 类的完全限定名,通过读取该文件而获取到 SystemDao 的真正实现类的完全限定名,然后通过反射技术在运行时动态生成该类,最终赋值给 SystemDao 接口,这样就可以解决当前问题了,下面使用 properties 文件作为配置文件, className.properties 如下:

className.properties
systemDao.name = top.rem.rain.demo1.reflection.SystemDaoImpl
SystemService.java
package top.rem.rain.demo1.reflection;
import java.lang.reflect.InvocationTargetException;
/**
 * @author LightRain
 */
public interface SystemService {
    /**
     * 反射创建对象
     * @throws ClassNotFoundException
     * @throws InstantiationException
     * @throws IllegalAccessException
     * @throws NoSuchMethodException
     * @throws InvocationTargetException
     */
    void reflectionCreationObject() throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException;
}
SystemServiceImpl.java
package top.rem.rain.demo1.reflection;
import org.apache.logging.log4j.util.PropertiesUtil;
import org.junit.jupiter.api.Test;
import java.lang.reflect.InvocationTargetException;
/**
 * @Author: LightRain
 * @Description: 系统业务层
 * @DateTime: 2023-12-30 22:55
 * @Version:1.0
 **/
public class SystemServiceImpl implements SystemService{
    private SystemDao systemDao;
    /**
     * 反射创建对象
     *
     * @throws ClassNotFoundException
     * @throws InstantiationException
     * @throws IllegalAccessException
     * @throws NoSuchMethodException
     * @throws InvocationTargetException
     */
    @Override
    public void reflectionCreationObject() throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
        // 读取配置文件的工具类
        PropertiesUtil propertiesUtil = new PropertiesUtil("className.properties");
        // 获取完全限定名称
        String className = propertiesUtil.getStringProperty("systemDao.name");
        // 通过反射
        Class<?> c = Class.forName(className);
        // 动态生成实例对象
        systemDao = (SystemDao) c.getDeclaredConstructor().newInstance();
        // 打印系统信息
        systemDao.systemInfo();
    }
    @Test
    public void test() throws ClassNotFoundException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException {
        SystemServiceImpl systemService = new SystemServiceImpl();
        systemService.reflectionCreationObject();
        /*
          执行结果:
            Java 运行版本:17.0.6
            Java 供应商:Oracle Corporation
            Java 安装目录:C:\LightRainData\IDEA\JDK\JDK-17.0.6
            操作系统名称:Windows 11
            操作系统架构:amd64
            操作系统版本:10.0
            用户名称:LightRain
            用户主目录:C:\Users\LightRain
         */
    }
}

上述代码的确如我们所愿生成了 SystemDao 实例,这样做的好处是在替换 SystemDao 实现类的情况下只需要修改配置文件的内容,而无需触及 SystemServiceImpl 内部代码,从而把代码修改的过程转到配置文件中,相当于 SystemServiceImpl 及其内部的 SystemDao 通过配置文件与 SystemDao 的实现类进行关联,这样 SystemServiceImplSystemDao 的实现类间也就实现了解耦合,当然 SystemSeviceImpl 类中存在着 SystemDao 对象是无法避免的,我们只能最大程度去解耦合。

piOyEWR.png

SpringIOC 也是一个 Java 对象,在某些特定时间被创建后,可以进行对其它对象的控制,包括初始化、创建、销毁等。在上述过程中,我们通过配置文件配置了 SystemDaoImpl 实现类的完全限定名称,通过反射机制在运行时为 SystemDao 创建具体实现类,包括 SystemServiceImpl 的创建, SpringIOC 容器都会帮我们完成,而我们唯一要做的就是把需要创建的类和其它依赖的类以配置文件的方式告诉 IOC 容器需要创建和注入哪些类。

Spring 通过这种控制反转 (IOC) 的设计模式来解耦合,这种方式使一个对象依赖其它对象时会通过被动的方式传进来,而不是通过手动创建这些类,我们可以把 IOC 模式看做工厂模式的升华版,只不过这个工厂里要生成的对象都是配置文件 (XML) 中给出定义的,然后利用 Java 反射技术根据 XML 中给出的类名生成相应对象。

从某种程度上来讲 IOC 相当于把在工厂方法里通过硬编码创建对象的代码,改成了由 XML 文件来定义,也就是把工厂和对象生成这两者独立分隔开来,目的就是提高灵活性和可维护性,再就是将对象之间的耦合度降到最低,因此我们要明白所谓的 IOC 就是将对象的创建权交由 Spring 来完成,让类之间的关系达到最低耦合度状态。

# Spring 快速入门

1️⃣ 使用 SpringIOC 功能需要先引入 Spring 核心依赖包,使用 Maven 作为构建工具。

pom.xml
<properties>
    <spring.version>6.1.0</spring.version>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<!--    添加 Spring 依赖    -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-core</artifactId>
    <version>${spring.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-beans</artifactId>
    <version>${spring.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>${spring.version}</version>
</dependency>

2️⃣ 创建 AccountDao 接口

AccountDao.java
package top.rem.rain.demo2.dao;
/**
 * @author LightRain
 */
public interface AccountDao {
    /**
     * 账户信息
     */
    void accountInfo();
}

3️⃣ 创建 Dao 具体实现类

AccountDaoImpl.java
package top.rem.rain.demo2.dao;
/**
 * @Author: LightRain
 * @Description: 实现类
 * @DateTime: 2023-12-31 21:01
 * @Version:1.0
 **/
public class AccountDaoImpl implements AccountDao {
    /**
     * 账户信息
     */
    @Override
    public void accountInfo() {
        System.out.printf("用户名称:%s", System.getProperty("user.name"));
    }
}

4️⃣ 创建 AccountService 接口

AccountService.java
package top.rem.rain.demo2.service;
public interface AccountService {
    /**
     * 获取账户
     */
    void getAccount();
}

5️⃣ 创建 Service 具体实现类

AccountServiceImpl.java
package top.rem.rain.demo2.service;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import top.rem.rain.demo2.dao.AccountDao;
import top.rem.rain.demo2.dao.AccountDaoImpl;
/**
 * @Author: LightRain
 * @Description: 实现类
 * @DateTime: 2023-12-31 21:04
 * @Version:1.0
 **/
public class AccountServiceImpl implements AccountService {
    /**
     * 注入对象
     */
    private AccountDao accountDao;
    /**
     * 获取账户
     */
    @Override
    public void getAccount() {
        accountDao.accountInfo();
    }
    /**
     * 通过 set 方法注入依赖对象
     *
     * @param accountDao AccountDao
     */
    public void setAccountDao(AccountDao accountDao) {
        this.accountDao = accountDao;
    }
}

6️⃣ 在创建完上面的接口和具体实现后,下面将通过 SpringIOC 容器来帮助我们创建并注入这些类,在项目的 resources 目录下创建 applicationcontext.xml 配置文件,具体配置代码如下:

applicationcontext.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">
    <!--  声明 accountDao 对象并交由 Spring 来进行管理与创建  -->
    <bean name="accountDao" class="top.rem.rain.demo2.dao.AccountDaoImpl"/>
    <!--  声明 accountService 对象并交由 Spring 来进行管理与创建  -->
    <bean name="accountService" class="top.rem.rain.demo2.service.AccountServiceImpl">
        <!--    注入 accountDao 对象需要 set 方法    -->
        <property name="accountDao" ref="accountDao"/>
    </bean>
</beans>

XML 文件中,我们可以看到有一个 beans 的顶级标签,同时还引入了核心命名空间, Spring 的功能在使用时都需要声明相应的命名空间,在上述的 xml 中的命名空间是最基本的,使用 IntelliJ IDEA 创建 Springxml 配置文件时默认就会生成。

通过 bean 子标签声明那些需要 IOC 容器帮助我们要创建的类,其中 name 是指明 IOC 创建后该对象的名称 (也可以使用 id 来替代 name ), class 是告诉 IOC 这个类的完全限定名称, IOC 会通过这组信息使用反射技术来帮助我们创建对应的类对象。

accountService 标签的声明中多出了一个 property 标签,这个标签指向了刚才创建的 accountDao 对象,它的作用是把 accountDao 对象传递给 accountService 实现类中的 accountDao 属性,该属性必须拥有 set 方法才可以注入成功,我们把这种往类对象中注入其它对象的操作统称为 依赖注入 ,其中 property 标签的 name 必须与 AccountServiceImpl 实现类中的变量名称相同。

7️⃣ 在 AccountServiceImpl 类中添加一个测试方法,其次使用这些类需要利用 Spring 提供的核心类, ApplicationContext 通过该类去加载已声明好的配置文件,然后就可以获取到我们需要的类了,测试方法如下:

AccountServiceImpl.java
package top.rem.rain.demo2.service;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import top.rem.rain.demo2.dao.AccountDao;
import top.rem.rain.demo2.dao.AccountDaoImpl;
/**
 * @Author: LightRain
 * @Description: 实现类
 * @DateTime: 2023-12-31 21:04
 * @Version:1.0
 **/
public class AccountServiceImpl implements AccountService {
    /**
     * 省略上述重复贴过的代码....
     */
    /**
     * 使用 XML 文件进行配置
     */
    @Test
    public void testAccountXML() {
        // 通过 ApplicationContext 加载配置文件
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationcontext.xml");
        // 多次获取并不会创建多个 accountService 对象,因为 Spring 默认创建是单实例的作用域
        AccountService accountService = (AccountService) applicationContext.getBean("accountService");
        accountService.getAccount();
        /*
            执行结果:用户名称:LightRain
         */
    }
}

# Spring 容器装配 - XML & 注解配置方式

# XML 配置方式

采用 xml 配置文件的方式对 bean 进行声明和管理,每一个 bean 标签都代表着需要被创建的对象并通过 property 标签可以为该类其它依赖对象,通过这种方式 Spring 容器就可以成功知道我们需要创建哪些 bean 实例了。如下:

applicationcontext.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">
    <!--  声明 accountDao 对象并交由 Spring 来进行管理与创建  -->
    <bean name="accountDao" class="top.rem.rain.demo2.dao.AccountDaoImpl"/>
    <!--  声明 accountService 对象并交由 Spring 来进行管理与创建  -->
    <bean name="accountService" class="top.rem.rain.demo2.service.AccountServiceImpl">
        <!--    注入 accountDao 对象需要 set 方法    -->
        <property name="accountDao" ref="accountDao"/>
    </bean>
</beans>

然后通过 ApplicationContextClassPathXmlApplicationContext 去加载 Spring 配置文件,来获取想要的实例并调用相应方法执行。

对于 ClassPathXmlApplicationContext 默认加载 classpath 路径下的文件,只需要指明对应文件的 classpath 路径即可,如果存在多个配置文件,则只需要分别传递即可, ClassPathXmlApplicationContext 是一个可以接收可变参数的构造函数。

// 默认查找 classpath 路径下的文件
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationcontext.xml");
// 多文件,也可传递数组
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationcontext.xml","applicationcontext2.xml",...);
// 默认为项目工作路径 即项目的根目录 
FileSystemXmlApplicationContext applicationContext = new FileSystemXmlApplicationContext("/src/main/resources/applicationcontext.xml");
// 也可以读取 classpath 下的文件
FileSystemXmlApplicationContext applicationContext = new FileSystemXmlApplicationContext("classpath:applicationcontext.xml");

# 注解配置方式

下面是通过注解的方式来达到与上述 xml 配置的效果,代码如下:

BeanConfig.java
package top.rem.rain.demo2.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import top.rem.rain.demo2.dao.AccountDao;
import top.rem.rain.demo2.dao.AccountDaoImpl;
import top.rem.rain.demo2.service.AccountService;
import top.rem.rain.demo2.service.AccountServiceImpl;
/**
 * @Author: LightRain
 * @Description: Bean 注解方式的配置
 * @DateTime: 2024-01-01 00:38
 * @Version:1.0
 **/
@Configuration
public class BeanConfig {
    @Bean
    public AccountDao accountDao(){
        return new AccountDaoImpl();
    }
    @Bean
    public AccountService accountService(){
        AccountServiceImpl accountService = new AccountServiceImpl();
        accountService.setAccountDao(accountDao());
        return accountService;
    }
}

@Configuration 注解表示 BeanConfig 类是配置文件类,相当于代替了 XML 的配置文件,这种基于 Java 注解配置方式是在 Spring 3.0 中引入的,使用注解进行配置时请确保引入了 spring-context 坐标。

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>6.1.0</version>
</dependency>

AccountServiceImpl 类中再添加一个测试方法,这次使用 AnnotationConfigApplicationContext 来加载 Java 配置文件类,执行结果是和 XML 一样的。

package top.rem.rain.demo2.service;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import top.rem.rain.demo2.config.BeanConfig;
import top.rem.rain.demo2.dao.AccountDao;
/**
 * @Author: LightRain
 * @Description: 实现类
 * @DateTime: 2023-12-31 21:04
 * @Version:1.0
 **/
public class AccountServiceImpl implements AccountService {
    /**
     * 省略上述重复贴过的代码....
     */
    /**
     * 使用 Java 注解进行配置
     */
    @Test
    public void testAccountBean() {
        // 通过 ApplicationContext 的 AnnotationConfigApplicationContext 加载配置文件
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(BeanConfig.class);
        // 名称必须 BeanConfig 中工程方法名称一致
        AccountService accountService = (AccountService) applicationContext.getBean("accountService");
        accountService.getAccount();
        /*
            执行结果:用户名称:LightRain
         */
    }
}

我们需要明白的是,在大部分情况下更倾向于使用 XMl 来配置 Bean 相关信息,这样会更加方便对代码进行管理,除了前面通过 xml 中使用 bean 标签为每个类声明实例外, Spring 还为我们提供了基于注解的声明方式。

# Spring 依赖注入

至于依赖注入就是当一个 bean 实例引用到了另一个 bean 实例时, Spring 容器会帮助我们创建依赖 bean 实例并注入到另一个 bean 中,在上述案例中的 AccountService 依赖于 AccountDaoSpring 容器会在创建 AccountService 的实现类和 AccountDao 的实现类后,把 AccountDao 的实现注入 AccountService 实例中,下面分别来介绍注入的方式。

# Setter 注入

Setter 注入:被注入的属性需要拥有 set 方法, Setter 注入支持基本类型和引用类型, Setter 注入是在 Bean 实例创建完成后执行的,观察入门案例,对象注入使用 property 标签的 ref 属性进行注入。

applicationcontext.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">
    <!--  声明 accountDao 对象并交由 Spring 来进行管理与创建  -->
    <bean name="accountDao" class="top.rem.rain.demo2.dao.AccountDaoImpl"/>
    <!--  声明 accountService 对象并交由 Spring 来进行管理与创建  -->
    <bean name="accountService" class="top.rem.rain.demo2.service.AccountServiceImpl">
        <!--    注入 accountDao 对象需要 set 方法    -->
        <property name="accountDao" ref="accountDao"/>
    </bean>
</beans>

其次除了上述的对象注入同时也可以注入简单值和 mapsetlist数组 ,简单值注入使用 property 标签的 value 属性。

package top.rem.rain.demo2.bean;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
 * @Author: LightRain
 * @Description: Set 注入示例
 * @DateTime: 2024-01-01 11:00
 * @Version:1.0
 **/
public class Account {
    /**
     * 账户信息
     */
    private String accountName;
    /**
     * 账户密码
     */
    private String accountPassword;
    /**
     * 系统信息
     */
    private List<String> systemInfo;
    /**
     * 朋友姓名
     */
    private Set<String> friendsName;
    /**
     * 书架
     */
    private Map<Integer,String> bookshelf;
    public void setAccountName(String accountName) {
        this.accountName = accountName;
    }
    public void setAccountPassword(String accountPassword) {
        this.accountPassword = accountPassword;
    }
    public void setSystemInfo(List<String> systemInfo) {
        this.systemInfo = systemInfo;
    }
    public void setFriendsName(Set<String> friendsName) {
        this.friendsName = friendsName;
    }
    public void setBookshelf(Map<Integer, String> bookshelf) {
        this.bookshelf = bookshelf;
    }
    
    @Override
    public String toString() {
        return "Account{" +
                "accountName='" + accountName + '\'' +
                ", accountPassword='" + accountPassword + '\'' +
                ", systemInfo=" + systemInfo +
                ", friendsName=" + friendsName +
                ", bookshelf=" + bookshelf +
                '}';
    }
}

XML 配置如下:

account.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">
    <!-- setter 通过 property 注入属性值,普通类型使用 value -->
    <bean id="account" scope="prototype" class="top.rem.rain.demo2.bean.Account">
        <property name="accountName" value="LightRain"/>
        <property name="accountPassword" value="123456r"/>
        <!-- 注入 List -->
        <property name="systemInfo">
            <list>
                <value>Java运行版本:17.0.6</value>
                <value>Java供应商:Oracle Corporation</value>
                <value>Java安装目录:C:\LightRainData\IDEA\JDK\JDK-17.0.6</value>
                <value>操作系统名称:Windows 11</value>
                <value>操作系统架构:amd64</value>
                <value>操作系统版本:10.0</value>
                <value>用户名称:LightRain</value>
                <value>用户主目录:C:\Users\LightRain</value>
            </list>
        </property>
        <!-- 注入 Set -->
        <property name="friendsName">
            <set>
                <value>古河渚</value>
                <value>古河早苗</value>
                <value>藤琳杏</value>
            </set>
        </property>
        <!-- 注入 Map -->
        <property name="bookshelf">
            <map>
                <entry key="1" value="Java并发编程的艺术"/>
                <entry key="2" value="Java架构之路"/>
                <entry key="3" value="Java性能权威"/>
            </map>
        </property>
    </bean>
</beans>

Account 类中添加一个测试方法,用于查看是否可以成功注入,代码如下:

package top.rem.rain.demo2.bean;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import top.rem.rain.demo2.service.AccountService;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
 * @Author: LightRain
 * @Description: Set 注入示例
 * @DateTime: 2024-01-01 11:00
 * @Version:1.0
 **/
public class Account {
    /**
     * 省略上述贴过的代码...
     */
    
    /**
     * 测试方法
     */
    @Test
    public void test(){
        // 通过 ApplicationContext 加载配置文件
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("account.xml");
        // 多次获取并不会创建多个 account 对象,因为 Spring 默认创建是单实例的作用域
        Account account = (Account) applicationContext.getBean("account");
        System.out.println("account.toString() = " + account.toString());
        /*
          执行结果:account.toString () = Account {accountName='LightRain', accountPassword='123456r', systemInfo=[Java 运行版本:17.0.6, Java 供应商:Oracle Corporation, Java 安装目录:C:\LightRainData\IDEA\JDK\JDK-17.0.6, 操作系统名称:Windows 11, 操作系统架构:amd64, 操作系统版本:10.0, 用户名称:LightRain, 用户主目录:C:\Users\LightRain], friendsName=[古河渚,古河早苗,藤琳杏], bookshelf={1=Java 并发编程的艺术,2=Java 架构之路,3=Java 性能权威}}
         */
    }
}

# 构造器注入

构造器是通过构造方法注入依赖,构造函数的参数一般情况下就是依赖项, Spring 容器会根据 bean 中指定的构造器参数来决定调用哪个构造函数,代码如下:

AccountDao.java
package top.rem.rain.demo3.dao;
/**
 * @author LightRain
 */
public interface AccountDao {
    /**
     * 账户信息
     */
    void accountInfo();
}
AccountDaoImpl.java
package top.rem.rain.demo3.dao;
/**
 * @Author: LightRain
 * @Description: 实现类
 * @DateTime: 2023-12-31 21:01
 * @Version:1.0
 **/
public class AccountDaoImpl implements AccountDao {
    /**
     * 账户信息
     */
    @Override
    public void accountInfo() {
        System.out.printf("用户名称:%s", System.getProperty("user.name"));
    }
}
AccountService.java
package top.rem.rain.demo3.service;
public interface AccountService {
    /**
     * 获取账户
     */
    void getAccount();
}
AccountServiceImpl.java
package top.rem.rain.demo3.service;
import top.rem.rain.demo3.dao.AccountDao;
/**
 * @Author: LightRain
 * @Description: 使用构造器注入依赖
 * @DateTime: 2023-12-31 21:04
 * @Version:1.0
 **/
public class AccountServiceImpl implements AccountService {
    /**
     * 需要注入的 Dao 对象
     */
    private final AccountDao accountDao;
    /**
     * 构造器注入依赖
     *
     * @param accountDao AccountDao
     */
    public AccountServiceImpl(AccountDao accountDao) {
        this.accountDao = accountDao;
    }
    /**
     * 获取账户
     */
    @Override
    public void getAccount() {
        accountDao.accountInfo();
    }
    
}
accountServiceImpl.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 name="accountDao2" class="top.rem.rain.demo3.dao.AccountDaoImpl"/>
    <!--  通过构造器注入依赖  -->
    <bean name="accountService" class="top.rem.rain.demo3.service.AccountServiceImpl">
        <!--  构造方法方式注入依赖  -->
        <constructor-arg name="accountDao" ref="accountDao2"/>
    </bean>
</beans>

AccountServiceImpl 类中添加 main 方法进行测试,完整代码如下:

AccountServiceImpl.java
package top.rem.rain.demo3.service;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import top.rem.rain.demo3.dao.AccountDao;
/**
 * @Author: LightRain
 * @Description: 使用构造器注入依赖
 * @DateTime: 2023-12-31 21:04
 * @Version:1.0
 **/
public class AccountServiceImpl implements AccountService {
    /**
     * 需要注入的 Dao 对象
     */
    private  AccountDao accountDao;
    /**
     * 构造器注入依赖
     *
     * @param accountDao AccountDao
     */
    public AccountServiceImpl(AccountDao accountDao) {
        this.accountDao = accountDao;
    }
    /**
     * 获取账户
     */
    @Override
    public void getAccount() {
        accountDao.accountInfo();
    }
    /**
     * 使用 XML 文件进行配置
     */
    public static void main(String[] args) {
        // 通过 ApplicationContext 加载配置文件
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("accountServiceImpl.xml");
        // 多次获取并不会创建多个 accountService 对象,因为 Spring 默认创建是单实例的作用域
        AccountService accountService = (AccountService) applicationContext.getBean("accountService");
        accountService.getAccount();
        /*
            执行结果:用户名称:LightRain
         */
    }
}

当然跟 set 注入一样,构造器注入也可以传入简单值和集合类型,需要注意当一个 bean 定义中有多个 constructor-arg 标签时,它们的顺序并不重要,因为 Spring 容器会通过传入的依赖参数与类中构造器参数进行比较。可惜的是在某些情况下可能会出现问题,如下 User 类,带有两个构造函数,参数类型和个数都一样,只是顺序不同,这样在 class 中定义是允许的,但对于 Spring 容器来说是一种灾难。

User.java
package top.rem.rain.demo3;
/**
 * @Author: LightRain
 * @Description: 不同顺序的构造参数
 * @DateTime: 2024-01-01 13:42
 * @Version:1.0
 **/
public class User {
    private String name;
    private int age;
    /**
     * 构造一
     * @param name
     * @param age
     */
    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }
    /**
     * 构造二
     * @param age
     * @param name
     */
    public User(int age, String name) {
        this.name = name;
        this.age = age;
    }
}
user.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 name="user" class="top.rem.rain.demo3.User">
        <constructor-arg type="java.lang.String" value="Rain"/>
        <constructor-arg type="int" value="17"/>
    </bean>
</beans>

User 类中添加如下测试方法:

User.java
public static void main(String[] args) {
    // 通过 ApplicationContext 加载配置文件
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("user.xml");
    User user = (User) applicationContext.getBean("user");
    System.out.println("user.age = " + user.age);
    /*
    执行结果:
      user.age = 17
      user.name = Rain
    */
}

在程序运行时 Spring 容器会尝试查找适合的 User 构造函数进而创建 User 对象,由于 constructor-arg 的注入并不重要,从而导致不知该用两种构造函数中的哪一种,这时 user 实例将创建失败, Spring 容器也将启动失败。

庆幸的是 Spring 早为我们预测到了这种情况,因此只要给 Spring 容器一点提示,它便能成功找到适合的构造函数从而创建 user 实例,在 constructor-arg 标签中存在一个 index 属性,通过 index 属性可以告诉 Spring 容器传递的依赖参数的顺序,下面的配置将会使 Spring 容器成功找到第一个构造函数并创建 user 实例。

user.xml
<bean name="user" class="top.rem.rain.demo3.User">
    <constructor-arg index="0" value="Rain"/>
    <constructor-arg index="1" value="17"/>
</bean>

在日常开发中 set 注入和构造器注入会经常混合使用,这并不稀奇,后面还会有注解装配,它在日常开发中更为常用。

# 依赖循环

除了上述情况,还存在一种依赖循环现象,在构造函数注入有一个无法解决的依赖循环问题,如下有两个 bean 分别是 AB 类,这两个 bean 通过构造函数相互依赖,这种情况下 Spring 容器将无法实例化这两个 bean

public class A{
    private B b;
    public A(B b){
        this.b = b;
    }
}
public class B{
    private A a;
    public B(A a){
        this.a = a;
    }
}
demo4.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 name="a" class="top.rem.rain.demo4.A">
        <constructor-arg name="b" ref="b"/>
    </bean>
    <bean name="b" class="top.rem.rain.demo4.B">
        <constructor-arg name="a" ref="a"/>
    </bean>
</beans>

此时由于 A 被创建时希望 B 被注入到自身,然而此时 B 还没有被创建,而且 B 也依赖于 A 这样就导致 Spring 容器左右为难,此时就会无法满足两方的需求,最后就会导致程序崩溃抛出异常。解决这种问题的方式是使用 set 依赖,有时但还是会造成一些不必要的困扰,因此强烈不建议在配置文件中使用依赖循环。

# 注解注入 & 自动装配

# 自动装配 - XML

除了手动注入外 Spring 还为我们提供了自动向 Bean 注入依赖的功能,这个过程被称为自动装配 (Autowired) 。当注入的 bean 特别多时,它将极大地节省编写程序注入的时间,在日常开发中, Spring 自动装配有三种方式: byName(根据名称)byType(根据类型)constructor(根据构造器)

byType 方式中, Spring 容器会基于反射查看 bean 定义的类,然后找到与依赖类型相同的 bean 注入到另外的 bean 中,这个过程需要 set 注入来完成,因此必须存在 set 方法,否则将注入失败。

UserDao.java
package top.rem.rain.demo5.dao;
/**
 * @author LightRain
 */
public interface UserDao {
    /**
     * 系统用户信息
     */
    void systemUserInfo();
}
UserDaoImpl.java
package top.rem.rain.demo5.dao;
/**
 * @Author: LightRain
 * @Description: 具体实现
 * @DateTime: 2024-01-01 17:18
 * @Version:1.0
 **/
public class UserDaoImpl implements UserDao {
    /**
     * 系统用户信息
     */
    @Override
    public void systemUserInfo() {
        System.out.println("系统用户:" + System.getProperty("user.name"));
    }
}
UserService.java
package top.rem.rain.demo5.service;
/**
 * @author LightRain
 */
public interface UserService {
    /**
     * 系统用户信息
     */
    void systemUserInfo();
}
UserServiceImpl.java
package top.rem.rain.demo5.service;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import top.rem.rain.demo5.dao.UserDao;
/**
 * @Author: LightRain
 * @Description: 具体实现
 * @DateTime: 2024-01-01 17:20
 * @Version:1.0
 **/
public class UserServiceImpl implements UserService {
    /**
     * 需要注入的依赖
     */
    private UserDao userDao;
    /**
     * 通过 set 方法注入依赖
     * @param userDao UserDao
     */
    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }
    /**
     * 系统用户信息
     */
    @Override
    public void systemUserInfo() {
        userDao.systemUserInfo();
    }
    
}

使用 XML 配置,通过 bean 标签的 autowire 属性启动名称为 userService 的自动装配功能。

demo5.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 name="userDao" class="top.rem.rain.demo5.dao.UserDaoImpl"/>
    <!--  byType:根据类型自动装配 userDao  -->
    <bean name="userService" autowire="byType" class="top.rem.rain.demo5.service.UserServiceImpl"/>
    
</beans>

UserServiceImpl 中添加一个 main 方法进行测试,代码如下:

UserServiceImpl.java
package top.rem.rain.demo5.service;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import top.rem.rain.demo5.dao.UserDao;
/**
 * @Author: LightRain
 * @Description: 具体实现
 * @DateTime: 2024-01-01 17:20
 * @Version:1.0
 **/
public class UserServiceImpl implements UserService {
    
    // 省略代码...
    
    public static void main(String[] args) {
        ApplicationContext app = new ClassPathXmlApplicationContext("demo5.xml");
        UserService userService = (UserService) app.getBean("userService");
        userService.systemUserInfo();
        /*
          执行结果:系统用户:LightRain
         */
    }
}

byType 模式还存在一种注入失败的情况,由于基于类型注入,因此当 XML 文件中存在多个相同类型名称不同的 Bean 实例时, Spring 容器依赖注入将会注入失败,因为存在多种适合的选项,此时 Spring 容器也无法知道该注入哪种,此时我们需要为 Spring 容器提供帮助,指定注入的那个 Bean 实例。通过 bean 标签的 autowire-candidate 属性设置为 false 来过滤那些不需要注入的 Bean 实例对象。

demo5.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 name="userDao" class="top.rem.rain.demo5.dao.UserDaoImpl"/>
    
    <!--  autowire-candidate=false 来过滤该类型,将不会自动被注入  -->
    <bean name="userDao2" autowire-candidate="false" class="top.rem.rain.demo5.dao.UserDaoImpl"/>
    
    <!--  byType: 根据类型自动装配 userDao  -->
    <bean name="userService" autowire="byType" class="top.rem.rain.demo5.service.UserServiceImpl"/>
</beans>

需要了解的是如果 Spring 容器中没有找到可以注入的 bean 实例对象时,将不会向依赖属性值注入任何 bean , 这时依赖 bean 的属性可能为 null , 因此我们小心处理这种情况,避免不必要的程序崩溃。

下面使用构造器方式自动注入依赖

UserServiceImpl.java
package top.rem.rain.demo5.service;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import top.rem.rain.demo5.dao.UserDao;
/**
 * @Author: LightRain
 * @Description: 具体实现
 * @DateTime: 2024-01-01 17:20
 * @Version:1.0
 **/
public class UserServiceImpl implements UserService {
    /**
     * 需要注入的依赖
     */
    private UserDao userDao;
    /**
     * 通过 constructor 模式使构造器注入依赖
     * @param userDao UserDao
     */
    public UserServiceImpl(UserDao userDao) {
        this.userDao = userDao;
    }
    /**
     * 通过 set 方法注入依赖
     * @param userDao UserDao
     */
//    public void setUserDao(UserDao userDao) {
//        this.userDao = userDao;
//    }
    /**
     * 系统用户信息
     */
    @Override
    public void systemUserInfo() {
        userDao.systemUserInfo();
    }
    
}
demo5.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 name="userDao" class="top.rem.rain.demo5.dao.UserDaoImpl"/>
    
    <!--  constructor 也可以自动装配 userDao  -->
    <bean name="userService" autowire="constructor" class="top.rem.rain.demo5.service.UserServiceImpl"/>
</beans>

总结:不管是 byType 还是 constructor 模式下,如果存在多个相同的 bean 实例都需要使用 autowire-candidate="false" 来将不需要的 bean 实例过滤掉,以免使 Spring 容器找到两个相同实却不知道需要注入哪一个的情况,如果不使用 autowire-candidate 属性来过滤掉不需要注入的 bean 实例, Spring 将会终止程序并抛出 UnsatisfiedDependencyException(为满足依赖异常) 的异常。

# 自动装配 -@Autowired 注解

如果在 bean 实例过多的情况下,手动设置自动注入会非常的耗费时间,好在 Spring 2.5 中引入了 @Autowired 注解,它可以对类成员变量、方法及构造函数进行标注,来完成自动装配工作。通过 @Autowired 注解标注在成员变量时不需要 set 方法, @Autowired 默认按类型匹配,先来看下面的代码示例,当然使用注解前需要先注册注解驱动,这样才可以使用 @Autowired 注解。

配置文件.xml
<!-- 在 xml 配置文件中添加,使用注解时必须启动注解驱动 -->
<context:annotation-config />
UserServiceImpl.java
package top.rem.rain.demo6.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.stereotype.Service;
import top.rem.rain.demo6.dao.UserDao;
/**
 * @Author: LightRain
 * @Description: 具体实现
 * @DateTime: 2024-01-01 17:20
 * @Version:1.0
 **/
public class UserServiceImpl implements UserService {
    // 标注在成员变量
    @Autowired
    private UserDao userDao;
    // 标注在构造器
    @Autowired
    public UserServiceImpl(UserDao userDao) {
        this.userDao = userDao;
    }
    // 标注在 set 方法
    @Autowired
    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }
    /**
     * 系统用户信息
     */
    @Override
    public void systemUserInfo() {
        userDao.systemUserInfo();
    }
}

在上述代码中有三种方式注入 userDao , XML 配置文件中只需要声明 bean 实例即可,在实际开发中,我们只需要选择其中一种方式进行注入就可以,一般情况建议使用成员变量注入,即可以省去 set 方法和构造函数还可以简化代码。

UserServiceImpl.java
package top.rem.rain.demo6.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.stereotype.Service;
import top.rem.rain.demo6.dao.UserDao;
/**
 * @Author: LightRain
 * @Description: 具体实现
 * @DateTime: 2024-01-01 17:20
 * @Version:1.0
 **/
public class UserServiceImpl implements UserService {
    // 标注在成员变量
    @Autowired
    private UserDao userDao;
    /**
     * 系统用户信息
     */
    @Override
    public void systemUserInfo() {
        userDao.systemUserInfo();
    }
}

@Autowired 中还可以传递一个 required=false 属性, false 表示当 userDao 实例存在就注入不存在就忽略,如果值为 true 那就必须进行注入,若 userDao 实例不存在就会抛出异常。由于默认情况下 @Autowired 是按类型匹配的 (byType模式) ,如果需要按名称 (byName模式) 进行匹配,则可以使用 @Qualifier 注解跟 @Autowired 注解一起来使用,注意:在使用注解前必须在 XML 配置中注册注解驱动。

UserServiceImpl.java
package top.rem.rain.demo6.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import top.rem.rain.demo6.dao.UserDao;
/**
 * @Author: LightRain
 * @Description: 具体实现
 * @DateTime: 2024-01-01 17:20
 * @Version:1.0
 **/
public class UserServiceImpl implements UserService {
    @Autowired
    @Qualifier("userDao1")
    private UserDao userDao;
    /**
     * 系统用户信息
     */
    @Override
    public void systemUserInfo() {
        userDao.systemUserInfo();
    }
}
demo6.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"
       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 https://www.springframework.org/schema/context/spring-context.xsd">
    <!-- 使用注解时必须启动注解驱动 -->
    <context:annotation-config />
    <!--  @Qualifier 注解会自动识别 userDao1  -->
    <bean name="userDao1" class="top.rem.rain.demo6.dao.UserDaoImpl"/>
    <bean name="userDao2" class="top.rem.rain.demo6.dao.UserDaoImpl"/>
    <bean name="userService" class="top.rem.rain.demo6.service.UserServiceImpl"/>
</beans>

UserServiceImpl 中添加一个 main 方法进行测试,完整代码如下:

UserServiceImpl.java
package top.rem.rain.demo6.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import top.rem.rain.demo6.dao.UserDao;
/**
 * @Author: LightRain
 * @Description: 具体实现
 * @DateTime: 2024-01-01 17:20
 * @Version:1.0
 **/
public class UserServiceImpl implements UserService {
    @Autowired
    @Qualifier("userDao1")
    private UserDao userDao;
    /**
     * 系统用户信息
     */
    @Override
    public void systemUserInfo() {
        userDao.systemUserInfo();
    }
    public static void main(String[] args) {
        ApplicationContext app = new ClassPathXmlApplicationContext("demo6.xml");
        UserServiceImpl userService = (UserServiceImpl) app.getBean("userService");
        userService.systemUserInfo();
        /*
           执行结果:系统用户:LightRain
         */
    }
}

# 自动装配 -@Resource 注解

@Resource 注解跟 @Autowried 注解具备相同功能,默认按照 byName 模式进行自动注入, @Resource 是由 J2EE 提供的,需要导入 jakarta.annotation-api 坐标,使用 Maven 构建坐标如下:

pom.xml
<dependency>
    <groupId>jakarta.annotation</groupId>
    <artifactId>jakarta.annotation-api</artifactId>
    <version>2.1.1</version>
</dependency>

@Resource 注解可以标注在成员变量和 set 方法上,但不可以标注在构造器。 @Resource 注解有两个重要属性:分别是 nametype 属性。 Spring 容器对 @Resource注解name 属性会解析为 bean 的名称, type 属性则解析为 bean 的类型。因此使用 name 属性,则按照 byName 模式进行自动注入,如果使用 type 属性则按照 byType 模式进行自动注入。倘若两个属性都不指定的话 Spring 容器将通过反射技术默认按照 byName 模式进行注入。

UserServiceImpl.java
package top.rem.rain.demo6.service;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import top.rem.rain.demo6.dao.UserDao;
/**
 * @Author: LightRain
 * @Description: 具体实现
 * @DateTime: 2024-01-01 17:20
 * @Version:1.0
 **/
public class UserServiceImpl implements UserService {
    @Autowired
    @Qualifier("userDao1")
    private UserDao userDao;
    // 此处等价于 @Autowired+@Qualifier
    @Resource(name = "userDao")
    private UserDao userDao;
    // 也可以在 set 方法上进行注解声明
    @Resource(name = "userDao")
    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }
}

# 自动装配 -@Value 注解及文件读取

关于 @Autowired@Resource 自动装配的依赖注入并不适合简单值类型如: intbooleanlongStringEnum 等,对于这些类型 Spring 容器提供了 @Value 注入方式,它可以解决很多硬编码问题, @Value 接收一个 String 的值,该值指定了将要被注入到内置的 Java 类型属性值,不必关心类型转换,在大多数情况下 Spring 容器都已自动处理好了,一般情况下 @Value 注解会和 properties 格式的文件结合使用。

jdbc.properties
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://127.0.0.1:3306/spring_ioc?characterEncoding=UTF-8&allowMultiQueries=true
jdbc.username=root
jdbc.password=123456
UserServiceImpl.java
package top.rem.rain.demo6.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import top.rem.rain.demo6.dao.UserDao;
/**
 * @Author: LightRain
 * @Description: 具体实现
 * @DateTime: 2024-01-01 17:20
 * @Version:1.0
 **/
public class UserServiceImpl implements UserService {
    @Autowired
    @Qualifier("userDao1")
    private UserDao userDao;
    /**
     * 使用 $ 占位符获取
     */
    @Value("${jdbc.url}")
    private String url;
    /**
     * SpEL 表达方式,其中代表 xml 配置文件中的 id 值 configProperties
     */
    @Value("#{configProperties['jdbc.username']}")
    private String userName;
    /**
     * 系统用户信息
     */
    @Override
    public void systemUserInfo() {
        System.out.println("url = " + url);
        System.out.println("userName = " + userName);
        userDao.systemUserInfo();
    }
    public static void main(String[] args) {
        ApplicationContext app = new ClassPathXmlApplicationContext("demo6.xml");
        UserServiceImpl userService = (UserServiceImpl) app.getBean("userService");
        userService.systemUserInfo();
        /*
           执行结果:
            url = jdbc:mysql://127.0.0.1:3306/spring_ioc?characterEncoding=UTF-8&allowMultiQueries=true
            userName = root
            系统用户:LightRain
         */
    }
}
demo6.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"
       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 https://www.springframework.org/schema/context/spring-context.xsd">
    <!-- 基于占位符方式 配置单个 properties -->
    <!--<context:property-placeholder location="jdbc.properties"/>-->
    <!-- 基于占位符方式 配置多个 properties -->
    <bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PreferencesPlaceholderConfigurer">
        <property name="location" value="jdbc.properties"/>
    </bean>
    <!-- 基于 SpEL 表达式 配置多个 properties id 值为 configProperties 提供 java 代码中使用 -->
    <bean id="configProperties" class="org.springframework.beans.factory.config.PropertiesFactoryBean">
        <property name="locations">
            <list>
                <value>classpath:jdbc.properties</value>
            </list>
        </property>
    </bean>
    <!-- 使用注解时必须启动注解驱动 -->
    <context:annotation-config />
    <bean name="userDao1" class="top.rem.rain.demo6.dao.UserDaoImpl"/>
    <bean name="userDao2" class="top.rem.rain.demo6.dao.UserDaoImpl"/>
    <bean name="userService" class="top.rem.rain.demo6.service.UserServiceImpl"/>
</beans>

# SpringIOC 容器管理

# Bean 的命名

每一个交给 SpringIOC(Spring容器) 创建的对象必须被分配至少一个名称,如果开发者没有提供, Spring 容器将会为其分配一个内部名称,通过 Bean 的名称,我们可以在其它类中查找该类并使用,在前面的案例中也是通过 Bean 名称获取到实际对象并执行对应的操作。在基于 XML 配置信息中,可以使用 id 属性来为一个 Bean 分配名称,在同一个 XML 配置文件中 id 是唯一的不可重复,当然也可以使用 name 来为 Bean 分配名称,使用 name 属性可以分配多个名称,可以使用空格、逗号、分号来分割给定 Bean 的多个名称,而 id 属性则不可以这样使用。

demo6.xml
<bean name="userDao1,userDao3" class="top.rem.rain.demo6.dao.UserDaoImpl"/>
<bean id="userDao2" class="top.rem.rain.demo6.dao.UserDaoImpl"/>

name 属性中声明了两个名称,除了第一个名称外,其它的名称都被称为别名。除了在 Bean 中定义名称外还可以用 alias 标签来向 Bean 赋予别名。

demo6.xml
<bean name="userDao1,userDao3" class="top.rem.rain.demo6.dao.UserDaoImpl"/>
<!--  name 属性指明要给哪个 Bean 赋予别名,alias 则指明赋予的别名  -->
<alias name="userDao1" alias="userDao4"/>

如果我们想要配置的 Bean 对象已存在,并且希望向一些 Bean 赋予特别的名称,此时别名就相当有用了。上述的 Bean 对象声明使用都在 xml 内手动声明的方式,当 Bean 对象过多时,管理 Bean 就会非常繁琐,此时 Spring 提供了基于 Java 注解的配置方式,下面将使用 @Service@Repository 注解,在使用 @Autowired 注解时需要在 XML 声明注解驱动。

AccountServiceImpl.java
package top.rem.rain.demo7.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.stereotype.Service;
import top.rem.rain.demo7.dao.AccountDao;
/**
 * @Author: LightRain
 * @Description: 使用注解注入依赖
 * @DateTime: 2023-12-31 21:04
 * @Version:1.0
 **/
@Service // 将此类彻底交给 Spring 容器来管理,@Service 注解代表此类是业务层
public class AccountServiceImpl implements AccountService {
    
    @Autowired
    private AccountDao accountDao;
    
    /**
     * 获取账户
     */
    @Override
    public void getAccount() {
        accountDao.accountInfo();
    }
    /**
     * 测试
     * @param args
     */
    public static void main(String[] args) {
        // 通过 ApplicationContext 加载配置文件
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("demo7.xml");
        // 多次获取并不会创建多个 accountService 对象,因为 Spring 默认创建是单实例的作用域
        AccountService accountService = (AccountService) applicationContext.getBean("accountServiceImpl");
        accountService.getAccount();
        /*
            执行结果:用户名称:LightRain
         */
    }
}
AccountDaoImpl.java
package top.rem.rain.demo7.dao;
import org.springframework.stereotype.Repository;
/**
 * @Author: LightRain
 * @Description: 实现类
 * @DateTime: 2023-12-31 21:01
 * @Version:1.0
 **/
@Repository // 此注解跟 @Component 具有相同效果
public class AccountDaoImpl implements AccountDao {
    /**
     * 账户信息
     */
    @Override
    public void accountInfo() {
        System.out.printf("用户名称:%s", System.getProperty("user.name"));
    }
}

有了注解声明,我们就不需要在 XML 中声明以上两个 Bean 了,但需要告诉 Spring 注解的 Bean 在哪个包下面,因此需要添加包扫描机制,此时需要启用 Springcontext 命名空间,配置如下:

demo7.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"
       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 https://www.springframework.org/schema/context/spring-context.xsd">
    <!-- 声明包扫描 -->
    <context:component-scan base-package="top.rem.rain.demo7" />
</beans>

以上的声明方式与之前在 XML 中声明的 Bean 效果是相同的,这里我们需要明白可以使用 @Component 注解来达到与 @Service@Repository 注解相同效果, @Component@Service 注解的含义并没有什么差异,只不过 @Service 更能让我们明白该类是业务类罢了。至于 @Repository 注解在表示数据访问层含义的同时还能启用与 Spring 数据访问相关链的其它功能,同时还可以给 @Component@Service@Repository 注解给定一个 String 值的名称,如果没有提供名称,那么会默认使用类名当做 Bean 名称 (第一个字母将会变为小写)。

@Service("accountService")
public class AccountServiceImpl implements AccountService {
    @Autowired
    private AccountDao accountDao;
    // ...
}
@Repository("accountDao")
public class AccountDaoImpl implements AccountDao {
    // ...
}

到这我们已经知道了在 Spring 框架中提供了与 @Component 注解具有同等效果的三个注解, @Repository 注解用于对 Dao 实现类进行标注, @Service 注解用于对 Service 实现类进行标注, @Controller 注解用于对 Controller 实现类进行标注 (控制器层),同时还了解了 Spring 容器通过 XMLbean 标签配置和 Java 注解两种声明 Bean 的方式,以后日常开发中将使用 SpringBoot 全局使用注解进行配置 (此时了解即可)。

# Bean 的实例化方法

在日常开发中创建对象最常用的是通过类的构造方法,事实上 Spring 容器正常情况下也是通过构造方法创建 bean 实例的。

Account.java
package top.rem.rain.demo7.bean;
/**
 * @Author: LightRain
 * @Description: Bean 的实例化方法
 * @DateTime: 2024-01-02 18:47
 * @Version:1.0
 **/
public class Account {
    private String userName;
    private String password;
    public Account(String userName, String password) {
        this.userName = userName;
        this.password = password;
    }
    /**
     * 默认无参构造
     */
    public Account() {
    }
    public String getUserName() {
        return userName;
    }
    public void setUserName(String userName) {
        this.userName = userName;
    }
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
}
demo7.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"
       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 https://www.springframework.org/schema/context/spring-context.xsd">
    <!-- 声明包扫描 -->
    <context:component-scan base-package="top.rem.rain.demo7" />
    <!-- 默认构造创建,并通过 property 注入属性值 -->
    <bean id="account" class="top.rem.rain.demo7.bean.Account" >
        <property name="userName" value="root" />
        <property name="password" value="123456" />
    </bean>
    <!-- 带参构造创建,并通过 constructor-arg 注入属性值 -->
    <bean id="account2" class="top.rem.rain.demo7.bean.Account" >
        <constructor-arg name="userName" value="LightRain" />
        <constructor-arg name="password" value="123456" />
    </bean>
</beans>

除了构造器创建外还存在另外两种比较冷门的创建方式,那就是静态方法和实例工厂方法构造,了解一下即可很少使用,代码如下:

BeanFactory.java
package top.rem.rain.demo7.bean;
/**
 * @Author: LightRain
 * @Description: 静态方法和实例工厂构造
 * @DateTime: 2024-01-02 18:54
 * @Version:1.0
 **/
public class BeanFactory {
    /**
     * 静态工厂创建
     *
     * @return Account
     */
    public static Account createAccount() {
        Account account = new Account();
        account.setUserName("root");
        account.setPassword("123456");
        return account;
    }
    /**
     * 通过工厂实例创建
     *
     * @return Account
     */
    public Account createAccount2() {
        Account account = new Account();
        account.setUserName("LightRain");
        account.setPassword("1234");
        return account;
    }
}
<?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">
    <!-- 静态工厂创建,调用静态方法 createAccount -->
    <bean id="account" class="top.rem.rain.demo7.bean.BeanFactory" factory-method="createAccount"/>
    <!-- 工厂实例创建,先创建工厂类在调用方法 createAccount2 -->
    <bean id="factory" class="top.rem.rain.demo7.bean.BeanFactory" />
    <bean id="account2" factory-bean="factory"  factory-method="createAccount2"/>
</beans>

在日常开发中使用构造实例化 bean 即可,后面的两种方式很少使用。

# Bean 的重写机制

Bean 的重写机制并不神秘,主要是当不同的 XML 文件中出现同名 id 属性的 bean 时读取的优先级问题,先来看下面两个示例,定义两个 Spring 配置文件并同时加载它们。

beanRewrite1.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">
    <!-- 默认构造创建,并通过 property 注入属性值 -->
    <bean id="account" class="top.rem.rain.demo7.bean.Account" >
        <property name="userName" value="beanRewrite1" />
        <property name="password" value="654321" />
    </bean>
</beans>
beanRewrite2.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">
    <!-- 默认构造创建,并通过 property 注入属性值 -->
    <bean id="account" class="top.rem.rain.demo7.bean.Account" >
        <property name="userName" value="beanRewrite2" />
        <property name="password" value="123456" />
    </bean>
</beans>

下面是测试方法,获取 bean 并调用

public static void main(String[] args) {
    ApplicationContext app = new ClassPathXmlApplicationContext("beanRewrite1.xml","beanRewrite2.xml");
    Account account = (Account) app.getBean("account");
    System.out.println("account.getUserName() = " + account.getUserName());
    System.out.println("account.getPassword() = " + account.getPassword());
    /*
      执行结果:
        account.getUserName () = beanRewrite2
        account.getPassword () = 123456
    */
}

执行结果很显然是后者,在不同的 XML 配置文件中使用相同 id 来命名,并声明相同类型的 bean 对象时, Spring 容器会默认加载最后添加的 beanRewrite2.xml 配置文件,也就是说 Bean 重写机制是当声明的 bean 名称一样时,后者会覆盖掉前者,我们还需要明确的一点是,在 web 开发中,一般都会将配置进行分层管理,然后通过一个主配置文件来聚合它 (springApplication.xml) , 在这种情况下分层的配置文件属于 springApplication.xml 的子文件,在这样的关系下遇到上述情况,一般都是子文件的优先级高,因此会加载子文件的 bean , 如在 beanRewrite1.xml 主文件中导入子文件 beanRewrite2.xml

beanRewrite1.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">
    <!-- 默认构造创建,并通过 property 注入属性值 -->
    <bean id="account" class="top.rem.rain.demo7.bean.Account" >
        <property name="userName" value="beanRewrite1" />
        <property name="password" value="654321" />
    </bean>
    <!-- 导入子文件 -->
    <import resource="beanRewrite2.xml" />
    
</beans>

重新执行测试方法来看结果:

public static void main(String[] args) {
    ApplicationContext app = new ClassPathXmlApplicationContext("beanRewrite1.xml","beanRewrite2.xml");
    Account account = (Account) app.getBean("account");
    System.out.println("account.getUserName() = " + account.getUserName());
    System.out.println("account.getPassword() = " + account.getPassword());
    /*
      执行结果:
        account.getUserName () = beanRewrite2
        account.getPassword () = 123456
    */
}

执行结果与之前是一样的,上述配置会优先加载 beanRewrite2.xml 中的 account , 从而忽略 beanRewrite1.xml 中的 account

  • 关于分层管理配置文件的命名一般按如下名称:
    • spring-web.xmlweb 层相关 bean 的声明。
    • spring-service.xmlservice 层相关 bean 的声明。
    • spring-dao.xmldao 层相关 bean 的声明。
    • spring-tx.xml :事务相关 bean 和规则声明。
    • spring-security.xml :安全相关声明。
    • spring-application.xml :聚集子配置文件的总 bean 声明。

# Bean 的作用域

# singleton 作用域

Bean 的作用域是指 Spring 容器在创建 Bean 后的生存周期,即:由创建到销毁的整个过程。在之前创建的所有 Bean 的作用域都是 singleton , 它是 Spring 默认的,在这样的作用域下每一个 Bean 实例只会被创建一次,而且 Spring 容器在整个应用程序生存周期中都可以使用该实例。因此在之前的代码中 Spring 容器创建 Bean 后,通过代码获取的 bean 无论多少次,它们都是同一个实例对象,我们可以使用 bean 标签的 scope 属性来指定一个 Bean 的作用域。

singletonScope.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">
    <!-- 默认情况下无需声明 Singleton -->
    <bean name="accountDao" scope="singleton" class="top.rem.rain.demo7.dao.AccountDaoImpl"/>
</beans>

# prototype 作用域

prototype 也是比较常用的作用域,它代表每次获取 Bean 实例时都会创建一个新实例对象,类似 new 操作符。

prototypeScope.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">
    <!-- 作用域:prototype -->
    <bean name="accountDao" scope="prototype" class="top.rem.rain.demo7.dao.AccountDaoImpl"/>
</beans>
Test.java
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import top.rem.rain.demo7.dao.AccountDao;
/**
 * @Author: LightRain
 * @Description: 测试类
 * @DateTime: 2024-01-02 20:16
 * @Version:1.0
 **/
public class Test {
    @org.junit.jupiter.api.Test
    public void prototypeScopeTest(){
        ApplicationContext app = new ClassPathXmlApplicationContext("prototypeScope.xml");
        AccountDao accountDao1 = (AccountDao) app.getBean("accountDao");
        AccountDao accountDao2 = (AccountDao) app.getBean("accountDao");
        System.out.println("accountDao1.toString() = " + accountDao1.toString());
        System.out.println("accountDao2.toString() = " + accountDao2.toString());
        /*
        执行结果:
        accountDao1.toString () = top.rem.rain.demo7.dao.AccountDaoImpl@75e91545
        accountDao2.toString () = top.rem.rain.demo7.dao.AccountDaoImpl@60d1a32f
         */
    }
}

在上面的执行结果可以看出两个是完全不同的实例对象,当然也可以通过注解来声明作用域。

AccountDaoImpl.java
package top.rem.rain.demo7.dao;
import org.springframework.context.annotation.Scope;
@Scope("prototype")
public class AccountDaoImpl implements AccountDao {
    // ....
}

还存在一种特殊的情景,当一个作用域为 singletonBean 依赖于一个作用域为 prototypeBean 时,配置如下:

<!-- 作用域 prototype-->
<bean name="accountDao" scope="prototype" class="top.rem.rain.demo7.dao.AccountDaoImpl"/>
<!-- 作用域 Singleton -->
<bean name="accountService" class="top.rem.rain.demo7.service.AccountServiceImpl">
    <!-- 注入作用域为 prototype 的 accountDao 对象时需要 set 方法 -->
    <property name="accountDao" ref="accountDao"/>
</bean>

在这种情况下希望每次 getBean("accountService") 处理的都是一个新的 accountDao 实例对象,但是由于 accountService 的依赖是在 Bean 被创建时注入的,而 accountService 是一个 singleton 作用域,整个生存周期中只会创建一次,因此它所依赖的 accountDao 实例对象也只会被注入一次,此后将不会再注入任何新的 accountDao 实例对象。想要解决这个问题,只能放弃使用依赖注入功能,使用代码来实现。

下面将通过实现 ApplicationContextAware 接口并重写 setApplicationContext 方法,这样 Spring 容器在创建 AccountServiceImpl 实例时会自动注入 ApplicationContext 对象,此时就可以通过 ApplicationContext 来获取 accountDao 实例了,这样就可以保证每次获取的 accountDao 实例都是新的 (此处了解即可,在实际开发中一般不会这么要求)。

demo8.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 name="accountDao" scope="prototype"  class="top.rem.rain.demo8.dao.AccountDaoImpl"/>
  <!-- accountDao 通过代码注入 -->
  <bean name="accountService" class="top.rem.rain.demo8.service.AccountServiceImpl" />
</beans>

代码注入示例如下:

AccountServiceImpl.java
package top.rem.rain.demo8.service;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.stereotype.Service;
import top.rem.rain.demo8.dao.AccountDao;
/**
 * @author LightRain
 */
public class AccountServiceImpl implements AccountService, ApplicationContextAware {
  private ApplicationContext applicationContext;
  @Override
  public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
    this.applicationContext = applicationContext;
  }
  /**
   * 获取账户
   */
  @Override
  public void getAccount() {
    System.out.println("getAccountDao().toString() = " + getAccountDao().toString());
  }
  private AccountDao getAccountDao() {
    return applicationContext.getBean(AccountDao.class);
  }
}

测试代码如下:

Test.java
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import top.rem.rain.demo7.dao.AccountDao;
import top.rem.rain.demo8.service.AccountService;
/**
 * @Author: LightRain
 * @Description: 测试类
 * @DateTime: 2024-01-02 20:16
 * @Version:1.0
 **/
public class Test {
    @org.junit.jupiter.api.Test
    public void prototypeScopeTest2(){
        // 加载配置文件
        ApplicationContext applicationContext=new ClassPathXmlApplicationContext("demo8.xml");
        // 测试获取不同实例的 AccountDao
        AccountService accountService= (AccountService) applicationContext.getBean("accountService");
        accountService.getAccount();
        AccountService accountService1= (AccountService) applicationContext.getBean("accountService");
        accountService1.getAccount();
        /*
        执行结果:
            getAccountDao ().toString () = top.rem.rain.demo8.dao.AccountDaoImpl@60dce7ea
            getAccountDao ().toString () = top.rem.rain.demo8.dao.AccountDaoImpl@662f5666
         */
    }
}

从执行结果可以看出用这种方式每次获取的 AccountDao 的实例都是不同的,这样就解决了刚才的问题,另一种情况是当一个作用域为 prototypeBean 依赖于一个 singleton 作用域的 Bean 时,解决方法跟上述是相同的。

注意:当一个 Bean 被设置为 prototype 作用域后 Spring 就不会对一个 bean 的整个生命周期负责,容器在初始化、配置、装饰或者是装配完一个 prototype 实例后,将它交给客户端,随后就对该 prototype 作用域的实例不再关心了。

因此我们需要慎用它,在一般情况下,对有状态的 bean 应该使用 prototype 作用域,而对无状态的 bean 则应该使用 singleton 作用域。至于有状态就是该 bean 具有保存信息功能,不能共享,否则会造成线程安全问题,而无状态则不需要保存信息,是线程安全的,可以共享,在 Spring 中大部分 bean 都是 singleton ,整个生命周期中只会存在一个。

# request&session 作用域

Spring2.5 中专门针对 Web 应用程序引进了 requestsession 这两种作用域,关于 request 作用域,对于每次 http 请求到达应用程序时, Spring 容器会创建一个全新的 request 作用域的 bean 实例,且该 bean 实例仅在当前 httprequest 作用域内有效,整个请求过程也只会使用相同的 bean 实例,因此我们可以根据需要放心更改所建实例的内部状态,而其它 http 请求则创建新实例的 bean 互不干扰,当处理请求结束, request 作用域的 bean 实例将被销毁,如在接收参数时可能需要一个 bean 实例来装载一些参数,显然每次请求参数几乎不会相同,因此希望 bean 实例每次都是足够新的而且只在 request 作用域范围内有效。那就是 Session 每当创建一个新的 HttpSession 时就会创建一个 Session 作用域的 bean ,并且该实例 bean 会伴随着会话的存在而存在。

SingletonBean.java
package top.rem.rain.demo9.bean;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
@Scope("singleton")
@Component
public class SingletonBean {
}
PrototypeBean.java
package top.rem.rain.demo9.bean;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Component;
@Scope(value = "prototype",proxyMode = ScopedProxyMode.TARGET_CLASS)
@Component
public class PrototypeBean {
}
RequestBean.java
package top.rem.rain.demo9.bean;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Component;
@Scope(value = "request",proxyMode = ScopedProxyMode.TARGET_CLASS)
@Component
public class RequestBean {
}
SessionBean.java
package top.rem.rain.demo9.bean;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Component;
@Scope(value = "session",proxyMode = ScopedProxyMode.TARGET_CLASS)
@Component
public class SessionBean {
}

上述分别创建了四种不同作用域的 Bean 并使用注解进行作用域的声明, @Component 代表它们是组件类,需要 Spring 容器帮忙创建, @Scope 表示作用域,其中的 value 指明是哪种作用域,在除了 SingletonBean 外,其它 Bean 还使用了 proxyMode 用来指明哪种代理模式,这里没有接口,因此使用 CGLib 进行代理 (此处后面会说明为什么这样做)。接下来将使用 SpringBoot3.0 进行测试。

BeanStatusController.java
package top.rem.rain.demo9.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import top.rem.rain.demo9.bean.PrototypeBean;
import top.rem.rain.demo9.bean.RequestBean;
import top.rem.rain.demo9.bean.SessionBean;
import top.rem.rain.demo9.bean.SingletonBean;
/**
 * @Author: LightRain
 * @Description: Controller
 * @DateTime: 2024-01-02 23:22
 * @Version:1.0
 **/
@Controller
public class BeanStatusController {
    @Autowired
    private SingletonBean singletonBean;
    @Autowired
    private PrototypeBean prototypeBean;
    @Autowired
    private RequestBean requestBean;
    @Autowired
    private SessionBean sessionBean;
    @RequestMapping("/test")
    @ResponseBody
    public void test(){
        println();
    }
    public void println(){
        System.out.println("第一次的singleton是 :" + singletonBean);
        System.out.println("第二次的singleton是 :" + singletonBean);
        System.out.println("第一次的prototype是 :" + prototypeBean);
        System.out.println("第二次的prototype是 :" + prototypeBean);
        System.out.println("第一次的requestBean是 :" + requestBean);
        System.out.println("第二次的requestBean是 :" + requestBean);
        System.out.println("第一次的sessionBean是 :" + sessionBean);
        System.out.println("第二次的sessionBean是 :" + sessionBean);
        System.out.println("===========================================");
    }
}

启动 SpringBoot 服务后使用浏览器连续进行三次访问,访问的路径是本地的 http://localhost:8080/test , 三次的访问结果如下:

访问结果
第一次的 singleton 是 :top.rem.rain.demo9.bean.SingletonBean@41576169
第二次的 singleton 是 :top.rem.rain.demo9.bean.SingletonBean@41576169
第一次的 prototype 是 :top.rem.rain.demo9.bean.PrototypeBean@752f81d6
第二次的 prototype 是 :top.rem.rain.demo9.bean.PrototypeBean@2a49795
第一次的 requestBean 是 :top.rem.rain.demo9.bean.RequestBean@232243da
第二次的 requestBean 是 :top.rem.rain.demo9.bean.RequestBean@232243da
第一次的 sessionBean 是 :top.rem.rain.demo9.bean.SessionBean@24c92325
第二次的 sessionBean 是 :top.rem.rain.demo9.bean.SessionBean@24c92325
===========================================
第一次的 singleton 是 :top.rem.rain.demo9.bean.SingletonBean@41576169
第二次的 singleton 是 :top.rem.rain.demo9.bean.SingletonBean@41576169
第一次的 prototype 是 :top.rem.rain.demo9.bean.PrototypeBean@2b86e029
第二次的 prototype 是 :top.rem.rain.demo9.bean.PrototypeBean@25410b29
第一次的 requestBean 是 :top.rem.rain.demo9.bean.RequestBean@217e8b59
第二次的 requestBean 是 :top.rem.rain.demo9.bean.RequestBean@217e8b59
第一次的 sessionBean 是 :top.rem.rain.demo9.bean.SessionBean@24c92325
第二次的 sessionBean 是 :top.rem.rain.demo9.bean.SessionBean@24c92325
===========================================
第一次的 singleton 是 :top.rem.rain.demo9.bean.SingletonBean@41576169
第二次的 singleton 是 :top.rem.rain.demo9.bean.SingletonBean@41576169
第一次的 prototype 是 :top.rem.rain.demo9.bean.PrototypeBean@192a7e9b
第二次的 prototype 是 :top.rem.rain.demo9.bean.PrototypeBean@17ed83ce
第一次的 requestBean 是 :top.rem.rain.demo9.bean.RequestBean@13d215eb
第二次的 requestBean 是 :top.rem.rain.demo9.bean.RequestBean@13d215eb
第一次的 sessionBean 是 :top.rem.rain.demo9.bean.SessionBean@24c92325
第二次的 sessionBean 是 :top.rem.rain.demo9.bean.SessionBean@24c92325
===========================================

从结果中可以看出 singletonBean 永远只有一个实例,而 prototypeBean 则每次被获取都会创建新实例,对应 RequestBean 在同一次的 http 请求中是同一个实例,当请求结束 RequestBean 也随着被销毁。在新的 http 请求中则会生成新的 RequestBean 实例,对于 SessionBean 由于是在同一个浏览器中访问属于同一次会话,因此 SessionBean 实例都是同一个实例对象。

下面将使用另一个浏览器进行访问来查看 SessionBean 是否会变化。

访问结果
第一次的 singleton 是 :top.rem.rain.demo9.bean.SingletonBean@41576169
第二次的 singleton 是 :top.rem.rain.demo9.bean.SingletonBean@41576169
第一次的 prototype 是 :top.rem.rain.demo9.bean.PrototypeBean@2a241147
第二次的 prototype 是 :top.rem.rain.demo9.bean.PrototypeBean@24e3ea73
第一次的 requestBean 是 :top.rem.rain.demo9.bean.RequestBean@20dc7a37
第二次的 requestBean 是 :top.rem.rain.demo9.bean.RequestBean@20dc7a37
第一次的 sessionBean 是 :top.rem.rain.demo9.bean.SessionBean@65dee048
第二次的 sessionBean 是 :top.rem.rain.demo9.bean.SessionBean@65dee048
===========================================

从访问结果中可以看出 SessionBean 已经发生了改变,这也就证明在不同的会话中 SessionBean 实例是不同的,但为什么需要在其它三种作用域上设置代理模式呢?这个问题的本质与前面的 singleton 作用域的 Bean 依赖于 prototype 作用域的 Bean 是相同的原理。

由于 Spring 容器只会在创建 bean 实例时帮助我们注入该实例 bean 所依赖的其它 bean 实例对象,而且只会注入一次,这并不是 requestsession 作用域所希望看到的,毕竟他们都需要在不同的场景下注入新的实例对象而不是唯一不变的实例对象。

为了解决这个问题,必须放弃直接在 XML 中注入 bean 实例,改用 Java 代码方式 (手动实现 ApplicationContextAware 接口) 或者注解的方式 ( @Autowired ) 进行注入,在示例中选择了注解的方式进行注入,并在 bean 的声明中声明了动态代理模式,幸运的是 Spring 容器是允许这样做的,以至于 Spring 通过代理的方式生成新的代理实例 bean 以此来满足创建新实例的需求。

在程序运行期间,当一个方法调用到达该代理对象时, Spring 容器便会尝试在当前的 RequestSession 会话中获取目标对象 Bean ,如果已存在则使用该 Bean 否则代理方法将创建新实例 Bean 来处理请求或会话,注意:这里指的是一次 http 请求或一次会话的过程。

如果希望 requestsession 作用域通过 xml 配置文件方式声明必须在 bean 标签中放置 <apo:scoped-proxy> 子标签,该作用与注解声明代理模式效果是相同的,经过测试这种声明代理的方式不适用于 prototype 作用域,该作用域生效的方式只有基于注解方式和基于实现 ApplicationContextAware 接口的两种方式。

<?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="requestBean" scope="request" class="top.rem.rain.demo9.bean.RequestBean" >
    <!-- 声明 aop 代理 -->
    <aop:scoped-proxy />
  </bean>
  <bean id="sessionBean" scope="request" class="top.rem.rain.demo9.bean.SessionBean" >
    <!-- 声明 aop 代理 -->
    <aop:scoped-proxy />
  </bean>
  
</beans>

如果 web 层使用的是 SpringMVC 来处理 web 请求则不需要做任何事情就可以使 RequestSession 作用域生效,倘若使用的是其它 web 层框架请务必在 web.xml 中声明如下监视器,以便使 RequestSession 作用域正常工作,配置代码如下:

web.xml
<web-app>
    <listener>
        <listener-class>org.springframework.web.context.request.RequestContextListener<listener-class>
    </listener>
</web-app>

# globalSession 作用域

它类似于 Session 作用域,相当于全局变量,类似 ServletApplication ,适用基于 portletweb 应用程序,请注意: portlet 在这指的是分布式开发,而不是 portlet 语言开发。

# 依赖注入 & IOC 的区别

  • DI(依赖注入) :在 Spring 创建对象的过程中,把对象依赖的属性注入到类中。
  • IOC(控制反转) :将对象的创建权交由 Spring 来进行管理。