Spring(1-IOC/DI入门)

70 阅读41分钟

Spring相关概念

Spring组成

Spring框架主要的优势是在 简化开发框架整合 上,至于如何实现就是我们要学习Spring框架的主要内容:

简化开发: Spring框架中提供了两个大的核心技术,分别是:

  • IOC
  • AOP
    • 事务处理(这属于AOP的具体应用,可以简化项目中的事务管理)

框架整合: Spring在框架整合这块已经做到了极致,它可以整合市面上几乎所有主流框架,比如:MyBatis、MyBatis-plus、Struts、Struts2、Hibernate……

  • 在Spring框架的学习中,我们主要是学习如何整合MyBatis。

综上所述,对于Spring的学习,主要学习四块内容:

  1. IOC
  2. 整合Mybatis(IOC的具体应用)
  3. AOP
  4. 声明式事务(AOP的具体应用)

Spring生态圈

官网:spring.io, 从官网可以了解到:

  • Spring能做什么:用以开发web、微服务以及分布式系统等,光这三块就已经占了JavaEE开发的九成多。
  • Spring并不是单一的一个技术,而是一个大家族,可以从官网的Projects中查看其包含的所有技术。

Spring发展到今天已经形成了一种开发的生态圈,Spring提供了若干个项目,每个项目用于完成特定的功能。

  • Spring已形成了完整的生态圈,也就是说我们可以完全使用Spring技术完成整个项目的构建、设计与开发。
  • Spring有若干个项目,可以根据需要自行选择,把这些个项目组合起来,起了一个名称叫Spring全家桶,如下图所示: image.png

说明:

图中的图标都代表什么含义,可以进入 spring.io/projects 网站进行对比查看。
这些技术并不是所有的都需要学习,额外需要重点关注Spring FrameworkSpringBootSpringCloud:

  • Spring Framework:Spring框架,是Spring中最早最核心的技术,也是所有其他技术的基础。
  • SpringBoot:Spring是来简化开发,而SpringBoot是来帮助Spring在简化的基础上能更快速进行开发。
  • SpringCloud:这个是用来做分布式之微服务架构的相关开发。

除了上面的这三个技术外,还有很多其他的技术,也比较流行,如SpringData,SpringSecurity等,这些都可以被应用在我们的项目中。我们这里所学习的Spring其实指的是Spring Framework。

  • 随着时间推移,版本不断更新维护,目前最新的是Spring5
  • Spring其实是Spring家族中的Spring Framework
  • Spring Framework是Spring家族中其他框架的底层基础,学好Spring可以为其他Spring框架的学习打好基础

Spring系统架构

前面我们说spring指的是Spring Framework,那么它其中都包含哪些内容以及我们该如何学习这个框架?

针对这些问题,我们将从系统架构图课程学习路线来进行说明:

系统架构图

Spring Framework的5版本目前没有最新的架构图,而最新的是4版本,Spring 4 的架构图清晰地展示了其分层架构”的理念,每一层都构建在下一层之上,并提供了不同的服务,开发者可以根据需要选择使用其中任意一层。 image.png

  1. 核心层: Core Container:核心容器,这个模块是Spring最核心的模块,其他的都需要依赖该模块
  2. AOP层:
  • AOP:面向切面编程,它依赖核心层容器,目的是在不改变原有代码的前提下对其进行功能增强
  • Aspects:AOP是思想,Aspects是对AOP思想的具体实现
  1. 数据层:
  • Data Access:数据访问,Spring全家桶中有对数据访问的具体实现技术
  • Data Integration:数据集成,Spring支持整合其他的数据层解决方案,比如Mybatis
  • Transactions:事务,Spring中事务管理是Spring AOP的一个具体实现,也是后期学习的重点内容
  1. Web层:这一层的内容将在SpringMVC框架具体学习
  2. Test层:该模块主要整合了JUnit(或TestNG)测试框架来完成单元测试和集成测试

Spring核心概念

在Spring核心概念这部分内容中主要包含IOC/DIIOC容器Bean,那么问题就来了,这些都是什么呢?

目前项目中的问题

数据层实现:

// BookDaoImpl1.java
public class BookDaoImpl1 implements BookDao {
    public void save() {
        System.out.println("book dao save ... 1");
    }
}
// BookDaoImpl2.java
public class BookDaoImpl2 implements BookDao {
    public void save() {
        System.out.println("book dao save ... 2");
    }
}

业务层实现:

public class BookServlet extends BookServlet {
    //这个地方是写死了的,耦合度高
    private BookDao bookDao = new BookDaoImpl();

    public void save() {
        bookDao.save();
    }
}

在当前的代码结构中,主要存在以下问题:

  1. 高耦合度:业务层(如BookServlet)需要直接new出数据层(如BookDaoImpl)的具体实现对象。
  2. 难以维护:当数据层的实现类发生变更时(例如,从BookDaoImpl1切换到BookDaoImpl2),业务层的代码也必须随之修改。这会导致每次变更都需要重新编译、打包和部署。
  3. 缺乏灵活性:代码严重依赖于具体的实现类,而不是接口,使得程序难以扩展和测试。

所以现在的问题就是,业务层不想new对象,运行的时候又需要这个对象,该怎么办呢?

解决方案:Spring的核心思想

为了解决上述问题,Spring提出了IOC(控制反转)  和 DI(依赖注入)  的核心概念。

IOC、IOC容器、Bean、DI
  1. IOC (Inversion of Control)控制反转
  • 什么是控制反转?
    使用对象时,由主动在代码内部new产生对象,转变为由外部来提供对象。对象创建的控制权由程序代码转移到了外部容器(Spring IOC容器),这种思想称为控制反转(IOC)。
  • Spring与IOC的关系
    Spring技术对IOC思想进行了具体的实现,提供了一个容器,称为 IOC容器,它就是IOC思想中的那个“外部”。
  • IOC容器的作用
    IOC容器 负责对象的创建、初始化等一系列工作,是一个存放着众多Bean的大工厂。
    这些被IOC容器创建和管理的对象统称为 Bean
  1. DI (Dependency Injection)依赖注入
  • 什么是依赖注入?
    在IOC容器中,建立Bean与Bean之间依赖关系的整个过程,就称为依赖注入。(解决了问题:虽然IOC容器帮我们创建了所有对象,但对象与对象之间的依赖关系还未建立)
  • 如何工作?
    程序员只需通过配置(XML或注解)告诉IOC容器,哪些Bean之间需要建立依赖关系(例如业务层需要依赖数据层,Service就需要依赖Dao)。容器在初始化时,就会自动将被依赖的对象赋值给成员变量,从而完成绑定。
  • 最终目标是:充分解耦
    通过IOC容器管理所有Bean(IOC),并在容器内部将有依赖关系的Bean进行绑定(DI),使得我们在使用对象时,不仅能直接从容器中获取,而且获取到的Bean已经自动处理好了所有的依赖关系,程序员无需再关心复杂的对象创建和依赖组装过程。

IOC和DI入门案例

案例演示 Sprin实现IOC和DI的具体过程:

代码实现

