目录

Spring-框架学习

目录

/weixin_miniapp.png

Spring 框架学习

技术体系结构

总体技术体系

  • 单一架构

    一个项目,一个工程,导出为一个 war 包,在一个 Tomcat 上运行,也叫 all in one。

    https://i-blog.csdnimg.cn/direct/ce01620aa1c7458ea35b8262bbb551b9.png#pic_center

    单一架构,项目主要应用技术框架为:Spring、SpringMVC 、Mybatis。

  • 分布式架构

    一个项目(对应 IDEA 中的一个 project),拆分成很多个模块,每个模块是一个 IDEA 中的一个 module。每一个工程都是运行在自己的 Tomcat 上。模块之间可以互相调用。每一个模块内部可以看成是一个单一架构的应用。

    https://i-blog.csdnimg.cn/direct/229c76f968f34a89805c5209614f6dae.png#pic_center

    分布式架构,项目主要应用技术框架:SpringBoot(SSM 的简化)、SpringCloud、中间件等。

框架的概念与理解

框架(Framework)是一个集成了基本结构、规范、设计模式、编程语言和程序库等基础组件的软件系统,它可以用来构建更高级别的应用程序。框架的设计和实现旨在解决特定领域中的常见问题,帮助开发人员更高效、更稳定地实现软件开发目标。

框架的优点包括以下几点:

  • 提高开发效率 :框架提供了许多预先设计好了的组件和工具,能够帮助开发人员快速进行开发。相较于传统手写代码,在框架提供的规范化环境中,开发者可以更快地实现项目的各种要求。
  • 降低开发成本 :框架的提供标准化的编程语言、数据操作等代码片段,避免了重复开发的问题,降低了开发成本,提供深度优化的系统,降低了维护成本,增强了系统的可靠性。
  • 提高应用程序的稳定性 :框架通常经过了很长时间的开发和测试,其中的许多组件、代码片段和设计模式都得到了验证。重复利用这些组件有助于减少 bug 的出现,从而提高了应用程序的稳定性。
  • 提供标准化的解决方案 :框架通常是针对某个特定领域的,通过提供标准化的解决方案,可以为开发人员提供一种共同的语言和思想基础,有助于更好地沟通和协作。

框架的缺点包括以下几个方面:

  • 学习成本高 :框架通常具有特定的语言和编程范式。对于开发人员而言,需要花费时间学习其背后的架构、模式和逻辑,这对于新手而言可能会耗费较长时间。
  • 可能存在局限性 :虽然框架提高了开发效率并可以帮助开发人员解决常见问题,但是在某些情况下,特定的应用需求可能超出框架的范围,从而导致应用程序无法满足要求。开发人员可能需要更多的控制权和自由度,同时需要在框架和应用程序之间进行权衡取舍。
  • 版本变更和兼容性问题 :框架的版本发布和迭代通常会导致代码库的大规模变更,进而导致应用程序出现兼容性问题和漏洞。当框架变更时,需要考虑框架是否向下兼容,以及如何进行适当的测试、迁移和升级。
  • 架构风险 :框架涉及到很多抽象和概念,如果开发者没有足够的理解和掌握其架构,可能会导致系统出现设计和架构缺陷,从而影响系统的健康性和安全性。

站在文件结构的角度理解框架,可以将框架总结: 框架 = jar包 + 配置文件

https://i-blog.csdnimg.cn/direct/74746cbb7ae44d2a9bb09b76a7c85c08.png#pic_center

Spring 框架介绍

在学习 Spring 框架之前,我们需要搞懂 Spring 和 Spring Framework 之前的区别。

实际上,Spring 框架和 Spring Framework 实际上是同一个概念的不同表达方式。Spring Framework 是 Spring 框架的官方名称。

广义的 Spring:Spring 技术栈 (全家桶)

广义上的 Spring 泛指以 Spring Framework 为基础的 Spring 技术栈。

经过十多年的发展,Spring 已经不再是一个单纯的应用框架,而是逐渐发展成为一个由多个不同子项目(模块)组成的成熟技术,例如 Spring Framework、Spring MVC、SpringBoot、Spring Cloud、Spring Data、Spring Security 等,其中 Spring Framework 是其他子项目的基础。

这些子项目涵盖了从企业级应用开发到云计算等各方面的内容,能够帮助开发人员解决软件发展过程中不断产生的各种实际问题,给开发人员带来了更好的开发体验。

狭义的 Spring:Spring Framework (基础框架)

狭义的 Spring 特指 Spring Framework,通常我们将它称为 Spring 框架。

Spring Framework(Spring 框架)是一个开源的应用程序框架,由 Pivotal 公司(现为 VMware 旗下)开发,最初是为了解决企业级开发中各种常见问题而创建的。它提供了很多功能,例如:依赖注入(Dependency Injection)、面向切面编程(AOP)、声明式事务管理(TX)等。其主要目标是使企业级应用程序的开发变得更加简单和快速,并且 Spring 框架被广泛应用于 Java 企业开发领域。

Spring 全家桶的其他框架都是以 Spring Framework 框架为基础。

Spring 主要功能模块

SpringFramework框架结构图:

https://i-blog.csdnimg.cn/direct/0215420bd3b74af9b5ef5a00ed061ca1.png#pic_center

  • Spring Core Container :提供 Spring 的核心功能,包括依赖注入和 Bean 工厂。
  • Spring AOP :提供面向切面编程的支持。
  • Spring DAO :提供数据访问对象(DAO)支持,简化了 JDBC 的使用。
  • Spring ORM :提供对象关系映射(ORM)框架的集成支持,如 Hibernate、JPA 等。
  • Spring Web :提供 Web 开发的支持,包括 Spring MVC。
  • Spring Context :提供应用上下文的支持,用于配置和管理应用组件。
  • Spring Test :提供测试支持,简化了单元测试和集成测试的编写。

Spring 的主要特点

  1. 轻量级和非侵入性 :
    • Spring 框架是轻量级的,它的核心容器不需要依赖任何特定的应用服务器。
    • Spring 的应用代码不依赖于 Spring 的特定类和接口,这使得应用代码更加干净和可移植。
  2. 依赖注入(DI) :
    • Spring 通过依赖注入机制来管理对象之间的依赖关系。开发者只需要定义好对象的依赖关系,Spring 容器会自动完成对象的创建和依赖注入。
    • 这种方式减少了代码的耦合性,使得代码更加模块化和易于测试。
  3. 面向切面编程(AOP) :
    • Spring 提供了强大的 AOP 支持,允许开发者将横切关注点(如日志、事务管理、安全性等)从业务逻辑中分离出来。
    • 通过 AOP,开发者可以更好地实现关注点分离,提高代码的可维护性和可重用性。
  4. 事务管理 :
    • Spring 提供了一个一致的事务管理抽象,支持编程式和声明式事务管理。
    • 开发者可以通过简单的配置来管理事务,而不需要直接处理底层的事务 API。
  5. MVC 框架 :
    • Spring MVC 是 Spring 框架中的一个模块,用于构建基于 MVC 设计模式的 Web 应用程序。
    • 它提供了灵活的配置和强大的数据绑定、验证、国际化等功能。
  6. 集成支持 :
    • Spring 框架提供了与多种其他技术和框架的集成支持,如 Hibernate、JPA、JMS、Quartz、JDBC 等。
    • 这使得开发者可以轻松地将 Spring 与其他技术栈结合使用。
  7. 测试支持 :
    • Spring 提供了强大的测试支持,开发者可以轻松地进行单元测试和集成测试。
    • Spring 的测试框架支持 JUnit 和 TestNG,并且提供了 Mock 对象和上下文配置的支持。

Spring 的优势

  1. 丰富的生态系统:Spring 生态系统非常丰富,支持许多模块和库,如 Spring Boot、Spring Security、Spring Cloud 等等,可以帮助开发人员快速构建高可靠性的企业应用程序。
  2. 模块化的设计:框架组件之间的松散耦合和模块化设计使得 Spring 具有良好的可重用性、可扩展性和可维护性。开发人员可以轻松地选择自己需要的模块,根据自己的需求进行开发。
  3. 简化 Java 开发:Spring 简化了 Java 开发,提供了各种工具和 API,可以降低开发复杂度和学习成本。同时,Spring 支持各种应用场景,包括 Web 应用程序、RESTful API、消息传递、批处理等等。
  4. 不断创新和发展:Spring 开发团队一直在不断创新和发展,保持与最新技术的接轨,为开发人员提供更加先进和优秀的工具和框架。

因此,这些优点使得 Spring 成为了一个稳定、可靠、且创新的框架,为企业级 Java 开发提供了一站式的解决方案。

Spring 使创建 Java 企业应用程序变得容易。它提供了在企业环境中采用 Java 语言所需的一切,支持 Groovy 和 Kotlin 作为 JVM 上的替代语言,并且可以根据应用程序的需求灵活地创建多种架构。从Spring 6.0.6开始,Spring 需要 Java 17+。

Spring 的两大核心

Spring 框架的两大核心:Spring IoC(控制反转)和 Spring AOP(面向切面编程)。

Spring IoC 框架和核心概念

Spring IoC(Inversion of Control,控制反转)是 Spring 框架的核心特性之一,也是 Spring 实现依赖注入(Dependency Injection,DI)的基础。IoC 是一种设计原则,它将对象的创建、依赖关系的管理以及生命周期控制从应用程序代码中转移到框架或容器中,从而降低了代码的耦合性,提高了代码的灵活性和可维护性。

组件和组件管理

什么是组件

组件(Component) 是软件系统中可独立部署、可替换的功能单元。它通常是一个封装了特定功能或行为的模块,具有清晰的接口和明确的职责。组件可以是类、模块、服务、库等,具体取决于上下文。

组件的特点

  • 独立性 :组件可以独立开发、测试和部署。
  • 可复用性 :组件可以在不同的项目或系统中重复使用。
  • 接口清晰 :组件通过定义良好的接口与外部交互,隐藏内部实现细节。
  • 职责单一 :每个组件通常只负责一个特定的功能或任务。