逻辑步骤

  1. 创建一个标准、全新的 Java Maven 项目

    image.png
  2. 添加 Spring 依赖 (IOC 基础)
    在 pom.xml 中添加 Spring 上下文依赖(spring-context)和 JUnit 测试依赖(用于后续测试)

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.2.10.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
    
  3. 创建数据访问层(DAO)接口及其实现类

    public interface BookDao {
        public void save();
    }
    
    public class BookDaoImpl implements BookDao {
        public void save() {
            System.out.println("book dao save ...");
        }
    }
    
  4. 创建业务服务层(Service)接口

    // BookService.java
    public interface BookService {
        public void save();
    }
    
  5. 创建业务服务层(Service)接口的实现类 (IoC服务类)
    将依赖对象的创建和管理权从类内部转移到外部IOC容器

    1. 声明一个 需要的外部依赖(Dao层)接口 类型的属性,用于依赖注入

    2. 提供该属性的 setter 方法,这是 DI 依赖注入 的通道。

    • 在Bean初始化阶段,容器自动调用此方法注入依赖;方法参数 是DAO层接口的实现类实例,由IOC容器提供。

    • setter方法必须严格按照JavaBean的规范命名(即set + 首字母大写的属性名),Spring才能正确找到并调用它。

    1. 使用注入的外部依赖对象(DAO层) 调用 DAO 层的方法,以执行业务操作
    // BookServiceImpl.java
    public class BookServiceImpl implements BookService {
        //1. 依赖声明,使用接口类型而非具体实现类
        private BookDao bookDao;
    
        //2. Setter方法,依赖注入的通道
        public void setBookDao(BookDao bookDao) {
            this.bookDao = bookDao;
        }
    
        //3. 使用注入的依赖调用业务方法,执行业务操作
        public void save() {
            System.out.println("book service save ...");
            bookDao.save();  // 使用注入的依赖 bookDao 调用 DAO 层的方法
        }
    }
    
  6. 创建 Spring 配置文件:将对象创建权交给 Spring (IOC 配置);由Spring容器建立Bean与Bean之间的依赖关系 (DI 配置)

    1. 在 src/main/resources 目录下,右键 -> 新建 -> XML配置文件 -> Spring配置 ,创建 applicationContext.xml: image.png

    2. 添加 Spring beans 的命名空间和架构定义

    3. 配置 DAO 层和Service层的 bean(使用 <bean> 标签):

    • IOC 配置<bean>标签表示配置bean,以将 类的对象创建权 交给Spring容器:
      • id 属性表示给bean命名;
      • class 属性表示该bean的类型,指定将哪个类的对象创建权 交给Spring容器
    • DI 配置,使用 <property> 标签配置 Service层的 bean,以引用容器中其他bean:
      • name 属性表示属性名称(与setter方法名对应,遵循JavaBean规范);
      • ref 属性是该 bean引用容器中其他bean的id。
    <?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 -->
        <bean id="bookDao" class="com.example.dao.impl.BookDaoImpl"/>
        <bean id="userDao" class="com.itheima.dao.impl.UserDaoImpl"/>
        
        <!-- 配置业务层bean -->
        <bean id="bookService" class="com.example.service.impl.BookServiceImpl">
            <!-- 配置service与dao的依赖关系 -->
            <property name="bookDao" ref="bookDao"/>
        </bean>
    </beans>
    
  • 注意:bean定义时,id属性在同一个上下文中(配置文件)不能重复
  1. 创建应用程序主类,以从容器获取 Bean 并使用方法 (IOC使用方式)

    1. 在 main 方法中获取 Spring IOC 容器
    2. 使用 getBean() 方法从容器获取 Service Bean对象
      IOC 容器获取 Bean对象 的标准句式:
      [接口类型] [变量名] = ([接口类型]) [容器变量名].getBean("[Bean名称]");
    • [接口类型] : 要获取的 Bean 实现的接口类型
    • [变量名] : 接收 Bean 实例的变量名(通常使用接口名的小驼峰形式)
    • [容器变量名] : ApplicationContext 实例的变量名(通常为 context
    • [Bean名称] : 在 Spring 配置中定义的 Bean ID/名称
    1. 调用 Service 层的方法
    public class App {
        public static void main(String[] args) {
            // 1. 获取IOC容器
            ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    
            // 2. 获取bean对象
            BookService bookService = (BookService) context.getBean("bookService");
    
            // 3. 调用方法
            bookService.save();
        }
    }
    
  2. 运行结果
    运行程序后,控制台将输出:

    book service save ...
    book dao save ...
    

注入标签详解

<property>注入标签:用于setter注入
<constructor-arg>注入标签:用于构造器参数注入

主要属性

  • name 属性:指定当前正在配置的Bean对应的类中的属性名,表示将ref指定id的Bean对象依赖注入到其中。
    • Spring 会根据名称将首字母大写,前面加"set"来寻找对应的setter方法
    • name="bookDao" :表示要配置当前bean的哪个属性(对应setter方法名)
    • 例如:name="bookDao" → 寻找 setBookDao() 方法
  • ref 属性:指定要引用的另一个bean的id,Spring会在IOC容器中查找对应id的bean进行注入
    • ref="bookDao" :表示要引用IOC容器中哪个bean(对应bean的id)
    • 例如:ref="bookDao" → 引用id为"bookDao"的bean
image.png
  • 可选属性:除了使用 name 属性,在可能有多个参数的构造方法注入中,还可以使用 indextype 属性来指定参数:
    • index:参数的索引位置(可选,从0开始)
    • type:参数的类型(可选,用于消除歧义)
    <!-- 使用索引方式 -->
    <constructor-arg index="0" ref="bookDao"/>
    
    <!-- 使用类型方式 -->
    <constructor-arg type="com.itheima.dao.BookDao" ref="bookDao"/>
    

可选属性:除了使用 name 属性,在配置构造器注入 (<constructor-arg>)  时,还可以使用 index 或 type 属性来精确指定参数,这对于解决歧义至关重要:

  • index:参数的索引位置(可选,从0开始)。
    主要用于:当构造方法有多个相同类型的参数时,显式指定参数的索引位置,避免因配置顺序错误导致的注入问题。
  • type:参数的类型(可选,值为 全限定类名或基本类型关键字)。
    主要用于:①帮助 Spring 在多个重载的参数数量相同且类型不同的构造方法中,通过指定类型匹配对应的一个构造方法;②确保简单类型(如 intlongdouble)的值被准确转换,消除类型歧义。

注意Setter 注入 (<property>) 中 ,index 属性极少使用,因为每个属性独立配置。但 type 属性在注入简单数据类型时依然推荐使用,以明确指定目标类型。

实践建议

  • Setter 注入:优先使用 name 属性,清晰易懂
  • 构造器注入
    • 参数少时使用 name 属性
    • 参数多或相同类型时使用 index 属性
    • 需要消除类型歧义时使用 type 属性
  • 简单类型注入:使用 type 属性明确指定数据类型,避免自动类型转换的问题

注入多个依赖

  • 可以在一个 <bean> 内使用多个 <property>(或 <constructor-arg>)标签来注入多个依赖
  • <property>标签的顺序:顺序不影响注入结果,因为 Spring 会解析所有依赖后再进行注入
  • <constructor-arg>标签的顺序:如果未使用 nameindex 属性,那么 <constructor-arg> 标签的顺序必须与构造方法中参数的声明顺序严格一致,因为 Spring 默认按索引匹配。

必须提供 setter(或 构造)方法
每个需要注入的属性都必须在类中有对应的 setter(或 构造)方法

构造器紧耦合问题
构造器注入使用 name 属性时,当构造函数中方法的参数名发生变化后,配置文件中的name属性也需要跟着变。
解决:使用 indextype 属性替换 name 属性能一定程度上解决耦合问题。
局限:

  • 使用 type 属性时,如果构造方法参数中有类型相同的参数,这种方式就不能使用了
  • 使用 index 属性时,如果构造方法参数顺序发生变化后,这种方式又带来了新的耦合问题。

IOC相关内容

通过前面案例,学习了bean如何定义配置DI如何定义配置以及容器对象如何获取的内容,接下来把这三块内容展开进行详细的讲解,深入的学习下这三部分的内容。

Bean 基础配置

Bean 基本配置(id 与 class)

在 Spring 配置中,bean 的基本配置使用以下格式:

<bean id="唯一标识符" class="类的全限定名"/>
  • bean 标签:用于配置由 Spring 容器创建和管理的对象
  • id 属性:给 bean 起一个同一配置文件中唯一的标识名称,用于在容器中查找该 bean
  • class 属性:指定 bean 的全限定类名(必须是具体实现类,不能是接口)

Bean 的 name 属性(别名配置)

除了使用 id 属性,还可以使用 name 属性为 bean 配置一个或多个别名:

<bean id="bookService" name="service service4 bookEbi" class="com.example.service.impl.BookServiceImpl">
    <property name="bookDao" ref="bookDao"/>
</bean>

<bean id="bookDao" name="dao" class="com.example.dao.impl.BookDaoImpl"/>
  • name 属性:为 bean 指定一个或多个别名,多个别名可以使用逗号、分号或空格进行分隔
  • EBI:全称 Enterprise Business Interface,是企业业务接口的缩写

获取 Bean 对象的方式

可以通过 id 或 name 属性中的任意一个值来获取 bean 对象:

public class AppForName {
    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        
        // 以下三种方式都可以获取到同一个bean对象
        BookService service1 = (BookService) ctx.getBean("bookService"); // 使用id
        BookService service2 = (BookService) ctx.getBean("service");    // 使用name中的第一个别名
        BookService service3 = (BookService) ctx.getBean("service4");   // 使用name中的第二个别名
        
        service1.save();
    }
}