简单来说:组件是映射到应用程序中所有可重用组件的 Java 对象,应该是可复用的功能对象!

注意: 组件一定是对象,但对象不一定是组件 。

组件的例子

  • 在 Spring 框架中,一个 Service 类、 Repository 类或 Controller 类都可以被视为组件。
  • 在前端开发中,一个按钮、表单或导航栏也可以被视为组件。

整个项目其实是由一个个组件组成的:

https://i-blog.csdnimg.cn/direct/5cfc16a0d3134899ae317204264adc75.png#pic_center

组件管理

组件管理(Component Management) 是指对组件的创建、配置、依赖注入、生命周期管理以及组织的过程。组件管理的目标是确保组件能够高效、灵活地协同工作,同时降低系统的复杂性和耦合性。

在 Spring 框架中,组件管理是通过 IoC 容器 来实现的。Spring 容器负责创建组件,管理它们的依赖关系,并控制它们的生命周期。

在 Spring 框架中,组件通常被称为 Bean 。

组件由 Spring 管理优势

  • 降低了组件之间的耦合性:Spring IoC 容器通过依赖注入机制,将组件之间的依赖关系削弱,减少了程序组件之间的耦合性,使得组件更加松散地耦合。
  • 提高了代码的可重用性和可维护性:将组件的实例化过程、依赖关系的管理等功能交给 Spring IoC 容器处理,使得组件代码更加模块化、可重用、更易于维护。
  • 方便了配置和管理:Spring IoC 容器通过 xml 文件或者注解,轻松的对组件进行配置和管理,使得组件的切换、替换等操作更加的方便和快捷。
  • 交给 Spring 管理的对象(组件),方可享受 Spring 框架的其他功能(AOP、声明事务管理)等。

普通容器和复杂容器

普通容器

普通容器只能用来存储数据,没有其它功能。

Java 中的普通容器:数组、集合等。

复杂容器

生活中的复杂容器

https://i-blog.csdnimg.cn/direct/d676260aa9c646138b50c2a26f255b88.png#pic_center

政府管理我们的一生,生老病死都和政府有关。

我们了解过的复杂容器: Servlet 容器 。

Servlet 容器能够管理 Servlet(init、service、destroy)、Filter、Listener 这样的组件的一生,所以它是一个复杂容器。

名称时机次数
创建对象默认情况:接收到第一次请求 修改启动顺序后:Web 应用启动过程中一次
初始化操作创建对象之后一次
处理请求接收到请求多次
销毁操作Web 应用卸载之前一次

同理, Spring IoC 容器也是一个复杂容器。它们不仅要负责创建组件的对象、存储组件的对象,还要负责调用组件的方法让它们工作,最终在特定情况下销毁组件。

Spring IoC 容器

IoC 的核心思想是 将控制权从应用程序代码反转给框架或容器 。在传统的编程模式中,对象的创建和依赖关系的管理是由开发者手动完成的,而在 IoC 模式下,这些工作由 Spring 容器负责。

Spring IoC 容器,负责实例化、配置和组装 bean(组件)。容器通过读取配置元数据来获取有关要实例化、配置和组装组件的指令。配置元数据以 xml、Java 注解或 Java 代码 形式表现。它允许表达组成应用程序的组件以及这些组件之间丰富的相互依赖关系。

IoC 容器实现

Spring IoC 容器是 Spring 框架的核心组件,负责管理对象的生命周期和依赖关系。Spring 提供了两种主要的 IoC 容器实现:

  1. BeanFactory :
    • 是 Spring 最基础的容器,提供了基本的依赖注入功能。
    • 适合资源受限的环境,延迟加载 Bean(只有在需要时才创建 Bean)。
  2. ApplicationContext :
    • 是 BeanFactory 的扩展,提供了更多的企业级功能,如国际化、事件传播、AOP 支持等。
    • 在应用启动时就会初始化所有的 Bean,适合大多数应用场景。

简而言之, BeanFactory 提供了配置框架和基本功能,而 ApplicationContext 添加了更多特定于企业的功能。 ApplicationContext 是 BeanFactory 的完整超集。

再往下, ApplicationContext 主要有4个容器实现类:

类型名简介
ClassPathXmlApplicationContext通过读取类路径下的 XML 格式的配置文件创建 IoC 容器对象
FileSystemXmlApplicationContext通过文件系统路径读取 XML 格式的配置文件创建 IoC 容器对象
AnnotationConfigApplicationContext通过读取 Java 配置类创建 IoC 容器对象
WebApplicationContext专门为 Web 应用准备,基于 Web 环境创建 IoC 容器对象,并将对象引入存入 ServletContext 域中。

IoC 的工作机制

Spring IoC 容器通过以下步骤实现对象的创建和依赖注入:

  1. 配置元数据 :
    • Spring 容器需要知道如何创建和管理对象,这些信息通过配置元数据提供。
    • 配置元数据可以通过 XML 文件、Java 注解或 Java 代码(基于配置类)来定义。
  2. 实例化 Bean :
    • 容器根据配置元数据创建 Bean 的实例。
  3. 依赖注入 :
    • 容器根据 Bean 之间的依赖关系,将所需的依赖注入到 Bean 中。
  4. 管理 Bean 的生命周期 :
    • 容器负责 Bean 的初始化、使用和销毁。

IoC 管理配置的方式

在 Spring 框架中,IoC 容器的配置方式主要有三种: 基于 XML 的配置 、 基于注解的配置 和 基于 Java 配置类 。每种配置方式都有其适用的场景和优势,开发者可以根据项目需求选择合适的配置方式。

基于 XML 的配置

这是 Spring 最早支持的配置方式,通过在 XML 文件中定义 Bean 及其依赖关系来配置 IoC 容器。

特点:

  • 优点 :配置与代码分离,适合大型项目或需要动态配置的场景。
  • 缺点 :配置繁琐,容易出错,可读性较差。

代码示例:

<!-- applicationContext.xml -->
<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="userRepository" class="com.example.UserRepository"/>

    <!-- 定义另一个 Bean,并注入依赖 -->
    <bean id="userService" class="com.example.UserService">
        <constructor-arg ref="userRepository"/>
    </bean>
</beans>

加载 XML 配置:

ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
UserService userService = context.getBean(UserService.class);
基于注解的配置

从 Spring 2.5 开始,支持通过注解来配置 IoC 容器。这种方式更加简洁,适合中小型项目。

常用注解:

  • @Component :标记一个类为 Spring 组件(通用注解)。
  • @Service :标记一个类为服务层组件。
  • @Repository :标记一个类为数据访问层组件。
  • @Controller :标记一个类为控制器层组件(通常用于 Spring MVC)。
  • @Autowired :自动注入依赖。
  • @Configuration :标记一个类为配置类。
  • @ComponentScan :扫描指定包下的组件。

示例:

// 定义一个组件
@Service
public class UserService {
    private final UserRepository userRepository;

    @Autowired
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
}

// 定义另一个组件
@Repository
public class UserRepository {
    public void save() {
        System.out.println("User saved!");
    }
}

// 配置类
@Configuration
@ComponentScan("com.example") // 扫描组件
public class AppConfig {
}

加载注解配置:

ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
UserService userService = context.getBean(UserService.class);
基于 Java 配置类

从 Spring 3.0 开始,支持通过 Java 类来配置 IoC 容器。这种方式结合了 XML 和注解的优点,既灵活又类型安全。

特点:

  • 优点 :类型安全,可读性强,适合需要编程式配置的场景。
  • 缺点 :配置与代码耦合,不适合需要动态配置的场景。

示例:

// 配置类
@Configuration
public class AppConfig {

    // 定义一个 Bean
    @Bean
    public UserRepository userRepository() {
        return new UserRepository();
    }

    // 定义另一个 Bean,并注入依赖
    @Bean
    public UserService userService(UserRepository userRepository) {
        return new UserService(userRepository);
    }
}

// 组件类
public class UserService {
    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
}

// 另一个组件类
public class UserRepository {
    public void save() {
        System.out.println("User saved!");
    }
}

加载 Java 配置:

ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
UserService userService = context.getBean(UserService.class);

IoC 和 DI

IoC 主要是针对对象的创建和调用控制而言的,也就是说,当应用程序需要使用一个对象时,不再是应用程序直接创建该对象,而是由 IoC 容器来创建和管理,即控制权由应用程序转移到 IoC 容器中,也就是“反转”了控制权。这种方式基本上是通过依赖查找的方式来实现的,即 IoC 容器维护着构成应用程序的对象,并负责创建这些对象。

传统模式 vs IoC 模式

  • 传统模式 :

    在传统编程模式中,对象的创建和依赖关系的管理是由开发者手动完成的。例如:

    public class UserService {
        private UserRepository userRepository = new UserRepository(); // 手动创建依赖对象
    }

    这种方式的缺点是代码耦合性高,难以测试和维护。

  • IoC 模式 :

    在 IoC 模式下,对象的创建和依赖关系的管理由框架或容器负责。例如:

    public class UserService {
        private UserRepository userRepository;
    
        // 通过构造函数注入依赖
        public UserService(UserRepository userRepository) {
            this.userRepository = userRepository;
        }
    }

    这种方式降低了代码的耦合性,提高了灵活性和可维护性。

IoC 的优点

  • 降低耦合性 :对象之间的依赖关系由容器管理,代码更加松耦合。
  • 提高可测试性 :依赖注入使得单元测试更加容易,可以通过 Mock 对象替换真实依赖。
  • 增强灵活性 :通过配置可以动态调整对象的创建和依赖关系。

DI 是 IoC 的一种实现方式,是指在组件之间传递依赖关系的过程中,将依赖关系在容器内部进行处理,这样就不必在应用程序代码中硬编码对象之间的依赖关系,实现了对象之间的解耦合。

依赖注入的方式包括构造函数注入、Setter 注入和字段注入。

  1. 构造函数注入 :

    • 通过构造函数注入依赖。

    • 示例:

      public class UserService {
          private final UserRepository userRepository;
      
          public UserService(UserRepository userRepository) {
              this.userRepository = userRepository;
          }
      }
  2. Setter 注入 :

    • 通过 Setter 方法注入依赖。

    • 示例:

      public class UserService {
          private UserRepository userRepository;
      
          public void setUserRepository(UserRepository userRepository) {
              this.userRepository = userRepository;
          }
      }
  3. 字段注入 :

    • 通过字段直接注入依赖。

    • 示例:

      public class UserService {
          @Autowired
          private UserRepository userRepository;
      }