重要注意事项

  1. 引用的 bean 必须在容器中存在

    • 在使用 <property name="bookDao" ref="bookDao"/> 时,ref 属性指定的 bean 必须在容器中存在
    • 如果引用的 bean 不存在,Spring 容器启动时会抛出异常
  2. 获取 bean 时的异常处理

    • 无论通过 id 还是 name 获取 bean,如果指定的标识符在容器中不存在,将抛出 NoSuchBeanDefinitionException 异常
    • 这意味着在获取 bean 前需要确保配置正确,或者进行适当的异常处理

Bean 的作用范围配置

使用 scope 属性可以配置 bean 的作用范围

<bean id="bookDao" name="dao" class="com.example.dao.impl.BookDaoImpl" scope="prototype"/>
  • scope 属性:为 bean 设置作用范围,可选值包括:
    • singleton:单例模式(默认值),整个容器中只有一个实例(地址值相同)
    • prototype:非单例模式,每次请求都会创建一个新的实例(地址值不同)

验证 IOC 容器中对象是否为单例

通过获取同一个 bean 两次,比较两个对象的地址值来判断是否为单例:

public class AppForScope {
    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        
        BookDao bookDao1 = (BookDao) ctx.getBean("bookDao");
        BookDao bookDao2 = (BookDao) ctx.getBean("bookDao");
        
        System.out.println(bookDao1); // 打印对象地址
        System.out.println(bookDao2); // 打印对象地址
    }
}

验证结果: image.png

结论:默认情况下,两个对象打印的地址值相同,证明 Spring 创建的 bean 对象都是单例的。

Scope 使用的重要思考

  1. 为什么 Bean 默认为单例?
  • 性能优化:单例模式避免了对象的频繁创建与销毁,实现了对象的复用
  • 资源节约:减少了内存开销和垃圾回收压力
  • 适用场景:对于无状态的服务对象,单例模式是最佳选择
  1. Bean 在容器中是单例的,会不会产生线程安全问题?
  • 有状态对象:如果对象有成员变量用于存储数据,所有请求线程共享同一个 bean 实例,会存在线程安全问题
  • 无状态对象:如果对象没有成员变量存储数据(仅提供方法),方法中的局部变量在调用结束后会被销毁,不会存在线程安全问题
  1. 哪些 Bean 对象适合交给容器管理?
  • 表现层对象(Controller)
  • 业务层对象(Service)
  • 数据层对象(Dao/Repository)
  • 工具对象(Utils)
  1. 哪些 Bean 对象不适合交给容器管理?
  • 封装实例的域对象(如 Entity、DTO、VO 等)
  • 原因:这些对象通常包含状态信息,如果作为单例管理会引发线程安全问题

Bean 实例化

核心问题: 对象已经能交给Spring的IOC容器来创建了,但是容器是如何来创建对象的呢?

本节旨在研究 Bean的实例化过程,主要解决两部分内容:

  1. Bean是如何创建的
  2. 实例化Bean的三种方式:构造方法、静态工厂和实例工厂(FactoryBean)

基本认知: Bean本质上就是对象。对象在Java中通常通过new关键字调用构造方法创建,因此Spring创建Bean本质上也是通过构造方法完成的。我们将基于此来验证Spring中Bean的三种创建方式。

学习目标: 这些方式中,重点掌握构造方法和FactoryBean即可。

环境准备

建立项目结构的步骤 同前面 IOC和DI入门案例-代码实现 的都一致

构造方法实例化

这是Spring中最基本和最常用的Bean创建方式。

创建验证与原理测试

步骤 1: 准备需要被创建的类

创建接口BookDao及其实现类BookDaoImpl

// BookDao.java
public interface BookDao {
    public void save();
}
// BookDaoImpl.java
public class BookDaoImpl implements BookDao {
    // 提供一个无参构造函数(可选,Java默认提供)
    public BookDaoImpl() {
        System.out.println("BookDaoImpl constructor is running ....");
    }
    @Override
    public void save() {
        System.out.println("book dao save ...");
    }
}

步骤 2: 将类配置到Spring容器

applicationContext.xml中使用<bean>标签进行配置。

<?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="bookDao" class="com.itheima.dao.impl.BookDaoImpl"/>
    
</beans>

步骤 3: 编写运行程序

创建应用程序入口类,获取Spring容器(IOC容器)并从中获取配置好的Bean。

// AppForInstanceBook.java
public class AppForInstanceBook {
    public static void main(String[] args) {
        // 1. 加载配置文件,初始化IOC容器(ApplicationContext是容器接口)
        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        
        // 2. 从容器中根据id获取Bean实例
        BookDao bookDao = (BookDao) ctx.getBean("bookDao");
        
        // 3. 使用Bean
        bookDao.save();
    }
}

运行结果:

image.png

结论: 从输出结果可以看到,Spring容器在创建bookDao这个Bean时,自动调用了BookDaoImpl类的无参构造方法。这证实了Spring默认使用构造方法来实例化Bean。

测试 1: 将构造方法改为 private

修改BookDaoImpl类的构造方法访问权限为private