DI 的优点

  • 解耦 :对象不需要知道如何创建或查找依赖,只需声明依赖关系。
  • 可测试性 :依赖注入使得单元测试更加容易,可以通过 Mock 对象替换真实依赖。
  • 可维护性 :依赖关系集中管理,便于修改和扩展。

IoC 实践

IoC 实现的几个步骤

  1. 定义组件(Bean) :

    在 Spring 中,组件通常被称为 Bean 。首先需要定义哪些类需要由 Spring 容器管理。

    定义方式:

    • XML 配置 :在 XML 文件中定义 Bean。
    • 注解配置 :使用 @Component 、 @Service 、 @Repository 、 @Controller 等注解标记类。
    • Java 配置类 :使用 @Bean 注解在配置类中定义 Bean。
  2. 配置元数据 :

    Spring 容器需要知道如何创建和管理 Bean,这些信息通过配置元数据提供。

  3. 初始化 IoC 容器 :

    Spring 容器负责管理 Bean 的创建和依赖注入。常见的 IoC 容器实现包括 ClassPathXmlApplicationContext 、 AnnotationConfigApplicationContext 等。

  4. 实例化 Bean :

    Spring 容器根据配置元数据创建 Bean 的实例。

    实例化过程:

    1. 容器读取配置元数据。
    2. 根据配置创建 Bean 的实例。
    3. 如果 Bean 有依赖关系,容器会递归创建依赖的 Bean。
  5. 依赖注入 :

​ Spring 容器根据 Bean 之间的依赖关系,将所需的依赖注入到 Bean 中。

  1. 管理 Bean 的生命周期 :

    Spring 容器负责管理 Bean 的整个生命周期,包括初始化、使用和销毁。

    生命周期阶段:

    1. 实例化 :容器创建 Bean 的实例。
    2. 属性赋值 :容器注入 Bean 的依赖。
    3. 初始化 :调用 Bean 的初始化方法(如 @PostConstruct 或 init-method )。
    4. 使用 :Bean 可以被应用程序使用。
    5. 销毁 :容器关闭时,调用 Bean 的销毁方法(如 @PreDestroy 或 destroy-method )。
  2. 使用 Bean :

    通过 Spring 容器获取 Bean 并使用。

基于 XML 方式管理配置

bean 标签的各个属性

在 Spring 框架中, <bean> 标签是用于定义和配置 bean 的核心元素。它可以通过 XML 配置文件来声明 Bean,并设置 Bean 的各种属性。

  1. id 和 name
  • 作用 :用于唯一标识一个 bean。

  • 区别 :

    • id 是 bean 的唯一标识符,必须唯一。
    • name 可以是多个别名,用逗号分隔。
  • 示例 :

    <bean id="userService" name="service,userServiceBean" class="com.example.UserService"/>
  1. class
  • 作用 :指定 bean 的全限定类名(包括包名)。

  • 示例 :

    <bean id="userService" class="com.example.UserService"/>
  1. scope
  • 作用 :定义 bean 的作用域。

  • 常用值 :

    • singleton :默认值,容器中只有一个 bean 实例。
    • prototype :每次请求时创建一个新的 bean 实例。
    • request :每个 HTTP 请求创建一个 bean 实例(仅适用于 Web 应用)。
    • session :每个 HTTP Session 创建一个 bean 实例(仅适用于 Web 应用)。
    • global-session :用于 Portlet 应用。
  • 示例 :

    <bean id="userService" class="com.example.UserService" scope="prototype"/>
  1. init-method 和 destroy-method
  • 作用 :

    • init-method :指定 bean 初始化后调用的方法。
    • destroy-method :指定 bean 销毁前调用的方法。
  • 示例 :

    <bean id="userService" class="com.example.UserService" init-method="init" destroy-method="cleanup"/>
  1. lazy-init
  • 作用 :控制 bean 是否延迟初始化。

  • 取值 :

    • true :延迟初始化,只有在第一次请求时才会创建 bean。
    • false :默认值,容器启动时立即初始化 bean。
  • 示例 :

    <bean id="userService" class="com.example.UserService" lazy-init="true"/>
  1. autowire
  • 作用 :指定 bean 的自动装配模式。

  • 常用值 :

    • no :默认值,不自动装配。
    • byName :根据属性名称自动装配。
    • byType :根据属性类型自动装配。
    • constructor :根据构造函数参数类型自动装配。
  • 示例 :

    <bean id="userService" class="com.example.UserService" autowire="byName"/>
  1. depends-on
  • 作用 :指定当前 bean 依赖的其他 bean,确保依赖的 bean 先初始化。

  • 示例 :

    <bean id="userService" class="com.example.UserService" depends-on="userRepository"/>
  1. parent
  • 作用 :指定当前 bean 的父 bean,继承父 bean 的配置。

  • 示例 :

    <bean id="parentService" class="com.example.ParentService" abstract="true">
        <property name="commonProperty" value="commonValue"/>
    </bean>
    
    <bean id="userService" class="com.example.UserService" parent="parentService">
        <property name="specificProperty" value="specificValue"/>
    </bean>
  1. abstract
  • 作用 :指定当前 bean 是否为抽象 bean。抽象 bean 不能被实例化,通常用作父 bean。

  • 取值 :

    • true :抽象 Bean。
    • false :默认值,非抽象 Bean。
  • 示例 :

    <bean id="parentService" class="com.example.ParentService" abstract="true"/>
  1. factory-method 和 factory-bean
  • 作用 :

    • factory-method :指定创建 bean 的工厂方法。
    • factory-bean :指定工厂 bean 的名称。
  • 示例 :

    <bean id="userServiceFactory" class="com.example.UserServiceFactory"/>
    <bean id="userService" factory-bean="userServiceFactory" factory-method="createUserService"/>
  1. primary
  • 作用 :指定当前 bean 为首选 bean。当有多个相同类型的 bean 时,优先使用 primary="true" 的 bean。

  • 取值 :

    • true :首选 Bean。
    • false :默认值,非首选 Bean。
  • 示例 :

    <bean id="userService" class="com.example.UserService" primary="true"/>
  1. property 子标签
  • 作用 :用于设置 bean 的属性值或依赖关系。

  • 常用属性 :

    • name :属性名称。
    • value :属性值(简单类型)。
    • ref :引用其他 Bean。
  • 示例 :

    <bean id="userService" class="com.example.UserService">
        <property name="userRepository" ref="userRepository"/>
        <property name="timeout" value="1000"/>
    </bean>
  1. constructor-arg 子标签
  • 作用 :用于通过构造函数注入依赖。

  • 常用属性 :

    • index :构造函数参数的索引。
    • type :构造函数参数的类型。
    • value :参数值(简单类型)。
    • ref :引用其他 bean。
    • name :构造函数参数的名称。
  • 示例 :

    <bean id="userService" class="com.example.UserService">
        <constructor-arg ref="userRepository"/>
        <constructor-arg value="1000"/>
    </bean>
  1. lookup-method 和 replaced-method
  • 作用 :

    • lookup-method :用于实现方法注入。
    • replaced-method :用于替换方法实现。
  • 示例 :

    <bean id="userService" class="com.example.UserService">
        <lookup-method name="createUser" bean="user"/>
    </bean>

组件声明配置

  1. 创建 maven 工程,导入依赖:

    <dependencies>
        <!--spring context依赖-->
        <!--当你引入Spring Context依赖之后,表示将Spring的基础依赖引入了-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>6.0.6</version>
        </dependency>
        <!--junit5测试-->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>5.3.1</version>
        </dependency>
    </dependencies>
  2. 基于无参构造函数构建:

    当通过构造函数方法创建一个 bean(组件对象) 时,所有普通类都可以由 Spring 使用并与之兼容。也就是说,正在开发的类不需要实现任何特定的接口或以特定的方式进行编码。只需指定 Bean 类信息就足够了。但是,默认情况下,我们需要一个默认(空)构造函数。

    1. 准备组件类:
    package org.yigongsui;
    
    public class HappyComponent {
    	//默认包含无参数构造函数
    	public void doWork() {
    		System.out.println("HappyComponent.doWork");
    	}
    }
    1. 编写 xml 配置文件:

    创建带有 Spring 约束的 xml 配置文件方法:

    https://i-blog.csdnimg.cn/direct/cb7eef026f82436997bc3abf788ca835.png#pic_center

    <?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="happyComponent" class="org.yigongsui.HappyComponent"/>
    
    </beans>
  3. 基于静态工厂方法实例化

    除了使用构造函数实例化对象,还有一类是通过工厂模式实例化对象。

    1. 组件类:
    public class ClientService {
      private static ClientService clientService = new ClientService();
      private ClientService() {}
    
      public static ClientService createInstance() {
    
        return clientService;
      }
    }
    1. xml 文件:
    <bean id="clientService"
      class="examples.ClientService"
      factory-method="createInstance"/>
  4. 基于实例工厂方法实例化

    1. 组件类:
    public class DefaultServiceLocator {
    
      private static ClientServiceImplclientService = new ClientServiceImpl();
    
      public ClientService createClientServiceInstance() {
        return clientService;
      }
    }
    1. xml 文件:
    <!-- 将工厂类进行ioc配置 -->
    <bean id="serviceLocator" class="examples.DefaultServiceLocator">
    </bean>
    
    <!-- 根据工厂对象的实例工厂方法进行实例化组件对象 -->
    <bean id="clientService"
      factory-bean="serviceLocator"
      factory-method="createClientServiceInstance"/>
  5. IoC 配置流程

https://i-blog.csdnimg.cn/direct/d2ae3fd990ac4599a7cec6ee246e7bd9.png#pic_center

组件依赖注入配置(DI)

  1. 目标

    通过配置文件,实现 IoC 容器中 bean 之间的引用(依赖注入 DI 配置)。

    主要涉及注入场景:基于构造函数的依赖注入和基于 Setter 的依赖注入。

  2. 思路

    https://i-blog.csdnimg.cn/direct/96b0dda2622b4688b26cfba1836de98c.png#pic_center

基于构造函数的依赖注入
单个构造参数

组件类:

public class UserDao {
}

public class UserService {
    
    private UserDao userDao;

    public UserService(UserDao userDao) {
        this.userDao = userDao;
    }
}

xml 文件:

<!-- 被引用类bean声明 -->
<bean id="userDao" class="org.yigongsui.ioc02.UserDao" />

<!-- 引用类bean声明 -->
<bean id="userService" class="org.yigongsui.ioc02.UserService">
  	<!-- 构造函数注入 -->
    <constructor-arg ref="userDao" />
</bean>

userDao 和 userService 声明不需要谁在前谁在后,因为 IoC 是一个高级容器,内部会进行缓存,先创建对象(IoC),再进行属性注入赋值(DI)。

多个构造函数

组件类:

public class UserDao {
}

public class UserService {
    
    private UserDao userDao;
    
    private int age;
    
    private String name;

    public UserService(int age , String name ,UserDao userDao) {
        this.userDao = userDao;
        this.age = age;
        this.name = name;
    }
}

xml 文件:

<bean id="userDao" class="org.yigongsui.ioc02.UserDao" />

<!-- 1.按照构造参数的顺序注入 -->
<bean id="userService1" class="org.yigongsui.ioc02.UserService">
    <constructor-arg value="18" />
    <constructor-arg value="zhangsan" />
    <constructor-arg ref="userDao" />
</bean>

<!-- 2.按照构造参数的名称注入 -->
<bean id="userService2" class="org.yigongsui.ioc02.UserService">
    <constructor-arg name="age" value="18" />
    <constructor-arg name="name" value="zhangsan" />
    <constructor-arg name="userDao" ref="userDao" />
</bean>

也可以使用 index 根据构造函数的下标索引配置,从0开始。

基于 Setter 方法注入

开发中,除了构造函数注入(DI)更多的使用的 Setter 方法进行注入。

组件类:

public class UserDao {
}

public class UserService {

    private UserDao userDao;
    private int age;
    private String name;

    public void setAge(int age) {
        this.age = age;
    }

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

    public void setName(String name) {
        this.name = name;
    }
}

xml 文件:

<bean id="userDao" class="org.yigongsui.ioc02.UserDao"/>

<bean id="userService" class="org.yigongsui.ioc02.UserService">
    <property name="age" value="18"/>
    <property name="name" value="zhangsan"/>
    <property name="userDao" ref="userDao"/>
</bean>

这里 name 值为 Setter 方法名去掉 set 后首字母小写的名字。

例如方法名 setUserDao ,name 值为 userDao 。

总结

依赖注入(DI)包含引用类型和基本数据类型,同时注入的方式也有多种,主流的注入方式为 Setter 方法注入和构造函数注入,两种注入语法都需要掌握。

需要特别注意:引用其他 bean,使用 ref 属性。直接注入基本类型值,使用 value 属性。

IoC 容器的创建和使用

想要配置文件中声明组件类信息真正的进行实例化成 bean 对象和形成 bean 之间的引用关系,我们需要声明 IoC 容器对象,读取配置文件,实例化组件和关系维护的过程都是在 IoC 容器中实现的。

组件类:

package org.yigongsui.ioc03;

public class User {

	private String name;

	public void setName(String name) {
		this.name = name;
	}

	public void work(){
		System.out.println(this.name + "今天工作了");
	}

}

Xml 文件:

<bean id="user" class="org.yigongsui.ioc03.User">
    <property name="name" value="张三"></property>
</bean>

测试类:创建 IoC 容器并实例化,读取 bean 对象并调用方法。

// 根据xml配置文件获取并实例化IoC容器
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");

// 根据bean的id属性获取IoC容器注入的对象
User user = applicationContext.getBean("user", User.class);

// 调用对象方法
user.work();

输出:

张三今天工作了

创建 IoC 容器的其它方法:

ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext();
//设置配置配置文件,方法参数为可变参数,可以设置一个或者多个配置
context.setConfigLocation("spring.xml");
//需要调用refresh方法,触发刷新配置
context.refresh();        

推荐直接根据 xml 文件配置,这种方法一般用在源码上。

获取 bean 对象的三种方法
  1. 根据 bean 的 id 和类型获取(推荐):
User user = applicationContext.getBean("user", User.class);
  1. 只根据 bean 的 id 属性获取:
User user = (User) applicationContext.getBean("user");

注意:只根据 id 返回的对象类型是 object 类型,需要强转成我们使用的类型。

  1. 只根据 bean 的类型获取:
User user = applicationContext.getBean(User.class);

注意:只能在配置一个 bean 类型的情况下使用,如果有多个 bean 的类型相同,会报错。

Bean 的作用域和周期方法配置

周期方法配置

我们可以在组件类中定义方法,然后当 IoC 容器实例化和销毁组件对象的时候进行调用。这两个方法我们成为生命周期方法。

类似于 Servlet 的 init() 或 destroy() 方法,我们可以在周期方法完成初始化和释放资源等工作。

组件类:

public class BeanOne {

  //周期方法要求: 方法命名随意,但是要求方法必须是 public void 无形参列表
  public void init() {
    // 初始化逻辑
  }
}

public class BeanTwo {

  public void cleanup() {
    // 释放资源逻辑
  }
}

xml 配置:

<beans>
  <bean id="beanOne" class="examples.BeanOne" init-method="init" />
  <bean id="beanTwo" class="examples.BeanTwo" destroy-method="cleanup" />
</beans>

测试:

ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");

BeanOne one = applicationContext.getBean("beanOne", BeanOne.class);
BeanTwo two = applicationContext.getBean("beanTwo", BeanTwo.class);

// 正常结束IoC容器
applicationContext.close();

注意:结束后,需要调用 applicationContext.close() 方法结束 IoC 容器,否则不会调用 destroy-method 。

组件作用域配置
bean 作用域概念

<bean> 标签声明 bean,只是将 bean 的信息配置给 Spring IoC 容器。

在 IoC 容器中,这些 <bean> 标签对应的信息转成 Spring 内部 BeanDefinition 对象, BeanDefinition 对象内,包含定义的信息(id、class、属性等等)。

这意味着, BeanDefinition 与 类 概念一样,Spring IoC 容器可以可以根据 BeanDefinition 对象反射创建多个 bean 对象实例。

具体创建多少个 bean 的实例对象,由 bean 的作用域 Scope 属性指定。

作用域的几种类型
取值含义创建对象的时机默认值
singleton在 IOC 容器中,这个 bean 的对象始终为单实例IOC 容器初始化时是
prototype这个 bean 在 IOC 容器中有多个实例获取 bean 时否

如果是在WebApplicationContext环境下还会有另外两个作用域(但不常用):

取值含义创建对象的时机默认值
request请求范围内有效的实例每次请求否
session会话范围内有效的实例每次会话否
代码测试

Xml 文件

<!--bean的作用域 
    准备两个引用关系的组件类即可!!
-->
<!-- scope属性:取值singleton(默认值),bean在IOC容器中只有一个实例,IOC容器初始化时创建对象 -->
<!-- scope属性:取值prototype,bean在IOC容器中可以有多个实例,getBean()时创建对象 -->
<bean id="happyMachine8" scope="prototype" class="com.atguigu.ioc.HappyMachine">
    <property name="machineName" value="happyMachine"/>
</bean>

<bean id="happyComponent8" scope="singleton" class="com.atguigu.ioc.HappyComponent">
    <property name="componentName" value="happyComponent"/>
</bean>

测试:

@Test
public void testExperiment08()  {
    ApplicationContext iocContainer = new ClassPathXmlApplicationContext("配置文件名");

    HappyMachine bean = iocContainer.getBean("happyMachine8",HappyMachine.class);
    HappyMachine bean1 = iocContainer.getBean("happyMachine8",HappyMachine.class);
    //多例对比 false
    System.out.println(bean == bean1);

    HappyComponent bean2 = iocContainer.getBean("happyComponent8",HappyComponent.class);
    HappyComponent bean3 = iocContainer.getBean("happyComponent8",HappyComponent.class);
    //单例对比 true
    System.out.println(bean2 == bean3);
}

FactoryBean 的特性及使用

FactoryBean 是 Spring 框架中的一个特殊接口,用于创建复杂的 Bean 实例。与普通的 Bean 不同, FactoryBean 本身是一个 Bean,但它负责创建和管理另一个 Bean 实例。通过 FactoryBean ,开发者可以自定义 Bean 的创建逻辑,从而实现更灵活的对象管理。

  1. 双重身份
  • FactoryBean 本身是一个 Bean,由 Spring 容器管理。
  • 它负责创建另一个 Bean 实例(称为目标 Bean)。
  1. 延迟创建
  • 目标 Bean 的创建可以延迟到第一次使用时,而不是在容器启动时立即创建。
  1. 复杂对象的创建
  • FactoryBean 适用于创建复杂的对象,例如:
    • 需要动态生成的对象。
    • 需要依赖外部资源的对象(如数据库连接、线程池等)。
    • 需要代理或装饰的对象。
  1. 透明的访问
  • 通过 FactoryBean 获取目标 Bean 时,Spring 容器会自动调用 FactoryBean 的 getObject() 方法,开发者无需显式调用。
FactoryBean 的核心方法

FactoryBean 接口定义了以下三个方法:

  1. T getObject()
  • 返回由 FactoryBean 创建的目标 Bean 实例。
  • 这是 FactoryBean 的核心方法。
  1. Class<?> getObjectType()
  • 返回目标 Bean 的类型。
  • 如果类型未知,可以返回 null 。
  1. boolean isSingleton()
  • 返回目标 Bean 是否是单例。
  • 如果返回 true ,表示目标 Bean 是单例;如果返回 false ,表示目标 Bean 是原型(每次请求都会创建一个新实例)。
代码示例
  1. 目标 Bean