public class BookDaoImpl implements BookDao {
    // 私有构造方法
    private BookDaoImpl() {
        System.out.println("book dao constructor is running .... (Private)");
    }
    public void save() {
        System.out.println("book dao save ...");
    }
}

运行结果: 程序依然成功执行,输出相同的结果。

结论: Spring能够成功调用类的私有构造方法。这证明Spring底层并非简单地使用new关键字,而是利用了反射(Reflection) 机制来突破Java的访问权限限制,从而实例化对象。

测试 2: 构造方法中添加一个参数

修改BookDaoImpl类,提供一个需要int类型参数的构造方法,并不提供无参构造

public class BookDaoImpl implements BookDao {
    // 需要一个int参数的构造方法
    public BookDaoImpl(int i) {
        System.out.println("book dao constructor with param is running ....");
    }
    public void save() {
        System.out.println("book dao save ...");
    }
}

运行结果: 此时运行程序会抛出异常BeanCreationException,提示找不到合适的默认构造方法。

核心结论
  • Spring IoC容器实例化Bean的首选且最常用方式是通过构造方法
  • 其底层实现机制是反射。配置<bean>标签的class属性后,容器会使用反射加载指定的类,并默认调用该类的无参构造方法来创建对象实例
  • 即使该构造方法是private的,反射机制也能正常工作。
  • 如果类中没有显式提供任何构造方法,Java编译器会默认提供一个无参构造,Spring即可使用它
  • 如果类中显式定义了带参数的构造方法,就必须同时提供一个无参构造方法,否则Spring将无法直接通过此默认方式实例化该Bean,后续需要通过工厂方法或在配置中指定构造参数等其他方式来解决。

静态工厂实例化

普通静态工厂创建Bean

在了解Spring的静态工厂实例化之前,我们先回顾如何使用普通的静态工厂来创建对象

(1) 准备接口和实现类
创建OrderDao接口及其实现类OrderDaoImpl

// OrderDao.java
public interface OrderDao {
    public void save();
}
// OrderDaoImpl.java
public class OrderDaoImpl implements OrderDao {
    @Override
    public void save() {
        System.out.println("order dao save ...");
    }
}

(2) 创建静态工厂类
创建一个工厂类OrderDaoFactory,并提供静态方法getOrderDao()

// OrderDaoFactory.java
public class OrderDaoFactory {
    public static OrderDao getOrderDao(){
        return new OrderDaoImpl();
    }
}

(3) 编写运行类
编写AppForInstanceOrder类,通过静态工厂获取对象。

// AppForInstanceOrder.java
public class AppForInstanceOrder {
    public static void main(String[] args) {
        // 通过静态工厂创建对象
        OrderDao orderDao = OrderDaoFactory.getOrderDao();
        orderDao.save();
    }
}

(4) 运行结果

order dao save ...

现在的问题是:如何将这种通过工厂创建对象的方式交给Spring来管理?

Spring静态工厂创建Bean

将静态工厂创建对象的方式整合到Spring中的具体步骤如下:

(1) 修改工厂类
在工厂方法中添加必要的业务操作,展示静态工厂的独特优势。

// OrderDaoFactory.java
public class OrderDaoFactory {
    public static OrderDao getOrderDao(){
        System.out.println("factory setup...."); // 模拟必要的业务操作
        return new OrderDaoImpl();
    }
}

(2) 配置Spring配置文件
applicationContext.xml中添加Bean配置。

<bean id="orderDao" class="com.itheima.factory.OrderDaoFactory" factory-method="getOrderDao"/>
  • class: 指定工厂类的全限定名
  • factory-method: 指定工厂类中创建对象的静态方法名

配置关系图示:\ image.png

(3) 修改运行类
改为从Spring IOC容器中获取Bean。

// AppForInstanceOrder.java
public class AppForInstanceOrder {
    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        OrderDao orderDao = (OrderDao) ctx.getBean("orderDao");
        orderDao.save();
    }
}

(4) 运行结果

factory setup....
order dao save ...
静态工厂实例化的意义

可能会有人疑问:工厂类中最终还是通过new创建对象,与直接使用构造方法实例化相比似乎更复杂,这种方式的意义是什么?

关键优势在于:在工厂的静态方法中,除了创建对象,还可以执行必要的业务操作(如初始化配置、日志记录、权限检查等),这些操作在直接new对象时是无法实现的。

应用场景:这种方式主要用于兼容早期的一些老系统,在现代Spring开发中了解即可,实际应用中较少使用

实例工厂实例化与FactoryBean

普通实例工厂创建Bean

(1) 准备接口和实现类
创建UserDao接口及其实现类UserDaoImpl

// UserDao.java
public interface UserDao {
    public void save();
}
// UserDaoImpl.java
public class UserDaoImpl implements UserDao {
    @Override
    public void save() {
        System.out.println("user dao save ...");
    }
}

(2) 创建实例工厂类
创建一个工厂类UserDaoFactory,提供一个普通方法(注意:与静态工厂不同,这里不是静态方法)。

// UserDaoFactory.java
public class UserDaoFactory {
    public UserDao getUserDao(){
        return new UserDaoImpl();
    }
}

(3) 编写运行类测试
编写AppForInstanceUser类,通过实例工厂获取对象。

// AppForInstanceUser.java
public class AppForInstanceUser {
    public static void main(String[] args) {
        // 创建实例工厂对象
        UserDaoFactory userDaoFactory = new UserDaoFactory();
        // 通过实例工厂对象创建UserDao
        UserDao userDao = userDaoFactory.getUserDao();
        userDao.save();
    }
}

(4) 运行结果

user dao save ...

现在的问题是:如何将这种实例工厂创建对象的方式交给Spring来管理?

Spring实例工厂创建Bean

将实例工厂创建对象的方式整合到Spring中的具体配置:

(1) 配置Spring配置文件
applicationContext.xml中添加以下配置:

<!-- 1. 先创建工厂实例 -->
<bean id="userFactory" class="com.itheima.factory.UserDaoFactory"/>
<!-- 2. 通过工厂实例创建目标对象 -->
<bean id="userDao" factory-method="getUserDao" factory-bean="userFactory"/>

配置解析:

  • 第一行:创建工厂实例,id为"userFactory"
  • 第二行:通过工厂实例创建目标Bean
    • factory-bean="userFactory" : 引用容器中已定义的id为"userFactory"的Bean作为工厂实例
    • factory-method="getUserDao" : 指定在"userFactory"这个工厂实例上要调用的方法名为"getUserDao"

配置关系图示:\ image.png

(2) 修改运行类
改为从Spring IOC容器中获取Bean。

// AppForInstanceUser.java
public class AppForInstanceUser {
    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        UserDao userDao = (UserDao) ctx.getBean("userDao");
        userDao.save();
    }
}

(3) 运行结果

user dao save ...

实例工厂实例化的配置相对复杂,为了简化这种配置方式,Spring提供了FactoryBean接口。

FactoryBean简化配置

FactoryBean是Spring提供的一种更简洁的实现工厂模式的方式。

(1) 创建FactoryBean实现类
创建一个实现FactoryBean接口的类UserDaoFactoryBean

// UserDaoFactoryBean.java
public class UserDaoFactoryBean implements FactoryBean<UserDao> {
    // 代替原始实例工厂中创建对象的方法
    public UserDao getObject() throws Exception {
        return new UserDaoImpl();
    }
    