public class User {
    private String name;
    private int age;

    // 构造函数、getter 和 setter 省略
}
  1. 实现 FactoryBean
import org.springframework.beans.factory.FactoryBean;

public class UserFactoryBean implements FactoryBean<User> {

    @Override
    public User getObject() throws Exception {
        // 创建复杂的 User 对象
        User user = new User();
        user.setName("John Doe");
        user.setAge(30);
        return user;
    }

    @Override
    public Class<?> getObjectType() {
        return User.class;
    }

    @Override
    public boolean isSingleton() {
        return true; // 目标 Bean 是单例
    }
}
  1. 配置 FactoryBean

在 XML 配置文件中注册 FactoryBean :

<bean id="userFactoryBean" class="com.example.UserFactoryBean"/>
  1. 获取目标 Bean

通过 Spring 容器获取目标 Bean:

ApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");

// 获取 FactoryBean 本身
UserFactoryBean userFactoryBean = context.getBean("&userFactoryBean", UserFactoryBean.class);

// 获取目标 Bean
User user = context.getBean("userFactoryBean", User.class);
System.out.println(user.getName()); // 输出: John Doe
注意事项
  1. 获取 FactoryBean 本身
  • 如果希望获取 FactoryBean 本身(而不是目标 Bean),可以在 Bean 名称前加上 & 符号。
  • 例如: context.getBean("&userFactoryBean") 。
  1. 目标 Bean 的作用域
  • 目标 Bean 的作用域由 isSingleton() 方法决定。
  • 如果 isSingleton() 返回 true ,目标 Bean 是单例;否则,每次请求都会创建一个新实例。
  1. 延迟初始化
  • FactoryBean 的目标 Bean 可以延迟初始化,直到第一次调用 getObject() 方法。
常见使用场景
  1. 创建代理对象
  • 使用 FactoryBean 创建动态代理对象,例如 AOP 中的代理。
  1. 集成第三方库
  • 通过 FactoryBean 集成第三方库的复杂对象,例如数据库连接池、线程池等。
  1. 动态生成对象
  • 根据运行时条件动态生成对象。
FactoryBean 和 BeanFactory 区别

** FactoryBean **是 Spring 中一种特殊的 bean,可以在 getObject() 工厂方法自定义的逻辑创建 Bean!是一种能够生产其他 Bean 的 Bean。 FactoryBean 在容器启动时被创建,而在实际使用时则是通过调用 getObject() 方法来得到其所生产的 Bean。因此, FactoryBean 可以自定义任何所需的初始化逻辑,生产出一些定制化的 bean。

一般情况下,整合第三方框架,都是通过定义 FactoryBean 实现!!!

BeanFactory 是 Spring 框架的基础,其作为一个顶级接口定义了容器的基本行为,例如管理 bean 的生命周期、配置文件的加载和解析、bean 的装配和依赖注入等。 BeanFactory 接口提供了访问 bean 的方式,例如 getBean() 方法获取指定的 bean 实例。它可以从不同的来源(例如 Mysql 数据库、XML 文件、Java 配置类等)获取 bean 定义,并将其转换为 bean 实例。同时, BeanFactory 还包含很多子类(例如, ApplicationContext 接口)提供了额外的强大功能。

总的来说, FactoryBean 和 BeanFactory 的区别主要在于前者是用于创建 bean 的接口,它提供了更加灵活的初始化定制功能,而后者是用于管理 bean 的框架基础接口,提供了基本的容器功能和 bean 生命周期管理。

基于注解+配置类方式管理配置

由于 xml 配置文件的编写非常复杂且麻烦,为了简化代码的编写,我们使用注解+配置类方式可以完全替代 xml 配置文件,这种方式也更适合我们的开发习惯。

核心注解

在使用注解+配置类开发方式时,我们需要先了解这种方法经常用到的注解。

Bean 定义与注册相关注解
  1. @Component

    • 作用 :标记一个类为 Spring 的组件(Bean),Spring 会自动扫描并注册到 IoC 容器中。
    • 使用场景 :通用的组件类。
    • 示例:
    @Component
    public class MyComponent {
        // 类内容
    }

    由于我们的业务通常要使用 MVC 三层架构,其中后端通常要编写 controller 、 service 、 dao 层,都需要使用 @Component 去标记,为了把它们与我们自定义的组件类做区分, @Component 衍生出有3个特化的注解去标记架构层。

    1. @Controller
      • 作用 :标记一个类为控制器组件,是 @Component 的特化版本。
      • 使用场景 :Web 层的控制器(Controller 层)。
    2. @Service
      • 作用 :标记一个类为服务层组件,是 @Component 的特化版本。
      • 使用场景 :业务逻辑层(Service 层)。
    3. @Repository
      • 作用 :标记一个类为数据访问层组件,是 @Component 的特化版本。Spring 会为 @Repository 类自动处理数据访问异常。
      • 使用场景 :数据访问层(DAO 层)。
  2. @Configuration

    • 作用 :标记一个类为配置类,相当于 XML 配置文件。
    • 使用场景 :定义 Bean 或配置 Spring 应用。
  3. @Bean

    • 作用 :在配置类中定义一个 Bean,方法返回值即为 Bean 实例。
    • 使用场景 :自定义 Bean 的创建逻辑,第三方 Bean 的注入。
依赖注入相关注解
  1. @Autowired

    • 作用 :自动注入依赖的 Bean。可以用于字段、构造方法、Setter 方法等。
    • 使用场景 :注入依赖对象。
  2. @Qualifier

    • 作用 :当存在多个相同类型的 Bean 时,用于指定具体的 Bean。
    • 使用场景 :解决 Bean 冲突。
  3. @Primary

    • 作用 :标记一个 Bean 为首选 Bean,当存在多个相同类型的 Bean 时,优先使用该 Bean。
    • 使用场景 :解决 Bean 冲突。
  4. @Value

    • 作用 :注入属性值(如从配置文件中读取的值)。
    • 使用场景 :注入简单类型(如字符串、数字)或配置属性。
配置与扫描相关注解
  1. @ComponentScan

    • 作用 :指定 Spring 扫描的包路径,自动注册带有 @Component 、 @Service 、 @Repository 、 @Controller 等注解的类为 Bean。
    • 使用场景 :启用组件扫描。
  2. @PropertySource

    • 作用 :加载外部配置文件(如 .properties 文件)。
    • 使用场景 :读取外部配置。
  3. @Import

    • 作用 :将其他配置类、组件类或特定的 ImportSelector 、 ImportBeanDefinitionRegistrar 实现类导入到当前配置类中。
    • 使用场景 :读取其他配置类。
Bean 生命周期相关注解
  1. @Scope

    • 作用 :用于定义 Bean 的作用域,不同的作用域会影响 Bean 的生命周期。
    • Singleton(默认) :Bean 在 Spring 容器中只有一个实例,生命周期与容器一致。
    • Prototype :每次请求时都会创建一个新的 Bean 实例,Spring 不管理其销毁。
    • Request / Session / Application :用于 Web 应用,分别对应请求、会话和应用级别的作用域。
  2. @PostConstruct :也可使用 @Bean 注解的 initMethod 属性指定。

    • 作用 :标记一个方法为 Bean 初始化后的回调方法。该方法会在 Bean 的依赖注入完成后执行。
    • 使用场景 :执行初始化逻辑,如数据加载、资源初始化等。
  3. @PreDestroy :也可使用 @Bean 注解的 destroyMethod 属性指定。

    • 作用 :标记一个方法为 Bean 销毁前的回调方法。该方法会在 Bean 被销毁之前执行。
    • 使用场景 :执行清理逻辑,如释放资源、关闭连接等。

注解使用

在大致了解了这些注解后,我们开始在代码中使用这些注解。

组件类标记为 Bean

准备组件类

  1. 自定义组件类:
@Component
public class MyComponent {
}
  1. DAO 层:
@Repository
public class UserDao {
}
  1. Service 层:
@Service
public class UserService {
}
  1. Controller 层:
@Controller
public class UserController {
}

这些注解可以直接标注在 Java 类上,将它们定义成 Spring Bean。

注解说明
@Component该注解用于描述 Spring 中的 Bean,它是一个泛化的概念,仅仅表示容器中的一个组件(Bean),并且可以作用在应用的任何层次,例如 Service 层、Dao 层等。 使用时只需将该注解标注在相应类上即可。
@Repository该注解用于将数据访问层(Dao 层)的类标识为 Spring 中的 Bean,其功能与 @Component 相同。
@Service该注解通常作用在业务层(Service 层),用于将业务层的类标识为 Spring 中的 Bean,其功能与 @Component 相同。
@Controller该注解通常作用在控制层(如 SpringMVC 的 Controller),用于将控制层的类标识为 Spring 中的 Bean,其功能与 @Component 相同。

通过查看源码我们得知, @Controller 、 @Service 、 @Repository 这三个注解只是在 @Component 注解的基础上起了三个新的名字。

对于 Spring 使用 IoC 容器管理这些组件来说没有区别,也就是语法层面没有区别。所以 @Controller 、 @Service 、 @Repository 这三个注解只是给开发人员看的,让我们能够便于分辨组件的作用。

注意:虽然它们本质上一样,但是为了代码的可读性、程序结构严谨!我们肯定不能随便胡乱标记。

关于 bean id 问题

在我们使用 XML 方式管理 bean 的时候,每个 bean 都有一个唯一标识——id 属性的值,便于在其他地方引用。现在使用注解后,每个组件仍然应该有一个唯一标识。

默认情况:

类名首字母小写就是 bean 的 id。例如: UserController 类对应的 bean 的 id 就是 userController 。

组件作用域及周期方法配置
  1. 准备 maven 依赖

在 java 9版本以后, @PostConstruct 和 @PreDestroy 注解被移到了 jakarta.annotation.PostConstruct 包下,所以需要引入依赖。

<dependencies>
  <dependency>
    <groupId>javax.annotation</groupId>
    <artifactId>javax.annotation-api</artifactId>
    <version>1.3.2</version>
  </dependency>
</dependencies>
  1. 定义组件类
@Component
public class MyBean {
}
  1. 配置周期方法