    // 返回所创建类的Class对象
    public Class<?> getObjectType() {
        return UserDao.class;
    }
    
    // 可选:设置是否为单例,默认true
    public boolean isSingleton() {
        return true; // 默认单例,可根据需要返回false
    }
}

(2) 简化Spring配置
在配置文件中只需配置FactoryBean本身。

<bean id="userDao" class="com.itheima.factory.UserDaoFactoryBean"/>

(3) 运行测试
运行类不需要任何修改,直接运行即可得到相同结果。

FactoryBean接口的三个方法:

  • T getObject():必须被重写,在此方法中实现创建Bean实例的逻辑并返回该实例。
  • Class<?> getObjectType():必须被重写,返回由getObject()方法创建的Bean的类型(Class对象)。
  • default boolean isSingleton():可选重写,返回Bean是否为单例模式,默认true(true,单例模式;false,非单例模式)

FactoryBean方式在Spring整合其他框架时会被广泛使用,是需要重点理解和掌握的内容。一般情况下,我们保持默认的单例模式即可,无需重写isSingleton()方法。

Bean的生命周期

  • 生命周期:从创建到消亡的完整过程。
  • Bean生命周期:Bean对象从创建到销毁的整体过程。

通过理解和控制Bean的生命周期,我们可以在Bean创建后到销毁前以及创建和销毁的关键时刻执行必要的操作,如资源初始化、连接池管理、缓存清理等,从而提高应用程序的健壮性和性能。

环境准备

建立项目结构 和 初始代码 同前面 IOC和DI入门案例-代码实现 的都一致

生命周期控制(初始化和销毁方法)

概述

在Spring框架中,我们可以对Bean的生命周期进行干预,特别是在两个关键阶段:

  • Bean创建之后:用于执行初始化操作,如初始化资源
  • Bean销毁之前:用于执行清理操作,如释放资源

Spring提供了两种方式来实现生命周期控制:

  1. 通过XML配置文件指定方法
  2. 通过实现Spring接口
配置方式实现

实现步骤

步骤1:添加初始化和销毁方法

在Bean的实现类中创建两个方法,方法名可任意定义。

public class BookDaoImpl implements BookDao {
    public void save() {
        System.out.println("book dao save ...");
    }
    
    // Bean初始化时执行的操作
    public void init() {
        System.out.println("init...");
    }
    
    // Bean销毁前执行的操作
    public void destory() {
        System.out.println("destory...");
    }
}

步骤2:配置生命周期方法

在Spring配置文件中,为需要配置生命周期的 bean 使用 init-method(指定初始化方法名)destroy-method(指定销毁方法名) 属性指定相应的方法。

<bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl" 
      init-method="init" 
      destroy-method="destory"/>

步骤3:运行测试

创建测试类验证生命周期方法(注意添加容器关闭的操作):

public class AppForLifeCycle {
    public static void main(String[] args) {
        // 需要使用ClassPathXmlApplicationContext才能调用close方法
        ClassPathXmlApplicationContext ctx = 
            new ClassPathXmlApplicationContext("applicationContext.xml");
        
        BookDao bookDao = (BookDao) ctx.getBean("bookDao");
        bookDao.save();
        
        // 直接调用close()方法关闭容器,触发destroy方法
        ctx.close();
    }
}

运行AppForLifeCycle打印结果为:

image.png

从结果中可以看出,init方法执行了,但是destroy方法却未执行,这是因为容器需要手动关闭。

容器关闭的两种方式

需要手动关闭容器的原因
Spring 的 ApplicationContext 接口实现(如 ClassPathXmlApplicationContext)负责管理 Bean 的生命周期。main 方法执行完毕后 JVM 立即退出,容器没有机会执行其正常的关闭流程,导致所有配置了 销毁方法destroy-method 的 Bean 都无法执行资源清理操作。

解决方法:由于ApplicationContext接口没有提供关闭方法,需要先创建其实现类ClassPathXmlApplicationContext实例,并提供两种关闭方式:

  • 创建其实现类ClassPathXmlApplicationContext实例ClassPathXmlApplicationContext bean对象名 = new ClassPathXmlApplicationContext("applicationContext.xml");

方式一:直接关闭容器:bean对象名.close();

// 创建ClassPathXmlApplicationContext实例
ClassPathXmlApplicationContext ctx = 
    new ClassPathXmlApplicationContext("applicationContext.xml");

// 执行业务逻辑...

// 直接调用close()方法关闭容器
ctx.close();

方式二:注册钩子关闭容器:bean对象名.registerShutdownHook();

// 创建ClassPathXmlApplicationContext实例
ClassPathXmlApplicationContext ctx = 
    new ClassPathXmlApplicationContext("applicationContext.xml");

// 执行业务逻辑...

// 注册JVM关闭钩子,在JVM退出前自动关闭容器
ctx.registerShutdownHook();

两种关闭方式的对比

方式执行时机适用场景
close()执行到close()方法时立即关闭容器需要精确控制容器关闭时机
registerShutdownHook()JVM退出前自动调用关闭希望自动清理资源,无需手动控制

注意prototype 作用域的 Bean,其销毁方法需要由客户端自行管理,Spring 容器在关闭时不会调用它们的 destroy-method

接口方式实现

Spring提供了两个接口来实现生命周期控制,无需XML配置。

实现方式

// 1.导入必要的Spring接口
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;

// 2.业务接口实现类,同时实现Spring生命周期接口
public class BookServiceImpl implements BookService, 
        InitializingBean,  // 初始化接口,用于Bean初始化后的回调
        DisposableBean {   // 销毁接口,用于Bean销毁前的回调
    
    private BookDao bookDao;
    
    public void setBookDao(BookDao bookDao) {  // Setter方法用于依赖注入
        System.out.println("执行setBookDao方法: 依赖注入完成");
        this.bookDao = bookDao;
    }
    
    public void save() {  // 业务方法
        System.out.println("执行save方法: 业务逻辑处理");
        bookDao.save(); 
    }
    
    // 3.实现DisposableBean接口的destroy方法
    // 这个方法会在Bean销毁前被Spring容器自动调用
    public void destroy() throws Exception {
        System.out.println("执行destroy方法: 释放资源、关闭连接等清理操作");
    }
    
    // 4.实现InitializingBean接口的afterPropertiesSet方法
    // 这个方法会在所有属性设置完成后被Spring容器自动调用
    public void afterPropertiesSet() throws Exception {
        System.out.println("执行afterPropertiesSet方法: 初始化资源、加载配置等准备操作");
    }
}

注意:初始化方法afterPropertiesSet(),翻译过来为属性设置之后。 初始化方法会在类中属性设置完毕之后执行

总结对比

两种实现方式对比

特性配置方式接口方式
耦合度低(与Spring解耦)高(需要实现Spring接口)
配置量需要在XML中配置无需XML配置
灵活性方法名可任意定义必须使用特定方法名

推荐使用配置方式,降低代码与Spring框架的耦合度

生命周期方法的执行顺序

完整示例输出结果:

set method executed
service init
book service save ...
book dao save ...
service destroy
destory...

此输出结果验证了生命周期方法的执行顺序:

  1. 属性注入(set方法)
  2. 初始化方法(afterPropertiesSet)
  3. 业务方法执行
  4. 销毁方法(destroy)

DI相关内容