@PostConstruct
public void initMethod() {
  System.out.println("bean init!");
}

@PreDestroy
public void destroyMethod() {
  System.out.println("bean destroy!");
}
  1. 配置作用域
@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON) //单例,默认值
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) //多例
// 二选一

完整代码

package org.yigongsui.component;

import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
@Component
public class MyBean {

	@PostConstruct
	public void initMethod() {
		System.out.println("bean init!");
	}

	@PreDestroy
	public void destroyMethod() {
		System.out.println("bean destroy!");
	}

}
引用类型自动装配(DI)
  1. 场景准备

    业务中通常 Controller 层需要 Service 层,Service 层需要 Dao 层,同时各层都需要声明方法。

    1. Controller 层
    @Controller
    public class UserController {
    	private UserService userService;
    
    	public void getUser(){
    		userService.getUser();
    		System.out.println("user controller");
    	}
    }
    1. Service 层
    // 接口
    public interface UserService {
    	void getUser();
    }
    
    // 实现类
    @Service
    public class UserServiceImpl implements UserService {
    	private UserDao userDao;
    
    	@Override
    	public void getUser() {
    		userDao.save();
    		System.out.println("user service");
    	}
    }
    1. Dao 层
    // 接口
    public interface UserDao {
    	void save();
    }
    
    // 实现类
    @Repository
    public class UserDaoImpl implements UserDao {
    	@Override
    	public void save() {
    		System.out.println("user save");
    	}
    }
  2. 自动装配实现

    1. 前提

      参与自动装配的组件(需要装配、被装配)全部都必须在 IoC 容器中。

      注意:不区分 IoC 的方式,XML 和注解都可以。

    2. @Autowired 注解

      在成员变量上直接标记 @Autowired 注解即可,不需要提供 set 方法。以后我们在项目中的正式用法就是这样。

      1. Controller 层
      @Autowired
      private UserService userService;
      1. Service 层
      @Autowired
      private UserDao userDao;

      @Autowired 注解标记位置 :

      1. 成员变量 :也就是上边代码位置,这是最主要的使用方式,与 xml 进行 bean ref 引用不同,他不需要有 set 方法。
      2. 构造器
      3. set 方法

      @Autowired 工作流程

      https://i-blog.csdnimg.cn/direct/eea824ae6aeb4b9fb95001ea6d169f36.png#pic_center

      • 首先根据所需要的组件类型到 IOC 容器中查找
        • 能够找到唯一的 bean:直接执行装配
        • 如果完全找不到匹配这个类型的 bean:装配失败
        • 和所需类型匹配的 bean 不止一个
          • 没有 @Qualifier 注解:根据 @Autowired 标记位置成员变量的变量名作为 bean 的 id 进行匹配
            • 能够找到:执行装配
            • 找不到:装配失败
          • 使用 @Qualifier 注解:根据 @Qualifier 注解中指定的名称作为 bean 的id进行匹配
            • 能够找到:执行装配
            • 找不到:装配失败

      示例:当有两个 service 实现类时

      // 实现类1
      @Service
      public class UserServiceImpl implements UserService {
      
      	@Autowired
      	private UserDao userDao;
      
      	@Override
      	public void getUser() {
      		userDao.save();
      		System.out.println("user service");
      	}
      }
      
      // 实现类2
      @Service
      public class UserService2Impl implements UserService {
      
      	@Autowired
      	private UserDao userDao;
      
      	@Override
      	public void getUser() {
      		userDao.save();
      		System.out.println("user service");
      	}
      }

      controller 层注入的 service 如果没有指定是哪个bean 就会报错。

      解决方法:

      1. service 层注入容器的 bean id 默认为首字母小写的方法名,controller 层的成员属性名可以设置为这个方法名实现精确匹配。

        @Autowired
        private UserService userServiceImpl;

        由于实际开放中,属性名是很关键的信息,所以不推荐使用这种方法。

      2. 使用 @Qualifier 注解匹配。

        @Autowired
        @Qualifier("userServiceImpl")
        private UserService userService;
      3. 使用 @Resource 注解匹配,这个注解的 name 属性根据 bean id 进行匹配,等价于 @Autowired + @Qualifier

        @Resource(name = "userServiceImpl")
        private UserService userService;

      @Resource 和 @Autowired 注解对比 :

      @Resource 和 @Autowired 都用于实现依赖注入,但它们在所属规范、注入方式、查找顺序、依赖处理、适用场景等方面存在一些差异,以下是详细对比:

      1. 所属规范

        • @Resource :它是 Java 标准注解,来自 JSR - 250 规范。这意味着使用该注解编写的代码具有更好的可移植性,不依赖于特定的框架,能在支持 JSR - 250 的不同 Java 环境中使用。
        • @Autowired :是 Spring 框架提供的注解,与 Spring 框架紧密绑定。如果项目不使用 Spring 框架,就无法使用该注解。
      2. 注入方式

        • @Resource :支持按名称和按类型两种注入方式。
          • 当指定 name 属性时,会按照名称进行注入。
          • 若未指定 name 属性,会先尝试按名称查找,若找不到则按类型查找。
        • @Autowired :默认按类型进行注入。当容器中存在多个相同类型的 Bean 时,仅使用 @Autowired 会抛出异常,此时通常需要结合 @Qualifier 注解指定 Bean 的名称来明确使用哪个 Bean。
      3. 查找顺序

        • @Resource :先根据 name 属性的值查找对应的 Bean,如果 name 未指定,则使用字段名或方法名作为查找的名称。若按名称找不到匹配的 Bean,再按类型查找。

          import javax.annotation.Resource;
          
          public class MyService {
              // 先按名称 "myRepository" 查找 Bean,如果没指定 name 属性则用字段名 "repository" 查找,找不到再按类型查找
              @Resource(name = "myRepository") 
              private MyRepository repository;
          }
        • @Autowired :先根据类型查找匹配的 Bean,若找到多个相同类型的 Bean,会根据字段名或参数名作为 Bean 的名称进行匹配,如果还无法确定则需要使用 @Qualifier 注解。

          import org.springframework.beans.factory.annotation.Autowired;
          import org.springframework.beans.factory.annotation.Qualifier;
          
          public class MyService {
              @Autowired
              @Qualifier("myRepository") // 当有多个 MyRepository 类型的 Bean 时,指定使用名为 "myRepository" 的 Bean
              private MyRepository repository;
          }
      4. 依赖处理

        • @Resource :如果找不到匹配的 Bean,会抛出 NoSuchBeanDefinitionException 异常,要求依赖必须存在。

        • @Autowired :默认情况下,依赖必须存在,若找不到匹配的 Bean 会抛出异常。但可以通过设置 required = false 来允许依赖为 null 。

          import org.springframework.beans.factory.annotation.Autowired;
          
          public class MyService {
              @Autowired(required = false) // 允许 repository 为 null
              private MyRepository repository;
          }
      5. 适用场景

        • @Resource :适用于需要明确指定依赖名称的场景,或者在跨框架开发中,为了保持代码的兼容性和可移植性,优先选择 @Resource 。
        • @Autowired :在纯 Spring 项目中广泛使用,尤其是当依赖的类型是唯一确定的时候,使用 @Autowired 可以更简洁地完成依赖注入。
成员属性赋值

@Value 通常用于注入外部化属性。

这里用数据库连接池来模拟示例:

  1. 创建外部属性 db.properties :
db.driver=com.mysql.cj.jdbc.Driver
db.url=jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
db.username=root
db.password=123456
  1. 引入 druid 依赖
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.2.18</version> 
</dependency>
<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <version>8.0.33</version> 
</dependency>
  1. 创建主配置类,用于扫描外部属性文件:
@Configuration
@PropertySource("classpath:db.properties")
public class MyConfig {
}
  1. 创建 druid bean ,注入属性:
@Bean
public DataSource getDataSource(@Value("${db.driver}") String driver,
                @Value("${db.url}") String url,
                @Value("${db.username}") String username,
                @Value("${db.password}") String password) {
  DruidDataSource dataSource = new DruidDataSource();
  dataSource.setDriverClassName(driver);
  dataSource.setUrl(url);
  dataSource.setUsername(username);
  dataSource.setPassword(password);
  return dataSource;
}
主配置类配置

为了完全取代 xml 文件配置,还需要一个类去扫描各个组件类,我们称之为主配置类。

主配置类需要使用 @Configuration 标记,表明它是主配置类。

@Configuration
public class MyConfig {
}

其次,主配置类还需要去扫描其它所有 spring IoC 容器配置的注解。使用 @ComponentScan 去扫描指定包下的注解:

@Configuration
@ComponentScan("org.yigongsui")
public class MyConfig {
}

可以扫描多个包:

@ComponentScan({"org.yigongsui","org.yigongsui1",...})

如果有外部配置文件,需要使用 @PropertySource 扫描

@PropertySource("classpath:db.properties")

如果还有其它主配置类,可以使用 @Import 注解导入

@Configuration
public class MyConfig2 {
}

@Configuration
@Import(MyConfig2.class)
public class MyConfig {
}

至此,注解+配置类就可以完全取代 xml 文件配置 IoC 容器了。

AOP 面向切面编程

AOP 简介

什么是 AOP

Spring AOP(Aspect-Oriented Programming,面向切面编程)是 Spring 框架的一个重要模块,用于将横切关注点(如日志记录、事务管理、安全检查等)与核心业务逻辑分离。通过 AOP,你可以将这些横切关注点模块化,并在不修改核心代码的情况下将其应用到多个模块中。

简单来说,在大型项目开发时,我们对于项目的每个操作都要记录日志,正常开发下,需要在每个方法都记录日志输出,一旦方法过多,非常麻烦,我们可以把日志的输出提取成一个公共的方法,然后让 AOP 帮我们把这个日志方法插入到每个方法中,AOP 就是做这种插入的工作。