依赖注入(Dependency Injection,DI)描述了在 Spring 容器中建立 bean 与 bean 之间依赖关系的过程。

Spring 主要提供了两种注入方式:

  • Setter 注入
  • 构造器注入

这两种方式均可用于注入以下类型:

  • 引用类型(对象依赖)
  • 简单类型(基本数据类型与 String)

Setter 注入

Setter 注入是通过调用类的 setter 方法来实现依赖注入的方式。

环境准备

建立项目结构 和 初始代码 同前面 IOC和DI入门案例-代码实现 的基本一致。

额外添加一个UserDao接口及其实现类:

// UserDao.java
public interface UserDao {
    void save();
}

// UserDaoImpl.java
public class UserDaoImpl implements UserDao {
    public void save() {
        System.out.println("user dao save ...");
    }
}

引用类型注入(ref 属性)

前面 IOC和DI入门案例 使用的注入方式就是 Setter注入 引用类型(对象依赖)

多个引用类型注入

与下面的 多个简单数据类型注入(value 属性) 中的方式同理,只是类型不同:多个 引用类型的setter方法、多个 引用类型的<property>标签。

多个简单数据类型注入(value 属性)

Setter 注入不仅可以用于引用类型,也可以用于简单数据类型(基本数据类型和 String 类型)。

  1. 声明属性并提供 Setter 方法

在目标类中声明简单数据类型的属性,并提供对应的 setter 方法:

public class BookDaoImpl implements BookDao {
    private String databaseName;
    private int connectionNum;

    // 为简单数据类型属性提供 setter 方法
    public void setConnectionNum(int connectionNum) {
        this.connectionNum = connectionNum;
    }

    public void setDatabaseName(String databaseName) {
        this.databaseName = databaseName;
    }

    @Override
    public void save() {
        System.out.println("book dao save ..." + databaseName + ", " + connectionNum);
    }
}
  1. 配置文件中的注入配置

在 Spring 配置文件中使用 property 标签的 value 属性注入简单数据类型:

<?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="bookDao" class="com.itheima.dao.impl.BookDaoImpl">
        <!-- 注入简单数据类型使用 value 属性 -->
        <property name="databaseName" value="mysql"/>
        <property name="connectionNum" value="10"/>
    </bean>
    
    <bean id="userDao" class="com.itheima.dao.impl.UserDaoImpl"/>
    
    <bean id="bookService" class="com.itheima.service.impl.BookServiceImpl">
        <!-- 注入引用数据类型使用 ref 属性 -->
        <property name="bookDao" ref="bookDao"/>
        <property name="userDao" ref="userDao"/>
    </bean>
</beans>
  1. 运行测试

创建测试类验证:

public class AppForDISet {
    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        BookService bookService = (BookService) ctx.getBean("bookService");
        bookService.save();
    }
}

运行AppForDISet类,查看结果,说明userDao已经成功注入:

image.png

类型转换
Spring 会对value属性值自动进行类型转换,但必须确保值的格式正确

  • ✅ 正确:<property name="connectionNum" value="10"/>
  • ❌ 报错:<property name="connectionNum" value="abc"/>(无法将"abc"转换为 int)

构造器注入

构造器注入 使用 <constructor-arg> 标签配置依赖。

<constructor-arg> 标签的name 属性必须与构造方法参数名保持一致。

当类中存在多个重载的构造方法时,Spring 会优先尝试匹配参数数量最多且所有参数都能在IOC容器中找到对应bean依赖的构造方法。

环境准备

建立项目结构 和 初始代码 同前面 IOC和DI入门案例-代码实现 的基本一致。

额外添加一个UserDao接口及其实现类:

// UserDao.java
public interface UserDao {
    void save();
}

// UserDaoImpl.java
public class UserDaoImpl implements UserDao {
    public void save() {
        System.out.println("user dao save ...");
    }
}

引用类型注入

  1. 将 Setter方法 替换为 构造方法 以注入

BookServiceImpl 类中删除 bookDao 的 setter 方法,并添加带有 bookDao 参数的构造方法:

public class BookServiceImpl implements BookService {
    private BookDao bookDao;
    
    // 构造方法注入
    public BookServiceImpl(BookDao bookDao) {
        this.bookDao = bookDao;
    }
    
    @Override
    public void save() {
        System.out.println("book service save ...");
        bookDao.save();
    }
}
  1. 修改配置文件使用构造器注入

applicationContext.xml 中使用 <constructor-arg> 标签配置构造器注入:

<?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="bookDao" class="com.itheima.dao.impl.BookDaoImpl"/>
    <bean id="bookService" class="com.itheima.service.impl.BookServiceImpl">
        <!-- 使用 constructor-arg 标签进行构造器注入 -->
        <constructor-arg name="bookDao" ref="bookDao"/>
    </bean>
</beans>

多个引用类型注入

与下面的 多个简单数据类型注入(value 属性) 中的方式同理,只是类型不同:多个 引用类型参数的构造方法、多个 引用类型的<constructor-arg>标签。

多个简单数据类型注入

  1. 添加多个简单属性并提供构造方法

修改BookDaoImpl类,添加多个参数的构造方法

public class BookDaoImpl implements BookDao {
    private String databaseName;
    private int connectionNum;

    public BookDaoImpl(String databaseName, int connectionNum) {
        this.databaseName = databaseName;
        this.connectionNum = connectionNum;
    }

    public void save() {
        System.out.println("book dao save ..."+databaseName+","+connectionNum);
    }
}
  1. 配置完成多个属性构造器注入

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

    <bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl">
        <constructor-arg name="databaseName" value="mysql"/>
        <constructor-arg name="connectionNum" value="666"/>
    </bean>
    <bean id="userDao" class="com.itheima.dao.impl.UserDaoImpl"/>
    <bean id="bookService" class="com.itheima.service.impl.BookServiceImpl">
        <constructor-arg name="bookDao" ref="bookDao"/>
        <constructor-arg name="userDao" ref="userDao"/>
    </bean>
</beans>

注入方式选择

根据Spring官方和实际开发经验,我们可以总结出以下准则:

  1. Spring框架倡导使用构造器注入,因为这样可以保证依赖不可变(immutable)并确保完全初始化的对象。
  2. 强制依赖:使用构造器注入。这样能保证对象在创建时就具有所有必需的依赖,避免null对象出现。
    强制依赖指的是对象在创建时必须提供的参数。
  3. 可选依赖:使用Setter注入。这样可以在对象创建后动态地改变依赖,灵活性强。(可选依赖指的是对象在创建时不一定需要,可以在后期设置的参数)
  4. 如果有必要,可以同时使用两种注入方式 :用构造器注入来完成强制依赖的注入,用Setter注入来完成可选依赖的注入。
  5. 实际开发中还要根据实际情况分析。如果受控对象(即我们要注入的Bean)没有提供setter方法,那么就必须使用构造器注入。
  6. 自己开发的模块,如果团队没有强制要求,可以根据需要选择。但推荐使用构造器注入来保证依赖的不可变性和避免null指针。

自动配置

概述

依赖自动装配 是指 Spring IoC 容器根据 bean 所依赖的资源,在容器中自动查找并注入到 bean 中的过程。

减少了手动配置的工作量,方便维护XML配置文件,提高了开发效率。

自动装配方式

Spring 提供了以下几种自动装配方式:

按类型自动装配 (byType)

  • 工作原理:Spring 容器会查找与需要注入的属性类型相匹配的 bean
  • 适用场景:当容器中只有一个该类型的 bean 时
  • 配置方式autowire="byType"
  • 特点
    • 最常用的自动装配方式
    • 如果有多个相同类型的 bean,会抛出异常NoUniqueBeanDefinitionException
    • 如果没有找到匹配类型的bean,不会报错,而是保留属性为null
  • 运行逻辑流程
    1. 扫描bean的所有setter方法
    2. 获取setter方法的参数类型
    3. 在容器中查找匹配该类型的bean
    4. 如果找到唯一匹配则注入,多个匹配报错

按名称自动装配 (byName)

  • 工作原理:Spring 容器会查找与需要注入的属性名称相匹配的 bean ID
  • 适用场景:当需要精确指定要注入的 bean 时
  • 配置方式autowire="byName"
  • 特点
    • 通过属性名与 bean ID 的匹配进行注入
    • 不会出现多个匹配的情况
  • 运行逻辑流程
    1. 扫描bean的所有setter方法
    2. 从setter方法名提取属性名(去掉"set"前缀)
    3. 在容器中查找bean id匹配属性名的bean
    4. 找到则注入,找不到则跳过

按构造方法自动装配 (constructor)

  • 工作原理:类似于 byType,但是应用于构造器参数
  • 适用场景:构造器注入的自动配置
  • 配置方式autowire="constructor"
  • 特点
    • 根据构造方法的参数类型进行自动装配
    • 如果找不到匹配的 bean,会抛出异常
  • 运行逻辑流程
    1. 查找bean的所有构造方法
    2. 按参数数量降序排序(Spring默认策略)
    3. 遍历排序后的构造方法,从参数最多的开始尝试
    4. 对每个构造参数按类型在容器中查找匹配bean
      • 如果所有参数都能找到唯一匹配的bean → 使用该构造方法
      • 如果有参数找不到匹配或多个匹配 → 立即放弃该构造方法
    5. 继续尝试参数次多的构造方法,重复步骤4
    6. 如果所有构造方法都无法满足 → 抛出BeanCreationException

不启用自动装配 (no)

  • 说明:默认值,不进行自动装配,需要手动配置所有依赖
  • 配置方式autowire="no" 或省略该属性

配置示例 与 autowire属性位置

自动装配的配置方式 autowire属性 写在 接受自动装配依赖 的bean标签中。

一般配置方式

<!-- 被依赖的bean(提供者) -->
<!-- 按类型自动装配 -->
<bean id="bookService" class="com.example.BookServiceImpl" autowire="byType"/>
<!-- 按名称自动装配 -->
<bean id="bookService" class="com.example.BookServiceImpl" autowire="byName"/>
<!-- 按构造方法自动装配 -->
<bean id="bookService" class="com.example.BookServiceImpl" autowire="constructor"/>
<!-- 不启用自动装配(默认) -->
<bean id="bookService" class="com.example.BookServiceImpl" autowire="no"/>

<!-- 接受自动装配的bean(消费者)- autowire写在这里 -->
<bean id="bookService" class="com.example.BookServiceImpl" autowire="byType">
    <!-- 可以同时配置简单类型属性 -->
    <property name="timeout" value="5000"/>
</bean>

全局默认自动装配(default-autowire)

<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"
       default-autowire="byType"> <!-- 设置全局默认自动装配方式,byType可换其他自动装配方式值 -->
    
    <!-- 所有bean默认使用byType自动装配 -->
    <bean id="bookService" class="com.example.BookServiceImpl"/>
    <bean id="userService" class="com.example.UserServiceImpl"/>
    
    <!-- 覆盖全局设置:不使用自动装配 -->
    <bean id="specialBean" class="com.example.SpecialService" autowire="no"/>

    <!-- 覆盖全局设置:使用不同的自动装配方式 -->
    <bean id="paymentService" class="com.example.PaymentServiceImpl" autowire="byName"/>
</beans>

与其他配置方式的结合

自动装配可以与其他配置方式结合使用:

下面的代码示例展示了一些良好实践

  • autowire="byType" :自动处理复杂的对象依赖关系
  • <property> 手动配置:明确设置简单的配置参数
  • 同时使用两种配置方式,手动配置的优先级更高,会覆盖自动装配对同一属性的设置。

示例:

BookServiceImpl 类的结构如下:

public class BookServiceImpl {
    // 引用类型依赖 - 通过 autowire="byType" 自动注入
    private BookRepository bookRepository;      // 自动注入
    
    // 简单类型配置 - 通过 <property> 手动配置
    private boolean cacheEnabled;              // 手动配置
    private int timeout;                       // 手动配置
    
    // 演示属性:会被自动装配和手动配置两种方式设置
    private BookRepository backupRepository;   // 演示覆盖特性
    
    // setter 方法...
    public void setBackupRepository(BookRepository backupRepository) {
        this.backupRepository = backupRepository;
    }
    
    // ... 其他setter方法
}

xml配置

<!-- 自动装配的依赖bean -->
<bean id="bookRepository" class="com.example.JdbcBookRepository"/>
<bean id="backupRepository" class="com.example.BackupBookRepository"/> <!-- 自动装配会注入这个 -->

<!-- 主要服务bean - 混合配置 -->
<bean id="bookService" class="com.example.BookServiceImpl" autowire="byType">
    <!-- 配置简单类型参数 -->
    <property name="cacheEnabled" value="true"/>
    <property name="timeout" value="5000"/>
    
    <!-- 手动配置会覆盖自动装配 -->
    <property name="backupRepository" ref="bookRepository"/> <!-- 最终使用这个,覆盖自动装配的backupRepository -->
</bean>

在实际项目中,服务类通常需要:

  • 外部依赖(其他服务、仓库、数据源等)- 适合自动装配
  • 配置参数(超时时间、特性开关、环境标识等)- 适合手动配置

实践建议

推荐使用场景

  1. 中小型项目:使用 byType 自动装配提高开发效率
  2. 原型开发:快速搭建和验证业务逻辑
  3. 测试环境:简化测试配置

不推荐使用场景

  1. 大型复杂系统:依赖关系复杂,需要显式配置保证清晰性
  2. 多模块项目:不同模块可能有相同类型的 bean
  3. 需要精确控制的场景:某些依赖需要特殊处理或包装

注意事项

注意:

  1. 自动装配用于引用类型依赖注入,不能对简单类型进行操作
  2. 自动装配无法处理集合类型的依赖(List、Map、Set等)
  3. 实际开发中优先使用按类型装配,使用按类型装配时(byType)必须保障容器中相同类型的bean唯一(对于相同类型的多个bean,需要配合@Primary@Qualifier或显式配置使用)
  4. 使用按名称装配时(byName)必须保障容器中具有指定名称的bean,因变量名与配置耦合,不推荐使用
  5. 显式配置(<property>/<constructor-arg>)的优先级高于自动装配,同时出现时自动装配配置失效
  6. 缺点:自动装配会降低配置的显式性,影响代码可读性;依赖关系不明确时,调试会更加困难

集合注入

概述

在 Spring 框架中,集合注入是指将数组、List、Set、Map、Properties 等集合类型作为依赖注入到 Bean 中的过程。集合中可以包含简单数据类型(String、Integer等)或引用数据类型(其他 Bean)。