AOP 的核心概念

  • Aspect(切面) :横切关注点的模块化单元,通常是一个类,包含多个通知(Advice)和切点(Pointcut)。
  • Join Point(连接点) :程序执行过程中的某个特定点,如方法调用、异常抛出等。
  • Advice(通知) :在连接点执行的动作,如前置通知、后置通知等。
  • Pointcut(切点) :用于匹配连接点的表达式,决定哪些连接点会触发通知。
  • Weaving(织入) :将切面应用到目标对象并创建代理对象的过程。

AOP 的实现方式

Spring AOP 基于动态代理实现,支持两种代理方式:

  • JDK 动态代理 :适用于实现了接口的类。
  • CGLIB 代理 :适用于没有实现接口的类。

AOP 的通知类型

Spring AOP 支持以下五种通知类型:

  • @Before :在目标方法执行之前执行。
  • @After :在目标方法执行之后执行,无论是否抛出异常。
  • @AfterReturning :在目标方法成功执行并返回结果后执行。
  • @AfterThrowing :在目标方法抛出异常后执行。
  • @Around :在目标方法执行前后执行,可以控制是否执行目标方法。

AOP 的应用场景

  • 日志记录 :记录方法调用参数、执行时间等。
  • 事务管理 :通过 @Transactional 注解管理数据库事务。
  • 权限校验 :在方法执行前检查用户权限。
  • 性能监控 :统计方法执行耗时。
  • 异常处理 :统一捕获并处理异常。

AOP 的优缺点

优点 :

  • 与 Spring 容器无缝集成。
  • 配置简单,学习成本低。
  • 无需额外编译步骤。

缺点 :

  • 仅支持方法级别的拦截(无法拦截字段修改)。
  • 自调用问题:同一类内部方法调用不触发 AOP。
  • 功能弱于 AspectJ(如不支持编译时织入)。

AOP VS AspectJ

特性Spring AOPAspectJ
织入时机运行时(动态代理)编译时/加载时
性能较低更高
功能范围方法级别方法、构造器、字段等
依赖仅需 Spring 容器需 AspectJ 编译器

基于注解实现 AOP

AOP 底层技术组成

AOP 的底层技术围绕 代理生成 和 字节码增强 展开:

  • Spring AOP 依赖动态代理(JDK/CGLIB),简单易用但功能有限。
  • AspectJ 通过编译时/加载时织入实现更强大的 AOP,但需要额外工具支持。
  • 字节码操作库 (如 ASM、Javassist)是高级 AOP 实现的基础,直接操作字节码提供最高灵活性。(了解即可)

https://i-blog.csdnimg.cn/direct/c2a4cf84f57b45759041fbdbc8e9de18.png#pic_center

AspectJ:早期的 AOP 实现的框架,Spring AOP 借用了 AspectJ 中的 AOP 注解。

AOP 代码实现

环境准备
  1. 准备依赖

    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>6.0.6</version>
    </dependency>
    <!--junit5测试-->
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter-api</artifactId>
      <version>5.3.1</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-aspects</artifactId>
      <version>6.0.6</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-test</artifactId>
      <version>6.0.6</version>
      <scope>test</scope>
    </dependency>
  2. 准备接口和实现类

    public interface Calculator {
    	int add(int a, int b);
    	int sub(int a, int b);
    	int mul(int a, int b);
    	int div(int a, int b);
    }
    
    @Component
    public class CalculatorImpl implements Calculator {
    	@Override
    	public int add(int a, int b) {
    		return a + b;
    	}
    
    	@Override
    	public int sub(int a, int b) {
    		return a - b;
    	}
    
    	@Override
    	public int mul(int a, int b) {
    		return a * b;
    	}
    
    	@Override
    	public int div(int a, int b) {
    		return a / b;
    	}
    }
  3. 编写配置类,扫描组件

    @Configuration
    @ComponentScan("org.yigongsui")
    public class MyConfig {
    }
  4. 测试

    @SpringJUnitConfig(MyConfig.class)
    public class Test {
    
    	@Autowired
    	private Calculator calculator;
    
    	@Test
    	public void test(){
    		System.out.println(calculator.add(1,2));
    	}
    }
编写切面

以日志输出为例,步骤:

  1. 定义方法用于存储增强代码,具体定义几个方法,根据插入的位置个数决定。

  2. 使用注解配置,指定插入目标方法的位置。

    • 前置: @Before
    • 后置: @AfterReturning
    • 异常: @AfterThrowing
    • 最后: @After()
    • 环绕: @Around()
  3. 配置切点表达式

    切点表达式通常以 execution 关键字开头,格式为:

    execution([访问修饰符] 返回值类型 [类全限定名].方法名(参数列表) [异常类型])

    语法分解:

    组成部分说明示例片段
    访问修饰符可选,如 public 、 protected ,默认匹配所有访问权限public
    返回值类型必填, * 表示任意返回值类型int 、 void 、 *
    类全限定名可选,支持通配符,默认匹配所有类com.example.service.*
    方法名必填,支持通配符( * 匹配任意字符, .. 匹配多级路径)get* 、 find*ById
    参数列表必填, .. 表示任意个数和类型的参数(String) , (int, ..)
    异常类型可选(Spring AOP 不支持基于异常的切点)throws IOException

    通配符与符号:

    符号作用示例
    *匹配 任意数量字符 (不能跨包)com.*.service.*Impl
    ..匹配 任意层级子包 或 任意数量参数execution(* com..*.*(..))
    +匹配 指定类型及其子类 (需配合 within 使用)within(com.example.BaseService+)

    https://i-blog.csdnimg.cn/direct/4a8900a8329748bda7c47d105ac6a4f3.png#pic_center

    语法细节

    • 第一位: execution( ) 固定开头。
    • 第二位:方法访问修饰符。
    public private 直接描述对应修饰符即可
    • 第三位:方法返回值。
    int String void 直接描述返回值类型

    注意 :

    特殊情况:如果不考虑访问修饰符和返回值,以 * 代替,不能一个为具体,一个为 * 。

    execution(* * ) 这是错误语法
    execution(*) 正确,必须都不考虑
    • 第四位:指定包的地址。
    固定的包: org.yigongsui.api|service|dao 
    单层的任意命名: org.yigongsui.*等价于org.yigongsui.api和org.yigongsui.dao  
    *就是任意一层的任意命名 
    任意层任意命名: org..impl等价于org.yigongsui.api.impl或者org.a.a.a.a.a.a.a.impl  
    注意: ..不能用作包开头,即不能写成..impl,可以写成*..impl

    第五位:指定类名称。

    固定名称: UserService 任意类名: * 部分任意: com..service.impl.*Impl 任意包任意类: *..*

    第六位:指定方法名称。

    语法和类名一致 任意访问修饰符,任意类的任意方法: * *..*.*

    第七位:方法参数。

    具体值: (String,int) != (int,String) 如果没有参数 ()       
    模糊值: 任意参数 有 或者 没有 (..)  ..任意参数的形式     
    部分具体和模糊:         
    	第一个参数是字符串的方法 (String..)         
      最后一个参数是字符串 (..String)        
      字符串开头,int结尾 (String..int)         
      包含int类型(..int..)
代码实现
  1. 切面代码:
@Aspect
@Component
public class LogAdvice {
	@Before("execution(* org.yigongsui.impl.*.*(..)))")
	public void start() {
		System.out.println("[AOP前置通知] 方法开始了");
	}

	@AfterReturning("execution(* org.yigongsui.impl.*.*(..)))")
	public void end() {
		System.out.println("[AOP返回通知] 方法成功返回了");
	}

	@AfterThrowing("execution(* org.yigongsui.impl.*.*(..)))")
	public void error() {
		System.out.println("[AOP异常通知] 方法抛异常了");
	}

	@After("execution(* org.yigongsui.impl.*.*(..)))")
	public void after() {
		System.out.println("[AOP后置通知] 方法最终结束了");
	}
}
execution(* org.yigongsui.impl.*.*(..)))

第一个 * :返回值,可以为任意类型。

org.yigongsui.impl :连接点的包名,也可以写成 org..impl

第二个 * :包下的所有类。

第三个 * :类下的所有方法。

(..) :方法任意数量参数。

开启配置类注解支持

  1. 在配置类上添加注解: @EnableAspectJAutoProxy
@Configuration
@ComponentScan("org.yigongsui")
@EnableAspectJAutoProxy
public class MyConfig {
}
  1. 测试
获取通知细节信息

在实际开发中,我们通常需要获取切点的信息,例如方法名,参数,返回值等。

如果要在通知上获取这些信息,我们需要用到 JoinPoint 接口。

JoinPoint 接口

JoinPoint 是面向切面编程(AOP)中的核心接口,用于封装目标方法的连接点信息。通过它,开发者可以在通知(Advice)中获取被拦截方法的上下文数据,例如方法签名、参数值、目标对象等。以下是其核心功能与使用方式的详细说明:

方法名返回值类型说明
getSignature()Signature获取连接点的方法签名(方法名、参数类型等元数据)
getArgs()Object[]返回方法调用时传入的 实际参数值 (按参数顺序存储)
getTarget()Object获取被代理的 目标对象 (即被调用的原始对象实例)
getThis()Object获取当前代理对象(AOP生成的代理类实例)
getKind()String标识连接点类型(Spring AOP中固定为 method-execution )
toShortString()String简化的连接点描述(格式如: execution(UserService.findById(Long)) )
toLongString()String完整的连接点描述(包含类全限定名和方法参数类型)

Signature 对象解析

getSignature() 返回的 Signature 接口进一步提供方法元数据,需强制转换为 MethodSignature 获取详细信息:

MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();

// 核心方法
Method method = methodSignature.getMethod();      // 获取被调用的方法对象
String methodName = methodSignature.getName();    // 方法名(如 "saveUser")
Class<?> returnType = methodSignature.getReturnType(); // 返回值类型
Class<?>[] paramTypes = methodSignature.getParameterTypes(); // 参数类型列表
String[] paramNames = methodSignature.getParameterNames();   // 参数名称列表(需编译时保留调试信息)