集合类型分类

集合类型特点适用场景
数组固定大小,有序,可重复需要固定数量元素的场景
List动态大小,有序,可重复需要有序且允许重复的集合
Set动态大小,无序,不可重复需要元素唯一性的场景
Map键值对集合需要通过键快速查找值的场景
Properties字符串键值对,继承自Hashtable配置参数、属性文件

环境准备

建立项目结构 和 初始代码 同前面 IOC和DI入门案例-代码实现 的基本一致。

核心类实现

BookDao.java

public interface BookDao {
    public void save();
}

BookDaoImpl.java

public class BookDaoImpl implements BookDao {
    
    private int[] array;
    private List<String> list;
    private Set<String> set;
    private Map<String, String> map;
    private Properties properties;

    public void save() {
        System.out.println("book dao save ...");
        System.out.println("遍历数组:" + Arrays.toString(array));
        System.out.println("遍历List:" + list);
        System.out.println("遍历Set:" + set);
        System.out.println("遍历Map:" + map);
        System.out.println("遍历Properties:" + properties);
    }
    
    // Setter方法
    public void setArray(int[] array) {
        this.array = array;
    }
    
    public void setList(List<String> list) {
        this.list = list;
    }
    
    public void setSet(Set<String> set) {
        this.set = set;
    }
    
    public void setMap(Map<String, String> map) {
        this.map = map;
    }
    
    public void setProperties(Properties properties) {
        this.properties = properties;
    }
}

测试类

AppForDICollection.java

public class AppForDICollection {
    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        BookDao bookDao = (BookDao) ctx.getBean("bookDao");
        bookDao.save();
    }
}

集合注入配置详解

基础配置文件
<?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="bookDao" class="com.itheima.dao.impl.BookDaoImpl">
        <!-- 各种集合注入配置将在这里添加 -->
    </bean>
</beans>
各种简单数据类型注入示例

数组类型注入

<property name="array">
    <array>
        <value>100</value>
        <value>200</value>
        <value>300</value>
    </array>
</property>

说明: 使用 <array> 标签定义数组,<value> 标签定义数组元素值。

List类型注入

<property name="list">
    <list>
        <value>itcast</value>
        <value>itheima</value>
        <value>boxuegu</value>
        <value>chuanzhihui</value>
    </list>
</property>

说明:
使用 <list> 标签定义List集合,元素有序且可重复。

标签混用特性

  • List底层通过数组实现,所以 <list><array> 标签可以混用
  • 强烈不推荐混用,为了语义清晰应该按实际类型使用对应标签

Set类型注入

<property name="set">
    <set>
        <value>itcast</value>
        <value>itheima</value>
        <value>boxuegu</value>
        <value>boxuegu</value>  <!-- 重复值会被自动去重 -->
    </set>
</property>

说明: 使用 <set> 标签定义Set集合,元素保持元素插入顺序且自动去重。

Map类型注入

<property name="map">
    <map>
        <entry key="country" value="china"/>
        <entry key="province" value="henan"/>
        <entry key="city" value="kaifeng"/>
    </map>
</property>

说明: 使用 <map> 标签定义Map集合,<entry> 标签定义键值对。

Properties类型注入

<property name="properties">
    <props>
        <prop key="country">china</prop>
        <prop key="province">henan</prop>
        <prop key="city">kaifeng</prop>
    </props>
</property>

说明: Properties的键和值都必须是String类型,使用 <props> 标签定义Properties,<prop> 标签定义属性。

引用类型注入示例

集合中注入引用类型时,将 <value> 标签改为 <ref> 标签:

<property name="serviceList">
    <list>
        <ref bean="userService"/>
        <ref bean="orderService"/>
    </list>
</property>

示例:

<!-- 需要定义被引用的bean -->
<bean id="userService" class="com.example.UserServiceImpl"/>
<bean id="orderService" class="com.example.OrderServiceImpl"/>

<bean id="bookService" class="com.example.BookServiceImpl">
    <property name="serviceList">
        <list>
            <ref bean="userService"/>    <!-- 引用已定义的bean -->
            <ref bean="orderService"/>   <!-- 引用已定义的bean -->
            <!-- 也可以内联定义bean -->
            <bean class="com.example.SpecialServiceImpl">
                <property name="timeout" value="5000"/>
            </bean>
        </list>
    </property>
</bean>
集合数据配置位置

对于集合注入,被注入的集合数据 直接配置在 接受依赖的bean的property标签内部,作为该bean配置的一部分。只有当集合元素本身是其他bean引用时,才需要先定义那些被引用的bean。

特殊情况:集合中包含引用类型
当集合中需要包含其他bean引用时,配置方式稍有不同:

<!-- 先定义被引用的bean -->
<bean id="service1" class="com.example.ServiceImpl1"/>
<bean id="service2" class="com.example.ServiceImpl2"/>

<!-- 在集合中使用ref引用外部bean -->
<bean id="bookService" class="com.example.BookServiceImpl">
    <property name="serviceList">
        <list>
            <ref bean="service1"/>  <!-- 引用外部bean -->
            <ref bean="service2"/>  <!-- 引用外部bean -->
        </list>
    </property>
</bean>

完整配置示例

<?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="bookDao" class="com.itheima.dao.impl.BookDaoImpl">
        <!-- 数组注入 -->
        <property name="array">
            <array>
                <value>100</value>
                <value>200</value>
                <value>300</value>
            </array>
        </property>

        <!-- List注入 -->
        <property name="list">
            <list>
                <value>itcast</value>
                <value>itheima</value>
                <value>boxuegu</value>
                <value>chuanzhihui</value>
            </list>
        </property>

        <!-- Set注入 -->
        <property name="set">
            <set>
                <value>itcast</value>
                <value>itheima</value>
                <value>boxuegu</value>
                <value>boxuegu</value>
            </set>
        </property>

        <!-- Map注入 -->
        <property name="map">
            <map>
                <entry key="country" value="china"/>
                <entry key="province" value="henan"/>
                <entry key="city" value="kaifeng"/>
            </map>
        </property>

        <!-- Properties注入 -->
        <property name="properties">
            <props>
                <prop key="country">china</prop>
                <prop key="province">henan</prop>
                <prop key="city">kaifeng</prop>
            </props>
        </property>
    </bean>
</beans>

运行结果

运行 AppForDICollection 类,控制台输出如下:

book dao save ...
遍历数组:[100, 200, 300]
遍历List:[itcast, itheima, boxuegu, chuanzhihui]
遍历Set:[itcast, itheima, boxuegu]  <!-- 注意:重复的boxuegu被去重 -->
遍历Map:{country=china, province=henan, city=kaifeng}
遍历Properties:{country=china, province=henan, city=kaifeng}

重要说明

注入方式

  • setter注入:使用 <property> 标签
  • 构造器注入:使用 <constructor-arg> 标签,内部同样可以使用集合标签
  • 一个<property><constructor-arg>标签内,只能包含一种集合类型标签来定义该属性或构造器参数的值(例外:集合内嵌套集合是允许的)

注意事项

  1. 类型匹配:确保集合的泛型类型与注入值类型一致
  2. Setter方法:必须提供对应的setter方法
  3. 集合去重:Set类型会自动去除重复元素
  4. 顺序保持:List保持元素顺序,Set不保证顺序