注意事项

  1. 代理对象与目标对象

    • getTarget() :返回原始对象(未被代理的实例)
    • getThis() :返回代理对象(Spring 使用 JDK 动态代理或 CGLIB 生成)
  2. 参数名称获取限制

    • methodSignature.getParameterNames() 需要编译时保留调试信息( -parameters 编译选项)
  3. 环绕通知特殊要求

    • 必须使用 ProceedingJoinPoint (继承自 JoinPoint )
    • 必须调用 proceed() 方法执行目标方法,否则会阻断调用链
  4. 性能影响

    频繁调用 getArgs() 或解析签名可能带来性能开销,建议在关键路径中谨慎使用。

代码实现
  1. 获取方法所属类的信息,方法名,参数信息。
@Before("execution(* org.yigongsui.impl.*.*(..)))")
public void start(JoinPoint joinPoint) {
  // 1.获取方法属于的类的信息
  String simpleName = joinPoint.getTarget().getClass().getSimpleName();
  System.out.println(simpleName);

  // 2.获取方法名称和修饰符
  String methodName = joinPoint.getSignature().getName();
  String s = Modifier.toString(joinPoint.getSignature().getModifiers());
  System.out.println(methodName);
  System.out.println(s);

  // 3.获取参数列表
  Object[] args = joinPoint.getArgs();
  for (int i = 0; i < args.length; i++) {
    System.out.println(args[i]);
  }
}
  1. 获取方法返回值。

直接在 @AfterReturning 注解的方法下获取返回值,使用 returning 属性指定,指定名称即为通知方法的形参名称。

@AfterReturning(value = "execution(* org.yigongsui.impl.*.*(..)))",returning = "result")
public void end(JoinPoint joinPoint, Object result) {
  System.out.println(result);
}
  1. 获取异常信息。

@AfterThrowing 注解下获取,使用 throwing 属性指定。

@AfterThrowing(value = "execution(* org.yigongsui.impl.*.*(..)))",throwing = "e")
public void error(JoinPoint joinPoint, Exception e) {
  System.out.println(e.getMessage());
}
切点表达式的提取和复用

正常需要在每一个通知上定义切点表达式,非常麻烦。

所以我们需要提取一个公共的切点表达式,并在通知上进行复用。

在当前类中提取
  1. 编写一个空方法,使用注解 @Pointcut ,定义切点表达式。
  2. 在其它通知上使用这个空方法的切点表达式。
@Pointcut("execution(* org.yigongsui.impl.*.*(..)))")
public void pc() {}

@Before("pc()")
public void start(JoinPoint joinPoint) {}

在实际开发中,如果有多个切点表达式,通常把切点表达式的方法提取到一个公共类中,其它通知直接引用即可。

环绕通知

环绕通知( @Around )是 AOP(面向切面编程)中最强大且灵活的通知类型 。它允许开发者完全控制目标方法的执行流程,包括拦截方法调用、修改参数、处理返回值或异常,甚至阻止方法执行。以下是其核心要点:

  1. 完全控制方法执行 :环绕通知通过 ProceedingJoinPoint 参数直接决定是否(及何时)执行目标方法。
  2. 唯一能修改参数和返回值的通知 :
    • 修改参数 :通过 pjp.getArgs() 获取参数数组并修改。
    • 替换返回值 :在 pjp.proceed() 后修改返回的 result 。
  3. 异常处理能力 :可捕获目标方法抛出的异常,决定是否重新抛出或静默处理。

总结来说, @Around 包含上面所有注解的所有功能。

使用这个注解,必须在通知方法里定义返回值(目标方法的返回值),定义参数( ProceedingJoinPoint joinPoint ),抛出异常或直接 try...catch 处理,执行目标方法。

@Around("pc()")
public Object around(ProceedingJoinPoint joinPoint){

  // 获取目标方法的参数
  Object[] args = joinPoint.getArgs();
  Object result = null;

  try {
    // 执行目标方法并获取方法结果
    result = joinPoint.proceed(args);
  } catch (Throwable e) {
    throw new RuntimeException(e);
  }

  // 返回结果
  return result;

}

之后在任意位置编写增强代码。

切面优先级设置

相同目标方法上同时存在多个切面时,切面的优先级控制切面的内外嵌套顺序。

  • 优先级高的切面:外面
  • 优先级低的切面:里面

使用 @Order 注解可以控制切面的优先级:数越小,优先级越高。

https://i-blog.csdnimg.cn/direct/a523689ed1514fba94ce83f42752a40d.png#pic_center

实际开发时,如果有多个切面嵌套的情况,要慎重考虑。例如:如果事务切面优先级高,那么在缓存中命中数据的情况下,事务切面的操作都浪费了。

https://i-blog.csdnimg.cn/direct/8abc7509081b44c693445cf5f0641961.png#pic_center

此时应该将缓存切面的优先级提高,在事务操作之前先检查缓存中是否存在目标数据。

总结

https://i-blog.csdnimg.cn/direct/a5a3f8102485471888e15bdb83eff065.png#pic_center

Spring 声明式事务

编程式事务

编程式事务是指手动编写程序来管理事务,即通过编写代码的方式直接控制事务的提交和回滚。在 Java 中,通常使用事务管理器(如 Spring 中的 PlatformTransactionManager )来实现编程式事务。

编程式事务的主要优点是灵活性高,可以按照自己的需求来控制事务的粒度、模式等等。但是,编写大量的事务控制代码容易出现问题,对代码的可读性和可维护性有一定影响。

编程式的实现方式存在缺陷:

  • 细节没有被屏蔽:具体操作过程中,所有细节都需要程序员自己来完成,比较繁琐。
  • 代码复用性不高:如果没有有效抽取出来,每次实现功能都需要自己编写代码,代码就没有得到复用。

声明式事务

Spring 的 声明式事务管理 是一种基于 AOP 的事务管理方式,开发者通过配置(而非硬编码)定义事务规则,将事务逻辑与业务代码解耦。其核心思想是 “约定优于配置” ,通过注解或 XML 定义事务边界和属性,由 Spring 框架自动管理事务的提交、回滚等操作,使用声明式事务可以将事务的控制和业务逻辑分离开来,提高代码的可读性和可维护性。

Spring 的声明式事务是由 Spring tx 模块实现的。

区别:

  • 编程式事务需要手动编写代码来管理事务 。
  • 而声明式事务可以通过配置文件或注解来控制事务 。

核心优势

  1. 非侵入性 :业务代码无需包含事务管理逻辑(如 try-catch 或 JDBC 事务 API),只需通过注解声明事务规则。
  2. 可维护性高 :事务配置集中管理,修改时无需改动业务代码。
  3. 灵活配置 :支持细粒度的事务属性控制(如传播行为、隔离级别、超时时间等)。

核心注解( @Transactional )

通过 @Transactional 注解标记需要事务管理的方法或类,支持以下关键属性:

属性作用默认值
propagation事务传播行为(多个事务方法嵌套调用时的处理规则)Propagation.REQUIRED
isolation事务隔离级别(解决并发事务导致的数据问题)Isolation.DEFAULT (依赖数据库)
timeout事务超时时间(秒),超时自动回滚-1(无限制)
readOnly是否只读事务(优化数据库引擎性能)false
rollbackFor指定触发回滚的异常类型(默认仅对 RuntimeException 和 Error 回滚){}
noRollbackFor指定不触发回滚的异常类型{}
事务传播行为(PROPAGATION)

定义多个事务方法相互调用时的处理规则:

传播行为说明
REQUIRED当前存在事务则加入,否则新建事务(默认值)
REQUIRES_NEW始终新建事务,若当前存在事务则挂起
NESTED在当前事务内嵌套子事务(支持部分提交,依赖数据库 Savepoint 功能)
SUPPORTS当前存在事务则加入,否则以非事务方式执行
NOT_SUPPORTED以非事务方式执行,若当前存在事务则挂起
MANDATORY必须在事务中执行,否则抛出异常
NEVER必须在非事务中执行,否则抛出异常

场景示例

// 外层方法
@Transactional(propagation = Propagation.REQUIRED)
public void outerMethod() {
    innerMethod(); // 调用内层方法
}

// 内层方法
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void innerMethod() {
    // 无论外层是否回滚,内层事务独立提交
}
事务隔离级别(ISOLATION)

解决并发事务导致的数据问题:

隔离级别脏读不可重复读幻读说明
DEFAULT---使用数据库默认级别(如 MySQL 默认为 REPEATABLE_READ )
READ_UNCOMMITTED✔️✔️✔️最低隔离级别,性能高但数据一致性差
READ_COMMITTED✖️✔️✔️避免脏读(Oracle 默认)
REPEATABLE_READ✖️✖️✔️避免脏读和不可重复读(MySQL 默认)
SERIALIZABLE✖️✖️✖️最高隔离级别,通过锁表避免并发问题,性能最低

Spring 事务管理器

Spring声明式事务对应依赖

  • spring-tx :包含声明式事务实现的基本规范(事务管理器规范接口和事务增强等等)。
  • spring-jdbc :包含 DataSource 方式事务管理器实现类 DataSourceTransactionManager 。
  • spring-orm :包含其他持久层框架的事务管理器实现类例如:Hibernate、Jpa 等。

Spring声明式事务对应事务管理器接口

https://i-blog.csdnimg.cn/direct/b28aff531f144e7fa1dab42557e4d2c6.png#pic_center

我们现在要使用的事务管理器是 org.springframework.jdbc.datasource.DataSourceTransactionManager ,将来整合 JDBC 方式、JdbcTemplate 方式、Mybatis 方式的事务实现!

DataSourceTransactionManager 类中的主要方法:

  • doBegin() :开启事务。
  • doSuspend() :挂起事务。
  • doResume() :恢复挂起的事务。
  • doCommit() :提交事务。
  • doRollback() :回滚事务。

代码示例

@Configuration
@EnableTransactionManagement // 启用事务管理
public class AppConfig {
    @Bean
    public DataSourceTransactionManager transactionManager(DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource); // 配置事务管理器
    }
}

@Service
public class UserService {
  	// 添加事务
    @Transactional(
        isolation = Isolation.READ_COMMITTED,
        propagation = Propagation.REQUIRED,
        timeout = 30,
        rollbackFor = {SQLException.class}
    )
    public void updateUser(User user) {
        // 业务逻辑
    }
}

/weixin_miniapp.png