Spring

核心技术

Spring框架的控制反转IoC 容器。

Spring面相切面编程 AOP 技术。

此外还有Spring和AspectJ

IOC容器

Spring IoC容器和Bean简介

原本是 “对象自己找依赖”(比如 A 类要用到 B 类,A 自己 new B、自己找 B 的实例),现在是 “容器给对象送依赖”(A 不用管 B 怎么来,容器提前准备好 B,在创建 A 时主动 “塞” 给 A)—— 这种 “找依赖的权力从对象手里转到容器手里” 的反转,就是 IoC;而容器 “塞依赖” 的具体动作,就是 DI(依赖注入)

IoC 是 “设计原则”(核心思想是 “反转依赖控制权”),DI 是 “实现方式”(具体怎么把依赖给对象)—— 二者本质是同一概念的不同角度描述,Spring 用 DI 的方式实现了 IoC 原则。

一个对象(比如 A 类)要和其他对象(比如 B 类、C 类,也就是 A 的 “依赖”)合作,不用自己去创建或查找这些依赖,只需要 “明确告诉容器自己需要什么”—— 告诉的方式有 3 种:

  • 构造参数:A 的构造方法里写public A(B b) { ... }(告诉容器 “我需要 B”);
  • 工厂方法参数:如果 A 是通过工厂方法创建的,工厂方法里写public static A createA(B b) { ... }(告诉容器 “创建我需要 B”);
  • 属性设置:A 里写private B b; + setter 方法public void setB(B b) { ... }(告诉容器 “我需要 B,创建后给我设进来”)。

Spring 的核心是 “IoC 容器”(可以理解为 “对象管家”),容器会提前创建好所有需要的依赖对象(比如 B、C),当容器创建 A(Spring 里的对象叫 “bean”)时,会按照 A 之前 “告诉” 的方式(构造参数 / 工厂参数 / 属性),把 B、C 主动 “塞” 到 A 里 —— 这个 “塞” 的动作就是 “依赖注入(DI)”。

  • 这是最关键的一句,解释 “控制反转” 的 “反转” 到底是什么:
    • 「传统方式(没有 IoC)」:Bean 自己控制依赖 → A 要用到 B,A 自己用B b = new B();(直接构建),或者自己找个 “服务定位器” 查 B 的实例(比如B b = ServiceLocator.getB();)—— 控制权在 A 手里。
    • 「IoC 方式」:容器控制依赖 → A 不用自己 new B、不用自己查 B,控制权转到了 IoC 容器手里 —— 这就是 “控制的反转”(从对象反转到容器)。

例子

IoC 方式(Spring 实现):容器控制依赖,注入给对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// DataBaseDao:还是原来的类,不用改
public class DataBaseDao {
public List<Score> queryScores() { ... }
}

// ScoreService:只“声明依赖”,不“创建依赖”(控制权交给容器)
public class ScoreService {
// 声明需要DataBaseDao(通过属性+setter,告诉容器自己需要它)
private DataBaseDao dataBaseDao;

// setter方法:给容器提供“注入依赖”的入口
public void setDataBaseDao(DataBaseDao dataBaseDao) {
this.dataBaseDao = dataBaseDao;
}

public void calculateScore() {
List<Score> scores = dataBaseDao.queryScores(); // 直接用容器注入的依赖
}
}

// Spring配置(告诉容器要管理哪些bean,以及依赖关系)
<bean id="dataBaseDao" class="com.example.DataBaseDao"/>
<bean id="scoreService" class="com.example.ScoreService">
<!-- 容器创建scoreService时,把dataBaseDao注入进去(调用setter方法) -->
<property name="dataBaseDao" ref="dataBaseDao"/>
</bean>

Spring IoC容器的两个核心组件

BeanFactory 和 ApplicationContext

Spring IoC 容器核心功能由两个接口支撑,他们的关系类似 基础班工具和升级版工具,ApplicationContext包含BeanFactory所有功能并且新增了更多特性。

1、BeanFactory接口

核心功能,能创建对象 Bean,组装对象之间的依赖关系,比如A 依赖B ,就会把 B 塞给 A ,不管对象是什么类型都能管。

2、ApplicationContext 接口

是BeanFactory 的 子接口,相当于继承了基础款的所有功能,是更强大的企业级 IoC容器。

新增了 AOP集成更方便:直接支持Spring AOP 不用额外复杂配置

国际化支持:能支持多语言消息 中文环境显示你好,英文环境显示Hello

事件发布:支持 事件通知 机制,比如某个对象状态变化时,自动通知其他关心这个变化的对象。

场景化扩展:针对特定场景提供专用容器,比如WebApplicationContext 专门给 Web 应用 比如Spring MVC 用,能更好地适配Web环境。

Bean的概念

Bean是Spring IoC容器管理的特殊对象,理解他的关系是区分 普通对象 和 Spring Bean

1、普通对象 vs Spring Bean

普通对象是自己new创建的对象,生命周期由自己控制,创建、销毁全靠代码 创建 -> 使用 -> 不可达 -> 销毁/回收

Spring Bean:由Spring IoC容器,BeanFactory或者ApplicationContext负责实例化(创建对象),组装(处理依赖,比如给对象的属性赋值)、管理(控制对象的生命周期,比如什么时候创建,什么时候销毁)

2、Bean的 配置元数据

容器怎么知道要创建哪些 Bean、Bean 之间有什么依赖关系?靠 “配置元数据”—— 就是你告诉容器的 “清单”,比如:

  • XML 配置文件(<bean id="userService" class="com.xxx.UserService">...</bean>);
  • 注解(@Component@Service等,标记哪些类需要被容器管理);
  • Java 配置类(@Configuration + @Bean注解,手动定义 Bean)。

这些元数据里会写明:要创建哪个类的对象、对象的依赖是谁、对象的初始化参数是什么等,容器照着 “清单” 干活。

IOC容器的概念

Spring IoC容器的工作机制

1、ApplicationContext 是 Spring IoC 容器的 实体代表,相当于一个智能工厂,核心职责有三个

  • 实例化:按照规则创建应用程序需要的对象,比如Service、Dao等
  • 配置:给对象设置属性,比如给UserService的name属性赋值
  • 组装:处理对象之间的依赖 比如OrderService需要UserService,容器会自动把UserService 连接到 OrderService里

2、核心依赖,容器做这些事的依据是配置元数据,相当于给工程的生产清单,清单里写着:

  • 要创建哪些类的对象,比如com.xxx.UserService
  • 每个对象的属性怎么设置,比如UserService的timeout设置为3000
  • 对象之间的依赖关系,比如OrderService 依赖 UserService

3、元数据格式:生产清单可以用3种形式写:

  • xml 文件 传统方式:比如 <bean id="userService" class="com.xxx.UserService"/>
  • Java注解:比如在类上标@Service,告诉容器这个类要被管理
  • Java代码:配置类的方式,比如用@Configuration + @Bean注解手动定义对象。

思路:

(1)定义xml文件,里面定义bean标签,因为后续每个bean标签会被解析为一个对象

(2)解析xml文件(dom4j),解析的时候,会解析xml文件中的bean标签,每个bean标签转换为一个bean对象,此对象包含两个属性:id、class,此对象用来存放bean的id和class值。

(3)因为xml文件中bean标签可能是多个,所以定义一个List集合,存储bean对象。

(4)遍历List集合,得到每一个bean对象,通过bean对象的class属性,反射创建对应的对象。

(5)对象创建好以后,将bean对象的id和反射创建的对象,放入map集合中。

(6)定义一个工厂,(2)-(5)步骤放在工厂的构造器中完成

(7)工厂中定义获取对象的方法,通过id从map集合中获取对象。

ApplicationContext的具体实现和使用场景

Spring提供了多个ApplicationContext的视线,就像工厂有 不同的生产线,适配不同的场景。

1、独立应用 非Web 常用的两种

  • ClassPathXmlApplicationContext:从项目的 类路径 比如 src/main/resources目录 ,读取XML配置文件
  • FileSystemXmlApplicationContext:从操作系统的文件系统路径 比如 D:/config/spring.xml 读取 xml 配置

2、Web应用:用专门的WebApplicationContext,适配Tomcat等Web 服务环境。

3、简化配置的技巧:

  • 虽然XML是传统格式,但可以用少量XML配置 “开启注解支持” 比如 <context:component-scan> ,之后主要使用@Service,@Autowired等注解管理Bean,不用写大量XML
  • 实际开发中几乎不用手动写代码创建容器:比如Web应用只需要在web.xml里加几行模版配置,容器会由 Web 服务器自动初始化
配置元数据

Spring IoC容器消费一种配置元数据。这种配置元数据代表了你,作为一个应用开发者,如何告诉Spring容器在你的应用中实例化、配置和组装对象。

基于XML的元数据并不是配置元数据的唯一允许形式。Spring IOC 容器本身与这种配置元数据的实际编写格式是完全解耦的。如今许多开发者用基于Java的配置

关于在Spring容器中使用其他形式的元数据的信息,请参见。

  • 基于注解的配置 使用基于注解的配置元数据定义Bean。
  • Java-based configuration 通过使用Java而不是XML文件来定义你的应用类外部的Bean。要使用这些特性,请参阅 @Configuration @Bean, @Import, 和 @DependsOn注解。

Spring的配置包括至少一个,通常是一个以上的Bean定义,容器必须管理这些定义。基于XML的配置元数据将这些Bean配置为顶层 <beans/> 元素内的 <bean/> 元素。Java配置通常使用 @Configuration 类中的 @Bean 注解的方法。

这些Bean的定义对应于构成你的应用程序的实际对象。通常,你会定义服务层对象、持久层对象(如存储库或数据访问对象(DAO))、表现对象(如Web控制器)、基础设施对象(如JPA EntityManagerFactory)、JMS队列等等。通常,人们不会在容器中配置细粒度的domain对象,因为创建和加载domain对象通常是 repository 和业务逻辑的责任。

下面的例子显示了基于XML的配置元数据的基本结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?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
https://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="..." class="...">
<!-- 这个bean的合作者和配置在这里 -->
</bean>

<bean id="..." class="...">
<!-- c这个bean的合作者和配置在这里 -->
</bean>

<!-- 更多bean 定义在这里 -->

</beans>
id 属性是一个字符串,用于识别单个Bean定义。
class 属性定义了 Bean 的类型,并使用类的全路径名。

id 属性的值可以用来指代协作对象。本例中没有显示用于引用协作对象的XML。

实例化一个容器

配置好元数据了,那么就可以实例化一个容器然后就可以根据配置在容器中new对象了

在ApplicationContext 构造函数的一条或者多条路径是资源字符串,它让容器从各种外部资源(如本地文件系统、Java CLASSPATH)加载配置元数据

1
ApplicationContext context = new ClassPathXmlApplicationContext("services.xml","daos.xml")

然后接口层service的对象引用的持久层的dao层对象,然后dao层的对象引用具体的类,这样子的配置类有些许的不同,下面是例子

下面的例子显示了 service 对象(services.xml)配置文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?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
https://www.springframework.org/schema/beans/spring-beans.xsd">

<!-- services -->

<bean id="petStore" class="org.springframework.samples.jpetstore.services.PetStoreServiceImpl">
<property name="accountDao" ref="accountDao"/>
<property name="itemDao" ref="itemDao"/>
<!-- additional collaborators and configuration for this bean go here -->
</bean>

<!-- more bean definitions for services go here -->

</beans>

下面的例子显示了数据访问对象(data access object) daos.xml 文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?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
https://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="accountDao"
class="org.springframework.samples.jpetstore.dao.jpa.JpaAccountDao">
<!-- additional collaborators and configuration for this bean go here -->
</bean>

<bean id="itemDao" class="org.springframework.samples.jpetstore.dao.jpa.JpaItemDao">
<!-- additional collaborators and configuration for this bean go here -->
</bean>

<!-- more bean definitions for data access objects go here -->

</beans>

从上面的例子可以看出,在xml配置中,

service层的时候由于引用的是对象,所以就用 id 配置service的具体名称 和 class 来制定service的实现类路径,然后在标签下用property标签用name来重新自定义引用的dao的名称,用ref来确定 service用到的dao的名称

dao层的时候,直接用id来制定dao层的名称用class来制定dao层的类路径。

拆分、组织Spring的XML配置元数据

当项目的xml文件过多的时候,为了让复杂的项目配置更清晰,设置了几个规则,核心目的就是拆分,组织xml配置元数据,而不是把所有的Bean定义写在一个XML,臃肿难以维护。

两种整合多XML配置的方式

1、创建ApplicationContext 容器 时,直接传入所有XML文件的路径,容器会自动合并所有Bean定义

1
2
3
4
5
// 示例:加载“服务层”和“数据层”的2个XML文件
ApplicationContext context = new ClassPathXmlApplicationContext(
"services.xml", // 服务层配置(如UserService、OrderService)
"daos.xml" // 数据层配置(如UserDao、OrderDao)
);

2、用<import/> 标签导入(更常用)

在一个 ’主XML配置文件‘ 中,通过<import resource="文件路径"/>标签导入其他XML,相当于把多个XML ‘合并’成一个

主配置文件(如applicationContext.xml

1
2
3
4
5
6
7
8
9
10
11
<beans>
<!-- 导入服务层配置:和主文件在同一目录/classpath位置 -->
<import resource="services.xml"/>
<!-- 导入资源配置:在主文件所在目录的“resources”子目录下 -->
<import resource="resources/messageSource.xml"/>
<!-- 前导斜线会被忽略,建议不写 -->
<import resource="/resources/themeSource.xml"/>

<!-- 主文件也可直接定义Bean -->
<bean id="systemConfig" class="com.example.SystemConfig"/>
</beans>

容器只用加载主xml,就能自动加载所有导入的子XML。

XML导入的路径规则

首先classpath是Java运行时JVM用来查找类文件.clssh和资源文件xml等的路径集合

1、spring项目默认会给一个resources文件,在该文件夹下默认就是classpath路径,也就是说如果把xml放在里面就直接写文件名就好了不用配置文件路径。

2、如果需要配置就需要配置一下项目路径,然后不要写前导斜线

用`$ {..}这样的占位符语法,在运行时读取JVM系统属性,动态拼接路径.

那么这个占位符的值可以从多个来源获取,优先级如下

1、JVM系统属性在启动命令的时候 java -Dxxx xxx.jar这样启动的时候

2、操作系统变量

3、.yml配置文件

在spring中需要配置占位符解析,在springboot中自动启用,无需配置。

用context命名空间开启注解扫描,在xml里面配置

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- 1. 头部声明context命名空间和对应的Schema -->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context" <!-- 声明context命名空间 -->
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context <!-- 引入context的Schema -->
https://www.springframework.org/schema/context/spring-context.xsd">

<!-- 2. 用context命名空间的标签开启注解扫描 -->
<!-- base-package:指定要扫描的包(容器会递归扫描这个包下所有带注解的类) -->
<context:component-scan base-package="com.example.service, com.example.dao"/>

</beans>

来开启扫描功能。

使用容器

现在注册好了,又配置扫描好了,这样所有的配置已经准备就绪开始使用了

1
2
3
4
5
6
7
8
// 创建和配置bean
ApplicationContext context = new ClassPathXmlApplicationContext("services.xml", "daos.xml");

// 检索配置的实例
PetStoreService service = context.getBean("petStore", PetStoreService.class);

// 使用配置的实例
List<String> userList = service.getUsernameList();

Bean概览

Bean包含的核心信息与容器的关系

容器的核心功能是管理Bean,容器内部通过BeanDefinition对象 存储Bean的定义信息,相当于Bean的数字档案,记录了创建和管理Bean的所有关键参数。

类别 核心属性 作用说明
Bean 的 “身份标识” Class、Name - Class:Bean 的全路径类名(如com.example.UserService),容器反射创建实例- Name:Bean 的唯一标识符(如 XML 的id),用于容器内引用
Bean 的 “行为规则” Scope、Lazy initialization mode - Scope:Bean 的作用域(如单例singleton、原型prototype),决定实例数量和生命周期- Lazy:是否懒加载(容器启动时不创建,首次使用时创建)
Bean 的 “依赖关系” Constructor arguments、Properties、Autowiring mode - 前两者:手动指定依赖(构造参数 / 属性注入)- Autowiring mode:自动装配规则(如按类型 / 名称注入依赖),减少手动配置
Bean 的 “生命周期” Initialization method、Destruction method - 初始化方法:Bean 实例化后执行(如资源初始化)- 销毁方法:Bean 销毁前执行(如资源释放)

1、Class 全路径类名,容器通过反射创建实例

2、Name 名称,Bean的唯一表示,用于容器内区分和引用

3、Scope 作用域 ,Bean的生命周期范围,比如单例singleton、原型prototype,决定容器创建Bean的实例数量和存活时间

4、构造参数/属性 ,一来注入的关键信息,指定Bean创建需要的参数或者属性值,以及依赖的其他Bean合作者

5、Autowiring mode自动装配模式 ,容器自动匹配并注入依赖的规则(如按类型、按名称),减少手动配置。

6、懒加载模式, 指定Bean 是否在容器启动时创建(默认立即创建),还是首次使用时才创建。

7、初始化/销毁方法, Bean生命周期回调方法,分别在Bean初始化完成后,销毁前执行

实例化Bean

Spring容器有三种方法实例化Bean

1、用构造函数实例化:容器通过反射调用构造函数直接创建 Bean,这类似于Java中的new 操作符。这个是根据bena中的id得到bean的名称还有bean中的class知道class类的位置,然后利用class.forname这个反射方法利用默认构造器在容器初始化的时候进行实例化。

2、用静态工厂方法进行实例化:容器调用类的静态工厂方法来创建Bean。这个时候需要在Bean标签里面用 factory -m 属性来制定工厂方法名称,并且这个方法必须是静态方法,因为实在初始化中开始创建的。

3、用实例工厂方法进行实例化:bean调用现有的Bean的非静态方法来创建新Bean。配置的时候,class属性留空,factory - bean 属性制定包含实例方法的Bean的名称,再用 factory - method 属性设置工厂方法名称

1
2
3
4
5
6
<bean id="serviceLocator" class="examples.DefaultServiceLocator">
<!-- inject any dependencies required by this locator bean -->
</bean>
<bean id="clientService"
factory - bean="serviceLocator"
factory - method="createClientServiceInstance"/>

4、确定Bean的运行时类型:由于Bean元数据定义中的类可能与实际运行时类型不同,且AOP代理可能会包装Bean实例,所以确定Bean运行时可以用BeanFactory.getType方法来获取Bean的实际运行时类型。

总结:静态方法可以不受IOC容器管控也能使用,然后方法三是利用IOC容器中已经实例化后的Bean的初始化方法来创建另一个Bean

总结

现在IOC容器已经被创建了,配置也配置好了,那么Bean是如何纳入容器管理,Bean又是如何初始化的呢?

  1. 容器启动(如ClassPathXmlApplicationContext初始化),触发refresh过程,加载并解析配置元数据;

  2. 将解析结果封装为 BeanDefinition,注册到BeanDefinitionRegistry(内部 Map);

  3. 容器根据 Bean 的作用域和懒加载设置,在合适的时机(启动时或getBean时)通过AbstractBeanFactory

    的实现类创建 Bean:

    • 先反射实例化原始对象;
    • 再进行属性填充(处理@Value@Autowired等依赖);
    • 执行初始化逻辑(InitializingBeaninit-method等);
  4. 单例 Bean 存入singletonObjects(单例池),原型 Bean 直接返回,最终通过getBean获取并使用

DI依赖注入

上面已经知道IOC容器如何创建实例Bean这里讲解如何注入属性。

两种DI的实现方式 构造器注入 和 Setter 注入

1、构造器注入

这种注入方式注入依赖后不可修改,一般为final修饰的,一般在需要强制注入的场景。

配置方式:无歧义的时候,直接按参数顺序配置

有歧义的时候,通过type 参数类型、index参数索引,从0开始、name参数名消除歧义

1
2
3
4
5
<!-- 用index消歧:第一个参数为int,第二个为String -->
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg index="0" value="7500000"/>
<constructor-arg index="1" value="42"/>
</bean>
1
2
3
4
5
6
7
8
9
10
public class ExampleBean {
private final int years; // 构造器注入后不可修改
private final String ultimateAnswer;

// 依赖通过构造器传入,由容器注入
public ExampleBean(int years, String ultimateAnswer) {
this.years = years;
this.ultimateAnswer = ultimateAnswer;
}
}

2、基于Setter的依赖注入

这个是容器先通过无参构造器/无参静态工厂创建Bean,再调用Setter方法注入依赖,依赖后续可以修改

通过<property>标签制定属性名和依赖,支持直接引用其他Bean 利用ref属性,或者设置基本类型值 value属性

1
2
3
4
5
<bean id="simpleMovieLister" class="examples.SimpleMovieLister">
<!-- 注入依赖的MovieFinder Bean -->
<property name="movieFinder" ref="movieFinder"/>
</bean>
<bean id="movieFinder" class="examples.MovieFinder"/>
1
2
3
4
5
6
7
8
public class SimpleMovieLister {
private MovieFinder movieFinder; // 可选依赖,可后续修改

// Setter方法供容器注入依赖
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
}
循环依赖流程

场景:Bean A构造器依赖 Bean B,Bean B构造器依赖 Bean A,形成循环。容器会检测到这种循环,抛出BeanCurrentlyInCreationException

依赖配置细节

1、xml中配置的value属性,Spring会自动将字符串转换为目标类型比如int 、 boolean

  • ```xml
    <property name="username" value="root"/>
    <property name="password" value="123456"/>
    
    </bean>
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18

    - 简化方式:使用`p命名空间`(属性式配置),如`p:username="root"`,减少嵌套标签。

    2、引用其他Bean 协作者

    - 通过`<ref bean="beanId"/>`引用容器中的其他 Bean,确保依赖的 Bean 先初始化。(这个是容器做的不需要手动排序,容器会先扫描BeanDefinition然后会根据依赖进行初始化的排序)
    - 父容器引用:使用`<ref parent="beanId"/>`引用父容器中的 Bean(适用于容器分层场景)。
    - 简化方式:`p命名空间`中用`p:属性名-ref="beanId"`(如`p:user-ref="userBean"`)。

    在Spring中,容器是可以分层的,你可以有多个ApplicationContext,他们之间形成父子关系

    ```java
    // 父容器:通常放公共组件(数据源、事务管理器等)
    ApplicationContext parent = new ClassPathXmlApplicationContext("parent-config.xml");

    // 子容器:有自己的 Bean,也可以访问父容器的 Bean
    ApplicationContext child = new ClassPathXmlApplicationContext("child-config.xml");
    child.setParent(parent); // 设置父子关系
1
2
3
4
5
6
7
8
9
10
<!-- 在 child-config.xml 中 -->
<bean id="userService" class="com.example.UserService">
<property name="dataSource" ref="dataSource"/>
<!-- ❌ 默认先在本容器找,找不到就报错 -->
</bean>

<bean id="userService" class="com.example.UserService">
<property name="dataSource" parent="dataSource"/>
<!-- ✅ 明确表示:去父容器里找这个 Bean -->
</bean>

⚠️ 注意:refparent 是不同的:

  • ref="xxx":先在当前容器找,找不到再去父容器找(默认行为)
  • parent="xxx"强制只在父容器中查找,不在本地容器中找

3、内部Bean

  • <property><constructor-arg>内部直接定义 Bean(无需id),仅供当前 Bean 使用,无法被外部引用。

  • 示例:

    1
    2
    3
    4
    5
    6
    7
    <bean id="outerBean" class="...">
    <property name="innerBean">
    <bean class="..."> <!-- 内部Bean -->
    <property name="name" value="test"/>
    </bean>
    </property>
    </bean>

4、集合类型(List、Set、Map、Properties)

  • 分别使用<list><set><map><props>标签配置,支持嵌套值或 Bean 引用。

  • 示例:

    1
    2
    3
    4
    5
    6
    <property name="someMap">
    <map>
    <entry key="key1" value="value1"/>
    <entry key="key2" value-ref="bean2"/>
    </map>
    </property>
  • 集合合并:子 Bean 可通过merge="true"继承父 Bean 的集合属性,并覆盖重复键值。

5、null与空字符串

  • 空字符串:<property name="email" value=""/>
  • null 值:<property name="email"><null/></property>

6、构造函数参数的快捷配置

  • 使用c命名空间通过属性指定构造函数参数,如c:username="root"(按名称)或c:_0="root"(按索引,_避免 XML 语法冲突)。

7、依赖关系的特殊处理

  • 1、通过depends-on 强制初始化顺序
    • 当 Bean A 依赖 Bean B 的初始化(但无需注入 B),用depends-on确保 B 先初始化。
    • 示例:<bean id="A" class="..." depends-on="B, C"/>(B 和 C 先于 A 初始化)。
    • 单例 Bean 中,depends-on还会控制销毁顺序(依赖的 Bean 后销毁)。
  • 2、懒加载 lazy-init
    • 默认情况下,单例 Bean 在容器启动时预初始化;设置lazy-init="true"后,首次调用getBean()时才初始化。
    • 容器级配置:<beans default-lazy-init="true">(所有 Bean 默认懒加载)。
    • 注意:若懒加载 Bean 被非懒加载 Bean 依赖,仍会在容器启动时初始化。

8、自动注入

Spring可以自动解析Bean之间的依赖关系,减少显示配置,拥有4种模式

模式 说明
no(默认) 不自动注入,需显式通过<ref>配置。
byName 按属性名匹配容器中的 Bean(如setUser(...)匹配名为user的 Bean)。
byType 按属性类型匹配容器中的 Bean(若存在多个同类型 Bean,抛出异常)。
constructor 类似byType,但用于构造函数参数(无匹配类型则抛异常)。
  • 限制:无法自动注入基本类型、String 等;显式配置(如<property>)优先级高于自动注入。
  • 排除自动注入:通过autowire-candidate="false"标记 Bean,使其不参与自动注入候选。
  • 优先候选:通过primary="true"标记 Bean,在多匹配时优先被选中。
1
<bean id="computer" class="Computer" autowire="byType"/>

image-20251026154320663

在实际开发中通常用注解@Resource来配置

9、方法注入

这个解决的是单例Bean依赖原型Bean,假设一个单例Bean里面依赖了一个task是个原型Bean,然后每次都运行打印task的id,但是由于单例Bean是单例的那么这里的task只会被注入一次,即使task是原型模式,也只能拿到第一个实例,那么这时候Spring会使用CGLIB动态生成子类,重写抽象方法。(spring里面已经集成了)

1
2
3
4
5
6
7
8
9
10
public abstract class ServiceA {

// 定义一个抽象方法,返回你要的原型 Bean
public abstract Task createTask();

public void doWork() {
Task task = createTask(); // 每次调用都返回新实例!
System.out.println("当前任务ID: " + task.id);
}
}
1
2
3
4
5
6
<!-- XML 配置 -->
<bean id="serviceA" class="com.example.ServiceA"/>
<bean id="task" class="com.example.Task" scope="prototype"/>

<!-- 关键:告诉 Spring,createTask() 方法要被动态实现 -->
<lookup-method name="createTask" bean="task"/>
1
2
3
4
5
6
7
8
9
10
11
@Component
public abstract class ServiceA {

@Lookup // 标记这个方法应该返回一个原型 Bean
public abstract Task createTask();

public void doWork() {
Task task = createTask(); // 每次都是新的!
System.out.println("当前任务ID: " + task.id");
}
}
Bean的作用域

Bean定义相当于创建对象的配方,而 作用域 Scope 控制从改配方创建的对象实例的生命周期和可见范围。

作用域也是通过配置制定,无需在Java级别硬编码

Spring支持6种作用域

作用域 说明 适用场景
singleton (默认)每个 Spring IoC 容器中,Bean 定义对应唯一实例,所有请求共享此实例。 无状态 Bean(如工具类、DAO)
prototype 每次请求(注入或getBean())都会创建新实例。 有状态 Bean(如用户会话相关对象)
request 每个 HTTP 请求对应一个实例,请求结束后销毁。 Web 应用,与单次请求相关的 Bean
session 每个 HTTP Session 对应一个实例,会话结束后销毁。 Web 应用,与用户会话相关的 Bean
application 绑定到 ServletContext 生命周期,整个 Web 应用共享一个实例。 Web 应用全局配置(类似 ServletContext)
websocket 绑定到 WebSocket 会话生命周期,适用于 STOMP 协议的 WebSocket 应用。 实时通信场景

1、singleton作用域,Spring单例是 每个容器每个Bean,而饿汉式单例是每个类加载器。

singleton是根据缓存机制实现的,因为每次初始化的时候,会把实例存入到一个map里面后续所有的请求直接从缓存中直接获取,不用重复创建。

饿汉式单例,是java设计模式,通过类加载时直接初始化静态实例,保证每个jvm终只有一个实例

1
2
3
4
5
6
public class Singleton {
// 类加载时直接初始化,饿汉式
private static final Singleton INSTANCE = new Singleton();
private Singleton() {} // 私有构造器阻止外部创建
public static Singleton getInstance() { return INSTANCE; }
}

2、prototype 是每次请求创建新势力,容器仅负责实例化和配置,不管理后续生命周期

这个就容器仅负责实例化,依赖注入和初始化,不管理后续生命周期

3、Web相关作用域(request / session / application)

Web相关的Scope的Bean需要感知当前的HTTP请求 / 会话,但Spring容器本身运行在Servlet容器中,而HTTP请求是由Servlet容器(如Tomcat)的线程处理的。

要使用这些需要现在Web环境中注册 RequestContextListener 或 RequestContextFilter,确保HTTP请求与线程绑定。

  • web.xml配置监听器
1
2
3
<listener>
<listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>
</listener>
  • 或配置过滤器
1
2
3
4
5
6
7
8
<filter>
<filter-name>requestContextFilter</filter-name>
<filter-class>org.springframework.web.filter.RequestContextFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>requestContextFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
  • 然后用注解配置

@RequestScope、@SessionScope、@ApplicationScope分别对应三种Web作用域:

1
2
3
@RequestScope
@Component
public class LoginAction { ... }

后面还可以自定义Scope,但是由于太麻烦这里不做解释,用得少,后续要用可以看官方文档

Bean的继承

Bean的继承允许子Bean继承父Bean定义的配置信息,仅需要覆盖或者补充差异部分,减少重复配置。但是Bean的继承是部分的继承不是全部继承,比如依赖是不会继承的,比如BeanA里面依赖了BeanB然后BeanC继承了BeanA,那么BeanC里面不会继承BeanB。然后scope的singleton也是不会继承的。

可继承
scope 包括 singletonprototype
构造参数 <constructor-arg>
属性值 <property>
方法重写 <lookup-method>
不继承
depends-on 子 Bean 必须显式声明依赖
lazy-init 默认 false,需显式设置
autowire 自动注入模式不继承
dependency-check 已过时,但也不继承
init-method / destroy-method 不继承,但可覆盖
factory-method 不继承,但可覆盖

如果用xml方式声明 就用 parent属性指定父Bean

还有一些抽象Bean,通过abstract = “ture” 标记,抽象Bean无法通过getBean方法获取实例,也不能被其他Bean引用为依赖。容器实例化单例的时候会忽略抽象Bean

容器扩展点

容器的扩展点是指实现特定的接口,对容器Bean的生命周期期间进行一些自定义干预的机制。

BeanPostProcessor 接口

这个是容器的扩展接口,定义了回调方法,作用于Bean实例,在容器完成Bean的实例化、依赖注入和初始化前后执行。

这个接口里面有两个核心的方法

postProcessBeforeInitalization 方法:在Bean的初始化方法执行之前调用

postProcessAfterInitialization方法:在Bean的初始化方法执行之后调用

这个接口的使用范围是对所在的容器内的Bean生效

执行顺序:多个BeanPostProcessor可以通过实现Ordered接口,设置order属性控制执行顺序,值越小越先执行,如果没实现,就会按注册顺序执行。

与AOP自动代理的关系

BeanPostProcessor实例及其直接引用的Bean不参与AOP自动代理(因为自动代理本身也是BeanPostProcessor,不过BeanPostProcessor启动阶段更早)

首先BeanPostProcessor和AOP都能对Bean进行增强,BeanPostProcessor一般用于容器启动过程中的早起干预

AOP一般用于Bean实例化完成之后,运用时调用方法时通过代理出发。

注册方式

  • 自动检测:ApplicationContext会自动扫描并注册配置中实现BeanPostProcessor接口的Bean,如果是用@Bean声明,返回类型需要明确为BeanPostProcessor或者其实现类。(如果@Bean方法的返回类型没有明确声明为BeanPostProcessor或其实现类,Spring 容器可能无法将其识别为BeanPostProcessor,从而当作普通 Bean 处理,导致其失去对其他 Bean 生命周期的干预能力。)
  • 编程式注册:通过ConfigurableBeanFactory.addBeanPostProcessor方法手动注册,适用于条件逻辑或跨容器复制场景。这种注册不遵循Ordered接口,按注册顺序执行,且优先于自动检测的实例。
BeanFactoryPostProcessor接口

这个的核心作用是在容器实例化任何Bean之前,对Bean的配置元数据进行修改或者增强。比如修改类名、属性值、依赖关系、作用于、初始化方法等。这个是在初始化之前修改元数据,而BeanPostProcessor在初始化之前进行一些操作,而这个接口是初始化之前修改Bean

FactoryBean接口

如果有很复杂的初始化逻辑,最好用java来表达,而不是用很长的xml来表达,你可以创建自己的FactoryBean,将复杂的初始化写到该类中,然后将Factory插入到容器中

然后这个接口提供三个方法:

  • T getObject(): 返回本工厂创建的对象的一个实例。该实例可能会被共享,这取决于该工厂是返回singleton还是prototype。
  • boolean isSingleton(): 如果这个 FactoryBean 返回 singleton,则返回 true,否则返回 false。这个方法的默认实现会返回 true
  • Class<?> getObjectType(): 返回由 getObject() 方法返回的对象类型,如果事先不知道类型,则返回 null

获取实例的特殊规则

  • 调用 getBean("beanId") 时,容器返回 FactoryBean 通过 getObject() 生成的目标对象
  • 若要获取 FactoryBean 自身实例,需在 ID 前加 &,即 getBean("&beanId")

获取目标对象,是获得工厂的产物,获取自身实例是获取工厂

基于注解的容器配置

在配置Spring时,注解是否比XML好?这个视情况而定,都可以

如果一个Bean的同一个属性即有注解又有xml那么最终会是xml中的配置

@Autowired 按类型

注意:无论是 @Autowired 注入字段、setter 方法、普通方法还是构造函数,其最终效果都等价于在 XML 中配置 <bean> 及其依赖关系 —— 它们只是“声明依赖”的不同写法,底层目标一致:让 IoC 容器完成对象创建和依赖装配。

4种注入方式,这个可作用于构造函数、setter 方法、普通方法、字段

这里理清一下:

  1. 扫描所有 <bean> 标签 → 存入一个类似 Map<String, BeanDefinition> 的注册表。
  2. 遍历每个 BeanDefinition:
    • 使用反射 Class.forName("...") 加载类。
    • 调用合适的构造函数(可能是无参也可能是有参)创建实例。
    • 如果有 <property><constructor-arg>,就通过 setter 或构造函数传参进行依赖注入。
  3. 最终把所有 Bean 放进容器(单例池),等待使用。

在springboot框架中会有个service层然后有个impl层,这个构造函数的注入是写在impl层里面的,其效果就是跟在xml中写依赖的效果一样。

  1. 扫描 @Component, @Service, @Repository, @Controller 等注解类 → 注册为 BeanDefinition。
  2. 分析这些类的构造函数、字段、方法上的 @Autowired@Value 等注解。
  3. 创建 Bean 实例时:
    • 若只有一个构造函数 → 直接调用它(传入匹配的 Bean 作为参数)
    • 若多个构造函数且有 @Autowired → 选那个标注的
    • 若没有构造函数注入 → 先用无参构造器创建对象,再对字段或 setter 注入
  4. 最终完成依赖装配。

所以才会有构造函数注入等4中注入方式。

注入方式 示例 特点
1. 构造函数注入 public UserService(UserRepo repo) 推荐!不可变、强依赖、易测试
2. Setter 注入 @Autowired public void setUserRepo(...) 可选依赖,灵活性高,不推荐
3. 字段注入 @Autowired private UserRepo userRepo; 简洁但破坏封装,难以单元测试,已不推荐
4. 普通方法注入 @Autowired public void init(Bean b) { ... } 少见,用于特殊逻辑
解决类型匹配不唯一问题

1、@Primary 指定默认优先Bean

当同一类型有多个 Bean 时,@Primary标记的 Bean 会成为 “默认首选”,无需额外指定名字,适合 “大部分场景用同一个 Bean” 的情况。

核心作用

  • 解决 “单值依赖(如单个字段、单个参数)的类型匹配冲突”:若按类型找到多个 Bean,自动选择带@Primary的 Bean。
  • 优先级低于@Qualifier@Qualifier更精准,@Primary是 “默认兜底”)。

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Configuration
public class MovieConfig {
// 标记为@Primary,成为默认首选
@Bean
@Primary
public MovieCatalog firstMovieCatalog() {
return new FirstMovieCatalog();
}

// 同一类型的另一个Bean,无@Primary
@Bean
public MovieCatalog secondMovieCatalog() {
return new SecondMovieCatalog();
}
}

// 注入时自动选择firstMovieCatalog(因为带@Primary)
public class MovieRecommender {
@Autowired
private MovieCatalog movieCatalog; // 实际是firstMovieCatalog
}

2、@Qualifier 精准按标识筛选Bean

当需要更精细地选择依赖(比如同一类型有多个 Bean,且无明显 “默认首选”)时,@Qualifier通过 “自定义标识” 匹配 Bean,解决 “@Primary无法区分多个非默认 Bean” 的问题。

核心作用:给Bean和注入添加统一标识,然后注入的时候用标识名注入,区别于按名称和按类型

在使用@Qualifier的时候,用@Autowired注入的时候,会先按类型筛选Bean然后用@Qualifier从标识中选中唯一目标。

3、@Genre 更加多功能标识注解

@Qualifier是@Genre的父类,但是由于@Qualifier使用纯字符串标识的痛点,并且功能稀少的问题。

所以用@Genre注解,可以使用枚举参数防错(可以不用字符串防止拼错),多条件扩展(可以用多个标识来区分)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public enum MovieGenre { ACTION, COMEDY, DRAMA }
// 1. 使用时直接选枚举,不会拼错
@Autowired
@Genre(MovieGenre.ACTION) // 编译时就有提示,不会拼错
private MovieCatalog actionCatalog;

// 2. 给Bean加多条件标识
@Bean
@Genre(type = MovieGenre.ACTION, format = "BLURAY")
public MovieCatalog actionBluRayCatalog() {
return new ActionMovieCatalog();
}

// 3. 注入时精准匹配多条件
@Autowired
@Genre(type = MovieGenre.ACTION, format = "BLURAY")
private MovieCatalog actionBluRayCatalog;

4、利用泛型 作为隐含的限定符

在Bean实现 泛型接口的时候,Spring会将泛型作为隐含的限定符,不需要额外加@Qualifier,可以通过泛型精准匹配依赖。

如果用@Autowired注入,会先按照接口类型筛选,然后再按照泛型类型进一步限定。

案例:

1、先配置泛型接口和实现类

1
2
3
4
5
6
7
// 泛型接口
public interface Store<T> {}

// 实现类1:泛型为String
public class StringStore implements Store<String> {}
// 实现类2:泛型为Integer
public class IntegerStore implements Store<Integer> {}

2、配置Bean,利用注解配置xml

1
2
3
4
5
6
7
@Configuration
public class MyConfiguration {
@Bean
public StringStore stringStore() { return new StringStore(); }
@Bean
public IntegerStore integerStore() { return new IntegerStore(); }
}

3、注入时通过泛型自动匹配

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Service {
// 泛型<String>匹配stringStore(Store<String>类型)
@Autowired
private Store<String> s1;

// 泛型<Integer>匹配integerStore(Store<Integer>类型)
@Autowired
private Store<Integer> s2;

// 注入所有Store<Integer>类型的Bean(集合注入同样支持)
@Autowired
private List<Store<Integer>> integerStores;
}

5、CustomAutowireConfigurer

这个是Spring提供的一个BeanFactoryPostProcessor 实现类

允许将未被 @Qualifier 注解标注的自定义注解 注册为合法的注入限定符

首先要了解自定义注解

自定义注解是用@interface关键字声明的特殊接口,这个类似于给jvm添加配置,让jvm认得我们的自定义注解

1
2
3
4
5
6
// 定义一个自定义注解
@Target({ElementType.FIELD, ElementType.PARAMETER}) // 注解可用于字段和参数
@Retention(RetentionPolicy.RUNTIME) // 运行时保留,允许反射解析
public @interface MyQualifier {
String value(); // 注解的属性(类似方法定义,用于传递参数)
}
  • @Target:指定注解能标注在什么地方(如类、字段、方法)。
  • @Retention:指定注解的生命周期(RUNTIME表示运行时有效,Spring 注入时需要这个)。
  • 属性:像value()这样的 “抽象方法”,使用时可传值(如@MyQualifier("user1"))。

那么我们现在使用自定义注解,然后如果我们想要自己的自定义注解在Spring自动注入中作为筛选标识,类似@Qualifier,就需要CustomAutowireConfigurer。

举例子:假设有多个DataSource类型的Bean(主库、从库、日志库),用自己的@DataSourceType标识

1
2
3
4
5
6
// 定义自定义注解:标记数据源类型
@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSourceType {
String value(); // 可选值:"master"(主库)、"slave"(从库)、"log"(日志库)
}

然后给Bean加标识也就是添加xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Configuration
public class DataSourceConfig {
// 新增:配置 CustomAutowireConfigurer,注册 @DataSourceType
@Bean
public CustomAutowireConfigurer customAutowireConfigurer() {
CustomAutowireConfigurer configurer = new CustomAutowireConfigurer();
Set<String> qualifierTypes = new HashSet<>();
// 传入 @DataSourceType 的全类名(需替换为你的实际包路径)
qualifierTypes.add("com.example.DataSourceType");
configurer.setCustomQualifierTypes(qualifierTypes);
return configurer;
}

// 原有的数据源 Bean 定义不变
@Bean
@DataSourceType("master")
public DataSource masterDataSource() {
// 示例:返回主库数据源(如 HikariDataSource)
HikariDataSource ds = new HikariDataSource();
ds.setJdbcUrl("jdbc:mysql://master:3306/db");
return ds;
}

@Bean
@DataSourceType("slave")
public DataSource slaveDataSource() {
HikariDataSource ds = new HikariDataSource();
ds.setJdbcUrl("jdbc:mysql://slave:3306/db");
return ds;
}
}

最后注入筛选

1
2
3
4
5
6
7
@Service
public class UserService {
// 注入主库数据源
@Autowired
@DataSourceType("master")
private DataSource dataSource;
}
@Resource 按名字

Spring支持在字段或者Bean属性设置方法上使用@Resource 进行注入。

@Resource 需要一个name属性,Spring将该值解释为要注入的Bean名称,如果没有明确指定名字,默认的名字来自于字段名或setter方法。如果是一个字段就采用字段名,如果是setter方法,则采用Bean的属性名。

1
2
3
4
5
public class MovieRecommender {
// 未指定name,默认按字段名"customerPreferenceDao"匹配Bean
@Resource
private CustomerPreferenceDao customerPreferenceDao;
}
  • 匹配逻辑:先找 idcustomerPreferenceDao 的 Bean(名称匹配);
  • 兜底逻辑:若没找到同名 Bean,再找 CustomerPreferenceDao 类型的 Bean,且优先选带 @Primary 的(类型兜底)。

@Resource 用在 setter 方法上且未指定 name 时,Spring 会从 setter 方法名中提取 “属性名” 作为默认 name(规则:去掉 set 前缀,首字母小写)。

1
2
3
4
5
6
7
8
9
public class SimpleMovieLister {
private MovieFinder movieFinder;

// 未指定name,从方法名setMovieFinder提取属性名"movieFinder"
@Resource
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
}
@Value 属性注入

@Value主要用于将外部配置 如yml文件、系统属性、动态计算值 注入到Bean的字段或者构造函数参数中,是 Spring读取外部配置的核心注解

1、注入外部 properties 配置 最常用

通过 ${配置键名} 语法读取外部 properties/yml 文件中的配置,需配合 @PropertySource 指定配置文件路径(Spring Boot 可省略,默认读取 application.properties/yml)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 1. 配置类:指定配置文件(非Spring Boot需加,Spring Boot可省略)
@Configuration
@PropertySource("classpath:application.properties") // 读取类路径下的配置文件
public class AppConfig {}

// 2. 外部配置文件(application.properties)
catalog.name=MovieCatalog

// 3. 注入配置
@Component
public class MovieRecommender {
// 注入 catalog.name 对应的值(MovieCatalog)
private final String catalog;
public MovieRecommender(@Value("${catalog.name}") String catalog) {
this.catalog = catalog;
}
}

2、设置默认值

通过 ${配置键名:默认值} 语法,当配置键不存在时,自动使用默认值(无需额外配置,Spring 原生支持)。

1
2
3
4
5
6
7
@Component
public class MovieRecommender {
// 若 catalog.name 不存在,使用默认值 "defaultCatalog"
public MovieRecommender(@Value("${catalog.name:defaultCatalog}") String catalog) {
this.catalog = catalog;
}
}

3、自动类型转换

Spring 内置 ConversionService,支持将配置文件中的 String 类型值自动转换为 基本类型(int、boolean 等)、数组、集合 等,无需手动转换。

1
2
3
4
5
6
7
8
9
10
@Component
public class MovieRecommender {
// 1. 自动转换为 int(配置文件:max.movies=50)
@Value("${max.movies}")
private int maxMovies;

// 2. 自动转换为 String 数组(配置文件:genres=Action,Comedy)
@Value("${genres}")
private String[] genres;
}

4、动态计算值 SpEL表达式

通过 #{SpEL表达式} 语法支持动态计算,可读取系统属性、拼接字符串、创建复杂数据结构(Map、List 等),灵活性极高。

1
2
3
4
5
6
7
8
9
10
@Component
public class MovieRecommender {
// 1. 读取系统属性(user.name)并拼接字符串(如 "adminCatalog")
@Value("#{systemProperties['user.name'] + 'Catalog'}")
private String userCatalog;

// 2. 动态创建 Map(无需外部配置,直接在注解中定义)
@Value("#{{'Thriller': 100, 'Comedy': 300}}")
private Map<String, Integer> movieCountMap;
}
@PostConstruct 和 @PreDestory

首先明确 这两个是注解 不是类,他们的作用是标记方法,让Spring在特定的生命周期节点自动调用方法。

1、@PostConstruct: Bean初始化完成后执行

触发时机 Bean 实例化(反射创建对象)、依赖注入(如 @Autowired 注入完成)之后,初始化方法(如 @Bean(initMethod="xxx"))执行之前。

核心用途 执行Bean的初始化后准备工作,比如初始化缓存、加载配置等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class CachingMovieLister {
private List<Movie> movieCache;

// 依赖注入(注入电影数据源)
@Autowired
private MovieDataSource dataSource;

// Bean初始化完成后,自动调用此方法填充缓存
@PostConstruct
public void populateMovieCache() {
// 此时 dataSource 已注入,可安全使用
this.movieCache = dataSource.loadAllMovies();
System.out.println("缓存初始化完成,共加载" + movieCache.size() + "部电影");
}
}

这里加深一下理解,Spring容器给了很多个属性配置的时机,一个是通过反射创建对象,然后用依赖注入的时候,然后再到@PostConstruct,最后再是初始化,然后才会把Bean开始使用,这是spring容器的生命周期固定流程。

2、@PreDestory: Bean销毁前执行

触发时机: Bean被销毁前 比如Spring容器关闭时,单例Bean会触发,原型Bean由JVM垃圾回收,不触发

核心用途: 执行Bean的销毁前的清理工作,比如关闭连接、释放资源、清空缓存等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class CachingMovieLister {
private List<Movie> movieCache;
private Connection dbConnection;

// 初始化时建立数据库连接
@PostConstruct
public void initConnection() throws SQLException {
this.dbConnection = DriverManager.getConnection("jdbc:mysql://xxx");
}

// Bean销毁前,自动调用此方法释放资源
@PreDestroy
public void clearMovieCache() throws SQLException {
// 清空缓存
this.movieCache.clear();
// 关闭数据库连接(避免资源泄漏)
if (dbConnection != null && !dbConnection.isClosed()) {
dbConnection.close();
}
System.out.println("资源清理完成");
}
}

配置扫描和管理

之前讲到很多Spring框架 对Bean的操作,比如属性注入,初始化等。

这里将Spring框架如何扫描到标记以及如何配置

1、核心机制:Classpath扫描

传统配置需要再xml中显式定义bean,而Classpath扫描允许Spring 自动扫描项目类路径下的特定类,将符合条件的类自动注册为容器中的Bean,无需手动编写Bean定义。

需要通过注解 @Component及其衍生注解 标记类。

通过xml或者JavaConfig @ComponentScan开启扫描

1
2
3
4
// JavaConfig 开启扫描,扫描 com.example 包下的组件
@Configuration
@ComponentScan(basePackages = "com.example")
public class AppConfig {}
@Component注解及其衍生
注解 核心作用 适用层级 / 场景 特殊附加功能
@Component 通用组件标记,适用于所有 Spring 管理的类 无明确层级的通用组件 无特殊附加功能,仅作为扫描标记
@Repository 标记 “数据访问层” 组件(如 DAO/Repository) 持久层(与数据库交互的类) 自动触发 “数据库异常翻译”(将 JDBC 异常转为 Spring 统一异常)
@Service 标记 “业务逻辑层” 组件 服务层(处理业务逻辑的类) 无特殊附加功能,但语义明确,便于代码分层和切面关联(如事务切面)
@Controller 标记 “表现层” 组件(如 Spring MVC 控制器) 控制层(接收请求、返回响应的类) Spring MVC 自动识别为控制器,支持请求映射(如 @RequestMapping
2、元注解和组合注解

Spring中 元注解是注解的底层依赖

元注解就是可以标注在其他注解上的注解,用于给目标注解赋予基础功能。

示例:

1
2
3
4
5
6
7
@Target(ElementType.TYPE)   // 元注解:指定@Service可标注在类上
@Retention(RetentionPolicy.RUNTIME) // 元注解:指定@Service在运行时有效
@Documented // 元注解:指定@Service会被javadoc文档记录
@Component // 元注解:让@Service拥有@Component的“组件标记”功能
public @interface Service {
// ...
}

具体这些元注解就是spring框架已经帮我们实现好的标记,有特定的功能,就跟jdk一样已经帮我们实现好了,那么我们只需要记住常用的元注解即可

了解完元注解之后,我们可以自定义注解,整合多个元注解然后自定义注解或者使用spring提供的组合注解

整合多个元注解:例如 Spring MVC 的 @RestController = @Controller(标记控制器)+ @ResponseBody(返回值直接转响应体),标注 @RestController 即可同时拥有两个注解的功能。

扫描机制

上面提到了元注解, spring能通过组件扫描自动检测元注解类,无需手动定义Bean,跟之前@Bean自己全程手动控制Bean的生命周期不同。

这里总结一下spring框架中两种核心的Bean注册方式

方式 @Component + 扫描 @Bean
谁负责创建 Bean? 类自己声明“我是组件” 配置类主动声明“我来提供这个 Bean”
注册方式 自动发现(被动) 主动定义(手动)
控制权 分散在各个类上 集中在配置类中
适合对象类型 自己写的业务类(Service, Repository 等) 第三方库的类或复杂初始化逻辑的对象

2中开启扫描的方式

1、通过配置类制定扫描包

@Configuration 类上加 @ComponentScan,指定扫描的包(basePackages 属性,可简化为直接写包名):

1
2
3
@Configuration
@ComponentScan("org.example") // 扫描org.example包及其子包下的组件
public class AppConfig {}

2、通过xml配置

1
<context:component-scan base-package="org.example"/>
3、扫描过滤

上面讲到扫描机制是通过定义componentScan扫描元注解和定制xml定义扫描范围。

那么这里是通过 Filter 灵活修改扫描范围,排除或者过滤,或者突破默认扫描范围进行扩大。

配置也是两种一种是xml,一种是通过componentScan的属性配置

  • JavaConfig:在 @ComponentScan 中通过 includeFilters(包含)、excludeFilters(排除)属性配置;
  • XML:在 <context:component-scan> 中通过 <context:include-filter><context:exclude-filter> 子标签配置。

Spring提供5种过滤类型FilterType

Filter 类型(FilterType) 示例表达式 匹配逻辑(如何判断类是否符合规则)
注解(ANNOTATION,默认) org.springframework.stereotype.Repository 类上是否有指定注解(如排除所有带 @Repository 的类)
可指定(ASSIGNABLE_TYPE) org.example.MovieFinder 类是否继承 / 实现了指定类 / 接口(如只包含实现 MovieFinder 的类)
AspectJ(ASPECTJ) org.example..*Service+ 类名是否匹配 AspectJ 表达式(如包含 org.example 包下所有以 Service 结尾的类)
正则(REGEX) .*Stub.*Repository 类名是否匹配正则表达式(如包含类名含 Stub 且以 Repository 结尾的类)
自定义(CUSTOM) org.example.MyTypeFilter 自定义 TypeFilter 实现类,按自定义逻辑判断(如按类的包路径 + 注解组合筛选)

示例

1
2
3
4
5
6
7
@Configuration
@ComponentScan(
basePackages = "org.example",
includeFilters = @Filter(type = FilterType.REGEX, pattern = ".*Stub.*Repository"), // 正则匹配包含
excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = Repository.class) // 注解匹配排除
)
public class AppConfig {}
在组件中定义Bean元数据

在普通 @Component 类中使用 @Bean 注解的场景

首先@Component是标记一个类为组件,然后Spring扫描就会创建他的实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// @Component 表示:我是一个需要被Spring管理的组件
@Component
public class UserService {

// 这个类本身就是一个Bean,Spring会创建UserService实例
public void doBusinessLogic() {
System.out.println("处理用户业务逻辑");
}
}

// Spring启动时:
// 1. 扫描到 @Component
// 2. 通过反射创建 UserService 实例
// 3. 放入容器,Bean名称默认为 "userService"

@Configuration 中 使用@Bean

那么@Configura 是标记一个类为配置类,专门用于定义如何创建其他Bean。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// @Configuration 表示:我是一个配置类,专门定义Bean的创建方式
@Configuration
public class AppConfig {

// 这个类本身也是Bean,但主要作用是定义其他Bean
@Bean
public DataSource dataSource() {
// 定义如何创建DataSource这个Bean
return new HikariDataSource();
}

@Bean
public JdbcTemplate jdbcTemplate() {
// 定义如何创建JdbcTemplate,并注入dataSource
return new JdbcTemplate(dataSource());
}
}

// 注意:这里调用dataSource()会被Spring拦截,确保返回单例

那么@Component和@Configuration的区别是什么呢?

对比维度 @Component @Configuration
中文含义 组件 配置类
本质 被动注册:这个类自己就是一个 Bean 主动注册:这个类用来定义别的 Bean
使用位置 普通业务类上(Service, DAO 等) 配置类上(通常包含 @Bean 方法)
是否可包含 @Bean 方法 可以(但不推荐) ✅ 推荐且标准用法
内部方法调用是否走代理? ✅ 是(关键区别!)
底层实现 普通 Bean 实例化 CGLIB 动态代理增强
典型用途 业务逻辑组件 数据源、第三方库集成、自定义 Bean 创建

举例:我们要创建两个 Bean:dataSourcejdbcTemplate

1
2
3
4
5
6
7
8
9
10
11
12
13
@Component
public class DatabaseConfig {

@Bean
public DataSource dataSource() {
return new HikariDataSource();
}

@Bean
public JdbcTemplate jdbcTemplate() {
return new JdbcTemplate(dataSource()); // ❌ 问题在这里!
}
}

问题来了:jdbcTemplate() 方法里调用了 dataSource(),这会创建一个新的 DataSource 实例,而不是使用 Spring 容器中的那个!

为什么?因为 @Component 类没有被代理,方法调用就是普通 Java 方法调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class DatabaseConfig {

@Bean
public DataSource dataSource() {
return new HikariDataSource();
}

@Bean
public JdbcTemplate jdbcTemplate() {
return new JdbcTemplate(dataSource()); // ✅ 正确:返回的是容器里的 Bean
}
}
  • Spring 会通过 CGLIB 动态代理 创建 DatabaseConfig 的子类
  • 所有 @Bean 方法都会被拦截
  • 再次调用 dataSource() 时,会从容器中获取已存在的 Bean,而不是重新 new 一个

@Configuration 的底层原理

当 Spring 遇到 @Configuration 类时:

  1. 使用 CGLIB 创建一个该类的子类代理

  2. 重写所有 @Bean 方法

  3. 在调用@Bean

    方法时:

    • 先检查容器中是否有该 Bean
    • 如果有 → 返回已有实例(单例)
    • 如果没有 → 执行方法创建并注册到容器

为什么用@Configuration或者@Component 而需要用@Bean

1、使用第三方类为Bean

@Component类等注解需要直接标注在类上,如果使用 第三方库的类 只能通过@Bean 方法手动创建实例并注册到容器

2、需要定制Bean的创建逻辑

@Component 注册的Bean,实例化逻辑由类的构造函数决定,无法插入复杂的创建逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Component
public class DataSourceConfig {
@Value("${spring.profiles.active}")
private String profile;

@Bean
public DataSource dataSource() {
if ("dev".equals(profile)) {
// 开发环境用H2内存库
return new H2DataSource();
} else {
// 生产环境用MySQL
HikariDataSource ds = new HikariDataSource();
ds.setJdbcUrl("jdbc:mysql://xxx");
return ds;
}
}
}

当需要利用组件的属性和方法的时候,可以将@Bean放到同一组件中也就是@Configuration中加@Bean这样就不用重复创建了

4、自动扫描组件和命名规则

Spring 自动扫描组件时如何给 Bean 命名 —— 默认按 “注解指定 name 或类名首字母小写” 生成,可通过自定义 BeanNameGenerator 改变规则,冲突时可用全类名命名,同时建议显式指定需被引用的 Bean 名称

1、手动指定name

示例:@Service("myMovieLister") 标注的类,Bean 名称为 myMovieLister

2、默认类名首字母小写

示例:MovieFinderImpl 类(无 name 指定),Bean 名称为 movieFinderImpl

3、自定义命名策略

  • JavaConfig:通过

    @ComponentScan nameGenerator

    属性指定全类名;

    1
    2
    3
    @Configuration
    @ComponentScan(basePackages = "org.example", nameGenerator = MyNameGenerator.class)
    public class AppConfig {}
  • XML:通过 <context:component-scan> name-generator

    属性指定全类名;

    1
    <context:component-scan base-package="org.example" name-generator="org.example.MyNameGenerator"/>

4、使用全限定类名作为Bean名称

自动扫描组件的作用域

自动扫描组件也需要声明他的生命周期

自动扫描的组件默认是单例(singleton),可通过 @Scope 指定其他作用域;也可自定义作用域解析器;对于非单例作用域的 Bean,需配置作用域代理解决依赖注入问题,确保在不同作用域下正常使用

1、那么如果是使用@Scope 注解 可以立刻指定该Bean的作用域,但是无法精细。

2、如果想要更加精细的操作,需要 自定义 作用域解析器 来决定Bean的作用域

  1. 实现 ScopeMetadataResolver 接口(需提供无参构造函数),编写自定义作用域解析逻辑;

  2. 在扫描配置中指定该解析器:

    • JavaConfig:通过@ComponentScan scopeResolver

      属性指定全类名;

      1
      2
      3
      @Configuration
      @ComponentScan(basePackages = "org.example", scopeResolver = MyScopeResolver.class)
      public class AppConfig {}
    • XML:通过 <context:component-scan> scope-resolver

      属性指定全类名;

      1
      <context:component-scan base-package="org.example" scope-resolver="org.example.MyScopeResolver"/>

3、作用域代理 解决 不同作用域Bean的依赖冲突

当一个长生命周期的Bean 依赖一个段生命周期的Bean的时候,会导致注入失败。

那么这时候作用域代理的解决思路就是 注入代理对象而不是真实对象 作用域代理会生成一个 “代理对象” 注入到依赖方(如 UserService,当依赖方调用代理对象的方法时,代理会 动态获取当前作用域的真实 Bean 实例,确保每次使用的都是 “有效实例”。

1
2
3
4
5
6
7
8
9
@Service // 单例
public class UserService {
@Autowired
private UserRequestContext requestContext; // 依赖请求域Bean

public String getCurrentUsername() {
return requestContext.getUsername(); // 问题:此处的requestContext可能已失效
}
}

以上面的场景为例,给 UserRequestContext 配置作用域代理:

1
2
3
4
5
6
7
8
9
@Scope(
value = "request",
proxyMode = ScopedProxyMode.INTERFACES // 生成基于接口的代理
)
@Component
public class UserRequestContext implements RequestContext {
private String username;
// getter/setter...
}

此时 UserService 注入的是 RequestContext 接口的代理对象:

  • UserService 调用 requestContext.getUsername() 时,代理会自动查找 “当前 HTTP 请求对应的 UserRequestContext 实例” 并调用其方法;
  • 无论多少个请求,代理总能找到 “当前请求的有效实例”,解决了 “单例依赖短生命周期 Bean” 的冲突。
作用域代理的两种类型
  • ScopedProxyMode.INTERFACES:基于 JDK 动态代理,要求被代理的 Bean 实现接口(如 UserRequestContext 实现 RequestContext 接口),代理对象实现相同接口。
  • ScopedProxyMode.TARGET_CLASS:基于 CGLIB 代理,直接代理类(即使 Bean 没有实现接口),通过继承目标类生成代理对象。

为什么直接注入会导致请求失效?

  1. 当 Spring 容器启动时,UserService被初始化,此时会尝试注入

    UserRequestContext

    • 但此时可能 没有任何 HTTP 请求正在处理(服务器刚启动,还没收到请求),request 作用域的 UserRequestContext 无法创建,可能注入 null 或抛出异常。
  2. 即使容器启动后收到第一个请求,UserService注入的是 “第一个请求的UserRequestContext实例”。

    • 当第一个请求处理完毕后,这个 UserRequestContext 实例会被销毁(随请求失效)。
  3. 当第二个请求到来时,UserService仍然持有 “已销毁的第一个请求的实例”,此时调用getUsername()会导致:

    • 访问已销毁的对象(可能抛出异常);
    • 即使没报错,获取到的也是第一个请求的数据(与当前请求无关,数据错误)。

在每次调用方法时(如 getUsername()),动态查找当前正在处理的 HTTP 请求,并获取该请求绑定的 UserRequestContext 实例,再调用其方法

当前请求如何被代理感知

代理能找到 “当前请求”,依赖于 ThreadLocal 机制

  • 服务器处理 HTTP 请求时,会将当前 HttpServletRequest 对象存入 ThreadLocal(线程局部变量),确保同一处理线程中随时能获取到当前请求。
  • 代理对象在调用方法时,会从 ThreadLocal 中取出 “当前请求”,再从请求中找到绑定的 UserRequestContext 实例(Spring 内部会将 request 作用域 Bean 与请求绑定并存储)。

资源 Resources

Resources 是 Spring 提供的一个抽象接口,用来统一访问“外部资源”——无论这些资源是来自 classpath、文件系统、URL 还是 JAR 包内部。

以前读取一个配置文件

1
2
3
// ❌ 传统做法:容易出错,依赖具体路径
File file = new File("config/app.properties");
InputStream is = new FileInputStream(file);

现在spring框架统一了访问api

Resource接口

位于 org.springframework.core.io. 包中的Spring Resource 接口,旨在成为一个更有能力的接口,用于抽象访问低级资源。下面的列表提供了 Resource 接口的概述。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public interface Resource extends InputStreamSource {

boolean exists();

boolean isReadable();

boolean isOpen();

boolean isFile();

URL getURL() throws IOException;

URI getURI() throws IOException;

File getFile() throws IOException;

ReadableByteChannel readableChannel() throws IOException;

long contentLength() throws IOException;

long lastModified() throws IOException;

Resource createRelative(String relativePath) throws IOException;

String getFilename();

String getDescription();
}

Resource 接口中最重要的一些方法是。

  • getInputStream(): 定位并打开资源,返回一个用于读取资源的 InputStream。我们期望每次调用都能返回一个新的 InputStream。关闭该流是调用者的责任。
  • exists(): 返回一个 boolean 值,表示该资源是否以物理形式实际存在。
  • isOpen(): 返回一个 boolean,表示该资源是否代表一个具有开放流的句柄。如果为 trueInputStream 不能被多次读取,必须只读一次,然后关闭以避免资源泄漏。对于所有通常的资源实现,除了 InputStreamResource 之外,返回 false
  • getDescription(): 返回该资源的描述,用于处理该资源时的错误输出。这通常是全路径的文件名或资源的实际URL。

内置的Resource实现

Spring包括几个内置的 Resource 实现。

这里是spring内置的一些读取资源的类,spring中还内置了很多数据验证,类型转换等的类,需要用的用的多就记住了,这里就不列出来了

Spring AOP

动态代理是 “工具”,Spring AOP 是 “用这个工具解决问题的完整方案”

回顾一下动态代理:jdk动态代理流程就是首先规定一个共同的接口,然后实现一个实现类实现接口写好核心业务,然后写自定义的MyInvocationHandler代理类实现InvocationHandler接口,然后在里面通过invoke方法然后在里面调用核心业务并且在核心业务前或后写增强代码,然后在写个代理生成工厂,用于动态生成刚刚的动态代理,然后这个代理工厂就用Proxy的newInstance方法生成代理类,然后传入代理类的类加载器和实现的接口还有拦截器就是刚刚的InvocationHandler就可以动态生成代理类了。然后使用的时候就新建目标对象,然后用代理工厂创建目标对象的代理对象,然后调用的时候用代理对象的方法就能实现增强方法了。

本章讨论了基于 schema 和 @AspectJ 的AOP支持。也就是包装了动态代理的功能了。

AOP中的各种新概念

那么Spring AOP怎么使用呢?需要先了解一下一些新的定义。

  • Aspect 切面:这个就是类似于增强类,只要给普通类@Aspect注解就能把这个类当成切面类,里面写各种增强的逻辑,进行模块化管理,这个类里面写的全是增强的逻辑。
  • Join point 连接点:就是可以被拦截的地方,也就是核心业务逻辑的地方。
  • Advice 通知:你写的增强逻辑的代码。
  • Pointcut 切入点:就是筛选逻辑,用来告诉spring那些地方用上增强逻辑,写在切面类中,他是一个普通方法,但是用@Pointcut注解标记,方法体一般为空,只起 命名+定义规则的作用
  • Introduction 引入:让你能在不改源码的情况下,让一个对象“假装”实现了某个新接口,并拥有对应的方法和状态。引入使用的是cglib动态代理。
  • Advisor = Pointcut + Advice 通知器 Spring aop 中整整被应用代理上的单元是Advisor

看到这里会有疑惑,那么引入和通知有什么区别,有通知不就好了,引入不也是增强方法吗?

这里还是有区别的,如果是通知,调用方是感知不到增强的只是被动的增强,比如日志增强,只是调用的时候被动的记录了日志,调用放感知不到还有日志这个功能。但是引入是扩展了接口方法,假设利用引入增强,那么调用方可以调用新方法,直接帮我们包装了动态代理,只需要配置好新接口和实现就能通过@DeclareParents 直接实现cglib动态代理。

  • Target Object 目标对象:被代理对象
  • AOP proxy AOP代理:AOP框架中实现的代理对象
  • Weaving 织入:首先只是一个概念不是方法,把切面逻辑 编织 进目标对象的执行流程中,生成一个能自动触发增强行为的代理对象。发生的时机是Spring容器创建Bean的过程中,在BeanPostProcessor阶段,自动生成代理对象,已经帮我们封装好了。

AOP通知类型

  • 前置通知 Before advice:在连接点前执行,无法阻止连接点执行
  • 返回后通知 After returning advice:在连接点无异常抛出后执行

  • 最终通知 After finally advice:无论连接点正常或者异常退出都会执行

  • 环绕通知 Around advice:可在连接点前后执行操作,还能决定是否自行连接点,或者自定义返回值,抛出异常。

AOP实践

启用@AspectJ配置

要用Java @Configuration 启用 @AspectJ 支持,请添加 @EnableAspectJAutoProxy 注解

1
2
3
4
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
}
声明一个Aspect
1
2
3
4
5
6
7
8
package com.xyz;

import org.aspectj.lang.annotation.Aspect;

@Aspect // 标识为切面类
@Component // 让Spring管理
public class NotVeryUsefulAspect {
}
声明切点
1
2
3
// 访问修饰符(可选) void 切点名称() { }
@Pointcut("切点表达式")
public void 切点名称() {} // 方法体必须为空

核心是「指定器 + 匹配规则」

1
2
3
// 访问修饰符(可选) void 切点名称() { }
@Pointcut("切点表达式")
public void 切点名称() {}

如果是复用切点就这么写就可以了

1
2
3
4
5
6
7
// 组合2个切点:service层的public方法
@Pointcut("publicMethod() && inServiceLayer()")
public void servicePublicMethod() {}

// 引用其他切面的切点(全类名+方法名)
@Pointcut("com.xyz.CommonPointcuts.businessService()")
public void referOtherPointcut() {}

切点表达式

在AOP支持的指定器中,excution是核心

1
2
3
4
5
6
7
execution(
访问修饰符? // 如public、private,可选,*表示任意
返回值类型 // 必须写,*表示任意返回值,全类名表示特定类型(如java.lang.String)
类路径? // 可选,如com.xyz.service.*,表示该类下的方法
方法名(参数模式) // 必须写,方法名支持*通配符,参数模式有固定写法
异常类型? // 可选,如throws java.lang.Exception,匹配抛出该异常的方法
)

常用指定器

需求 切点表达式
所有 public 方法 execution(public * *(..))
所有名称以 set 开头的方法 execution(* set*(..))
AccountService 接口的所有方法 execution(* com.xyz.service.AccountService.*(..))
service 包下的所有方法 execution(* com.xyz.service.*.*(..))
service 包及子包下的所有方法 execution(* com.xyz.service..*.*(..))..表示子包)
service 包下返回 String 的 public 方法 execution(public String com.xyz.service.*.*(..))

辅助指定器

指定器 作用 示例
within 匹配指定包 / 类下的方法 within(com.xyz.service..*)(service 包及子包)
bean 匹配指定名称的 Spring Bean 的方法 bean(tradeService)(Bean 名为 tradeService)、bean(*Service)(通配符,匹配所有后缀为 Service 的 Bean)
@annotation 匹配带有指定注解的方法 @annotation(org.springframework.transaction.annotation.Transactional)(匹配带 @Transactional 的方法)
@within 匹配所在类带有指定注解的方法 @within(com.xyz.annotation.Log)(类上有 @Log 注解的所有方法)
target 匹配目标对象(被代理类)是指定类型的方法 target(com.xyz.service.UserService)(目标对象实现 UserService 接口)
声明Advice

切面中定义的增强逻辑代码,会在匹配的连接点 执行时被触发。

它必须与一个 切点表达式关联,决定在哪些方法上生效。

切点可以是:直接写在Advice里面通过@Before等注解然后在里面写切点的匹配条件(内联切点)

1
@Before("execution(* com.xyz.dao.*.*(..))")

或者通过引用命名切点,先用一个方法定义好切点匹配逻辑然后再另一个方法引用切点方法。

1
2
3
4
@Pointcut("execution(* com.xyz.dao.*.*(..))")
public void dataAccessOperation() {}

@Before("dataAccessOperation()")

五种Advice类型以及命名方式

类型 注解 触发时机 典型用途
Before @Before 方法执行之前 权限检查、日志记录
After Returning @AfterReturning 方法正常返回后 处理返回值、缓存结果
After Throwing @AfterThrowing 方法抛出异常后 异常恢复、日志记录
After (Finally) @After 方法无论正常或异常退出后(类似 finally) 资源释放(锁、连接)
Around @Around 环绕整个方法执行 性能监控、事务控制、缓存

参数绑定!

没有参数绑定的话,Advice 只能做 “无差别增强”(比如不管目标方法是什么、传了什么参数,都只打印固定日志);但实际开发中,增强逻辑往往需要 “针对性处理”,必须依赖目标方法的具体信息。

1、绑定返回值(@AfterReturning

1
2
3
4
5
@AfterReturning(
pointcut = "execution(* service.*(..))",
returning = "result"
)
public void logResult(Object result) { ... }
  • returning 的值必须与方法参数名一致。
  • 自动限制只匹配返回 Object(或指定类型)的方法。

2、绑定异常(@AfterThrowing

1
2
3
4
5
@AfterThrowing(
pointcut = "execution(* service.*(..))",
throwing = "ex"
)
public void handleException(DataAccessException ex) { ... }
  • 只匹配抛出 DataAccessException 或其子类的方法。

3、绑定方法参数(args

1
2
@Before("execution(* dao.*(..)) && args(account, ..)")
public void validate(Account account) { ... }
  • 匹配第一个参数为 Account 的方法,并将实际对象传入。

4、绑定注解(@annotation

1
2
3
4
@Before("@annotation(auditable)")
public void audit(Auditable auditable) {
AuditCode code = auditable.value();
}

5、访问 JoinPoint 上下文

1
2
3
4
5
6
@Before("...")
public void log(JoinPoint jp) {
Object[] args = jp.getArgs();
Object target = jp.getTarget();
String method = jp.getSignature().getName();
}
  • @Around 必须使用 ProceedingJoinPoint

Around Advice额外说明

1
2
3
4
5
6
7
8
@Around("execution(* service.*(..))")
public Object profile(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
Object result = pjp.proceed(); // ⚠️ 必须调用 proceed() 才会执行原方法
long end = System.currentTimeMillis();
System.out.println("Time: " + (end - start));
return result; // 返回给调用者
}
  • 必须返回 Object,即使原方法是 void
  • proceed() 可以调用 0 次、1 次或多 次(但通常只调 1 次)。
  • 可传新参数:pjp.proceed(new Object[]{newArg})

同一个切面内,执行顺序

1
@Around → @Before → 方法执行 → @AfterReturning / @AfterThrowing → @After

除了JoinPoint都需要显示指定参数名进行匹配

JoinPoint 接口提供了许多有用的方法。

  • getArgs(): 返回方法的参数。
  • getThis(): 返回代理对象。
  • getTarget(): 返回目标对象。
  • getSignature(): 返回正在被 advice 的方法的描述。
  • toString(): 打印对所 advice 的方法的有用描述。
Introduction引入
  • Introduction 允许一个切面动态地为目标类添加新的接口和实现,即使这些类在源码中并没有实现该接口。
  • 在 Spring AOP 中通过 @DeclareParents 注解实现。

效果:目标对象在运行时“看起来”像是实现了某个新接口,并具备其实现逻辑。

组成部分 说明
@DeclareParents 用于声明引入的注解
value 属性 AspectJ 类型匹配表达式,指定哪些类要被增强(如 "com.xyz.service.*+" 表示 service 包下所有类及其子类)
字段类型 要引入的接口类型(如 UsageTracked
defaultImpl 属性 提供该接口的默认实现类(如 DefaultUsageTracked.class
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Aspect
public class UsageTracking {

// 引入声明:所有 service 实现类都“实现” UsageTracked 接口
@DeclareParents(
value = "com.xyz.service.*+",
defaultImpl = DefaultUsageTracked.class
)
public static UsageTracked mixin; // 字段类型 = 要引入的接口

// 使用引入的接口
@Before("execution(* com.xyz..service.*.*(..)) && this(usageTracked)")
public void recordUsage(UsageTracked usageTracked) {
usageTracked.incrementUseCount(); // 调用新增的方法
}
}

Aspect实例化模式

为什么需要非单例切面?*

单例切面的问题:切面实例是共享的,无法存储「专属某个目标对象 / 代理对象」的状态。比如想统计「每个服务 Bean 的方法调用次数」—— 如果用单例切面,计数器是全局共享的,无法区分是哪个服务 Bean 的调用;而非单例切面(perthis/pertarget)能为每个服务 Bean 创建专属实例,计数器可以独立存储,互不干扰。

每个切面在Spring容器中默认只有一个实例,如果想要非单例模型切面也是可以支持的这里有两个模型

perthis(pointcut) 每个唯一的代理对象(this创建一个切面实例。 → 当某个服务 Bean 首次被调用时,为其创建专属切面实例。
pertarget(pointcut) 每个唯一的目标对象(target创建一个切面实例。 → 与 perthis 类似,但在 CGLIB 代理下可能表现不同(this 是代理,target 是原始对象)。

大多数场景下(比如 JDK 动态代理,目标对象实现接口),一个目标对象对应一个代理对象,perthispertarget 效果一致;但在 CGLIB 代理(目标对象无接口)或特殊代理场景下,可能出现 “多个代理对象对应同一个目标对象”(罕见),此时两者会有差异。

这时候perthis会有多个,pertarget只有一个。

那么这时候就能用计数器来计数或者在逻辑代码里面实现自己想要的特定功能了,比如每个目标Bean的引用次数

使用方式

切面类加@Scope("perthis(切点)")@Scope("pertarget(切点)"),配合@Aspect@Component即可。

Spring AOP 默认根据目标对象是否有接口选择 JDK 代理或 CGLIB 代理;通过设置 proxy-target-class=”true” 可强制使用 CGLIB,从而代理类的所有非 final 方法,但需注意 final 方法无法被增强。

可能会出现的问题

首先String aop不能代理final方法,也就是说final方法无法被增强。

自调用问题

1
2
3
4
5
6
7
8
9
public class SimplePojo implements Pojo {
public void foo() {
this.bar(); // ← 这是“自调用”
}

public void bar() {
// some logic...
}
}

如果这个SimplePojo被代理了,然后这个foo方法调用的是this方法,这时候没用走代理对象的方法而是调用对象内部的方法,这样就不会触发增强方法,导致Advice不执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Service
public class ServiceA {
@Autowired
private ServiceB serviceB;

public void foo() {
serviceB.bar(); // ← 跨 Bean 调用,走代理
}
}

@Service
public class ServiceB {
@Transactional
public void bar() { ... } // ← Advice 生效
}

总结

首先这个是Spring框架的一个IOC容器,主要是将对象创建、依赖注入、生命周期交给Spring容器,里面有几个关键组件一个是BeanFactory基础的IOC容器,ApplicationContext高级的容器支持AOP、事件、国际化等,BeanDefinition描述Bean的元数据。

Spring容器启动流程

分为三个阶段,准备阶段,Bean处理阶段,完成阶段。

准备阶段,首先加载配置文件,读取XML、JavaConfig等配置,获取数据库连接,AOP等配置类信息,然后创建容器生成ApplicationContext,初始化BeanFactory,然后解析Bean定义,解析BeanDefinitions,提取Bean的元数据

Bean处理阶段,然后就实例化Bean,根据BeanDefinitions创建Bean对象,然后依赖注入属性,通过构造器或者属性注入Bean依赖,然后管理生命周期,调用初始化方法如InitializingBean.afterPropertiesSet,然后执行后处理器,利用BeanPostProcessor增强Bean生成AOP代理。

完成阶段,发布事件,发送容器启动等事件。然后完成启动了。

Web 项目如何加载 Spring XML

1
2
3
4
5
6
7
8
9
src/
├── main/
│ ├── java/
│ └── resources/
│ ├── applicationContext.xml ← 根上下文
│ └── spring-mvc.xml ← MVC 上下文
└── webapp/
└── WEB-INF/
└── web.xml

可以理解为spring的配置文件都放在resource文件夹下,然后web项目需要配置服务器加载的配置文件位置通过配置DispathcherServlet可以读取加载resouces文件夹下的配置文件,在非SpringBoot项目这些都需要手动配置。

虽然 XML 配置仍在使用,但现代 Spring Boot 项目更常用:

  • Java Config:用 @Configuration 类替代 XML
  • 自动配置:Spring Boot 自动配置 DataSource、MVC 等,无需手动写 bean
  • application.properties/yml:外部化配置

测试

在Spring中如何让测试更容易。

Spring提倡测试驱动开发,也就是先通过所有测试用例然后再优化重构开发。

测试的重要概念

1、核心概念

首先Spring测试支持单元测试和集成测试。

单元测试:主要目的是不启动容器,隔离依赖,spring通过提供Mock对象和工具类来提供单元测试的环境。

隔离的目的,比如测试UserService,但是这个依赖UserDao,然后把这个依赖换成一个假的可控制的替代者,让测试只关注UserService本身的逻辑,不受外部依赖影响。

Stub是桩,简单的假对象,只返回固定结果,硬编码模拟依赖的行为

Mock是模拟,更灵活的假对象,比如指定调用册数,参数匹配等。

正是因为IoC 是依赖注入,而不是Service自己New对象,我们才能轻松替换依赖。由于UserDao是通过构造器或者Setter注入的,测试时可以直接传入Mock或者Stub对象,替代真实的Dao

集成测试:启动部分/完成的容器,验证组件能否正常协作,提供@SpringBootTest,@DataJpaTest,MocMvc等来支持。

支持的组件

Mock对象用于单元测试

  • MockEnvironment / MockPropertySource模拟配置环境
  • MockHttpServletRequest / MockHttpSession 模拟Web请求
  • MockServerHttpRequest 用于响应式测试

测试工具类

  • ReflectionTestUtils:反射操作私有字段/方法
  • AopTestUtils:获取AOP代理背后的原始对象
  • ModelAndViewAssert :断言MVC控制器返回结果

集成测试框架

  • Servlet StackMockMvc(基于 Mock Servlet API,无需部署)
  • Reactive StackWebTestClient(支持无服务器或端到端测试)

如何使用Spring进行测试

1、单元测试 不依赖Spring容器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 直接 new + Mock 依赖
@Test
void testUserService() {
// 1. 创建 Mock 依赖
OrderRepository mockRepo = Mockito.mock(OrderRepository.class);
when(mockRepo.count()).thenReturn(5L);

// 2. 手动组装被测对象
UserService service = new UserService();
ReflectionTestUtils.setField(service, "orderRepository", mockRepo); // 注入私有字段

// 3. 调用并断言
assertEquals(5L, service.getOrderCount());
}

2、集成测试

  • 完整上下文测试
1
2
3
4
5
6
7
8
9
10
11
12
@SpringBootTest
class UserServiceIntegrationTest {

@Autowired
private UserService userService; // 由 Spring 注入完整依赖链

@Test
void testWithRealDependencies() {
// 可能使用内存数据库(如 H2)
assertNotNull(userService.createUser("Alice"));
}
}
  • 切片测试(只加载必要组件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@DataJpaTest // 仅加载 JPA 相关 Bean
class UserRepositoryTest {

@Autowired
private TestEntityManager em;

@Autowired
private UserRepository repository;

@Test
void findByEmail() {
User user = new User("alice@example.com");
em.persistAndFlush(user);

Optional<User> found = repository.findByEmail("alice@example.com");
assertTrue(found.isPresent());
}
}
  • Web 层测试(MockMvc)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@WebMvcTest(UserController.class) // 仅加载 Web 层
class UserControllerTest {

@Autowired
private MockMvc mockMvc;

@MockBean
private UserService userService; // 用 Mock 替代真实 Service

@Test
void shouldReturnUser() throws Exception {
when(userService.findById(1L)).thenReturn(new User("Alice"));

mockMvc.perform(get("/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("Alice"));
}
}

案例

案例:用 TDD(测试驱动开发)开发一个用户注册功能

🎯 需求

  • 用户注册时,邮箱不能重复
  • 成功注册返回用户 ID

步骤 1:先写测试(红)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// UserControllerTest.java
@WebMvcTest(UserController.class)
class UserControllerTest {

@Autowired
private MockMvc mockMvc;

@MockBean
private UserService userService;

@Test
void registerUser_withDuplicateEmail_returns409() throws Exception {
// Given
String email = "exist@example.com";
when(userService.isEmailExists(email)).thenReturn(true);

// When & Then
mockMvc.perform(post("/register")
.contentType(APPLICATION_JSON)
.content("""
{"email": "%s", "name": "Alice"}
""".formatted(email)))
.andExpect(status().isConflict()); // 409
}

@Test
void registerUser_success_returns201() throws Exception {
// Given
when(userService.register(any(User.class))).thenReturn(123L);

// When & Then
mockMvc.perform(post("/register")
.contentType(APPLICATION_JSON)
.content("""
{"email": "new@example.com", "name": "Bob"}
"""))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").value(123));
}
}

🔴 此时测试失败(因为 Controller 还没写)


步骤 2:写最小实现(绿)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// UserController.java
@RestController
public class UserController {

private final UserService userService;

public UserController(UserService userService) {
this.userService = userService;
}

@PostMapping("/register")
public ResponseEntity<?> register(@RequestBody User user) {
if (userService.isEmailExists(user.getEmail())) {
return ResponseEntity.status(CONFLICT).build();
}
Long id = userService.register(user);
return ResponseEntity.status(CREATED).body(Map.of("id", id));
}
}
1
2
3
4
5
// UserService.java(接口)
public interface UserService {
boolean isEmailExists(String email);
Long register(User user);
}

🟢 测试通过!


步骤 3:重构 + 补充单元测试

  • UserService 实现类写单元测试
  • 验证业务逻辑(如密码加密、事件发布等)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// UserServiceImplTest.java
class UserServiceImplTest {

private UserRepository repo = Mockito.mock(UserRepository.class);
private UserService service = new UserServiceImpl(repo);

@Test
void register_savesUserAndReturnsId() {
User input = new User("new@example.com", "Bob");
when(repo.save(any())).thenAnswer(inv -> {
User saved = inv.getArgument(0);
saved.setId(999L);
return saved;
});

Long id = service.register(input);

assertEquals(999L, id);
verify(repo).save(argThat(u -> u.getEmail().equals("new@example.com")));
}
}

事务

简介

首先事务就是一组操作,要么全成功,要么全失败。

然后传统Java EE 开发有两种事务方式:一种是全局事务,太重太麻烦,目的是同事操作多个资源,比如该数据库+发消息到MQ,要么两者都成功要么都失败,用Java Transaction API 用JTA,通常运行在应用服务器里面,因为必须用到JNDI(这个只有重服务器里面有这个服务器里的 “资源注册表”)所以轻服务器用不了。另一种是本地事务,只能管理一个资源,最大的劣势了这个就是。

现在Spring有独立的JTA组件,只需要在代码加@Transactional注解,配置里写清楚资源信息,轻服务器也能跑了

Spring提供两种事务管理方式

1、声明式事务(推荐)

通过注解或 XML 配置,无需写事务代码,由 Spring 自动管理。

1
2
3
4
5
6
7
8
9
@Service
public class UserService {

@Transactional // ← 声明式事务:方法执行前后自动开启/提交/回滚事务
public void register(User user) {
userRepository.save(user);
emailService.sendWelcomeEmail(user.getEmail());
}
}

⚙️ 底层基于 AOP 实现:Spring 会为该方法创建代理,在调用前后插入事务控制逻辑。

2、编程式事务(灵活但繁琐)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Autowired
private PlatformTransactionManager transactionManager;

public void register(User user) {
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
userRepository.save(user);
emailService.sendWelcomeEmail(user.getEmail());
transactionManager.commit(status); // 提交
} catch (Exception e) {
transactionManager.rollback(status); // 回滚
throw e;
}
}

为什么Spring层管理事务?事务不是MySQL的事吗?

因为使用Spring管理事务无论换Oracle还是MySQL或者别的数据库都能一行搞定,开发者不需要关心底层用的是哪种持久化技术。

Spring事务的抽象

了解一下Spring事务的顶层抽象。

Spring事务的核心 是 TransactionManager 接口

这个接口有两个主要实现:

PlatformTransactionManager 用于传统阻塞应用

1
2
3
TransactionStatus getTransaction(TransactionDefinition def);
void commit(TransactionStatus status);
void rollback(TransactionStatus status);

ReactiveTransactionManager 用于响应式编程

1
2
Mono<ReactiveTransaction> getReactiveTransaction(...);
Mono<Void> commit(...);

这两个实现都是由Spring内部使用,但是也可以编程调用

配置@Transactional

属性 作用
Propagation(传播行为) 方法被调用时,如何参与事务? 例如:加入现有事务(REQUIRED) or 新开一个(REQUIRES_NEW
Isolation(隔离级别) 防止脏读、不可重复读、幻读等并发问题
Timeout(超时) 事务最多执行多久,超时自动回滚
Read-only(只读) 查询操作设为只读,可优化性能(比如 不生成 redo/undo 日志

如果使用编程式事务中会有用到TransactionStatus

这个是事务执行时的状态控制的,然后在编程式事务中可以通过它

  • 判断是否是新事务:isNewTransaction()
  • 手动标记回滚:setRollbackOnly()
  • 检查是否已完成:isCompleted()

上面提到Spring内部实现了一套实现,我们只需要修改配置即可轻松切换全局事务还是本地事务,那么这里需要有不同的配置

不同数据访问技术,对应不同的 TransactionManager 实现

技术 事务管理器类 配置要点
纯 JDBC DataSourceTransactionManager 需要注入 DataSource
Hibernate HibernateTransactionManager 需要注入 SessionFactory
JPA JpaTransactionManager 需要注入 EntityManagerFactory
JTA(全局事务) JtaTransactionManager 不需要指定数据源,由应用服务器管理

那么这里需要再JavaConfig里面手动配置对应的类了然后Spring会帮我们自动注入,然后在事务启动的时候会自己调用对应的Manager来对应事务了。

1
2
3
4
5
6
7
8
9
10
11
@Configuration
@EnableTransactionManagement
public class TxConfig {

// 配置 JTA 对应的事务管理器:JtaTransactionManager
@Bean
public PlatformTransactionManager txManager() {
// 不需要注入 DataSource!因为 JTA 由应用服务器或独立组件(如 Atomikos)管理资源
return new JtaTransactionManager();
}
}

事务同步

  • 使用 Spring 封装好的模板类或代理,比如:

    • JdbcTemplate(用于 JDBC)
    • 带事务感知的 SessionFactory / EntityManagerFactory Bean(用于 Hibernate/JPA)
  • 好处:

    • 自动处理:连接创建、复用、关闭、异常转换、事务同步

    • 你的代码只写业务逻辑,比如:

      1
      jdbcTemplate.query("SELECT * FROM users", rowMapper);

      → 完全不用管 Connection 从哪来、要不要关。

事务同步的核心是:让 “资源操作”(比如 JDBC 查询 / 修改、MQ 发送)和 “事务生命周期”(开启→准备→提交 / 回滚)同步

Spring 是通过「TransactionSynchronizationManager(事务同步管理器)+ 线程局部变量(ThreadLocal)+ 同步回调接口」实现的。

声明式事务机制

上面我们知道用@Transactional 然后将@EnableTransactionManagement 添加到配置中就能使用事务了,但是这还不够,这里讲解一下Spring框架如何实现声明式事务的。

内部通过 AOP代理 + 事务拦截器TransactionInterceptor + 事务管理器TransactionManager 实现

用AOP代理包装目标服务,用注解/XML 定义事务规则,比如那些方法只读,哪些需要读写,最终由事务管理器驱动事物的开启、提交或者回滚。

Spring声明式事务关键靠3个核心组件协同工作

1、AOP动态代理,Spring返回的是一个代理对象

2、事务元数据,在@Transactional里面读取事务的元数据,告诉Spring那些方法需要事务,事务是什么类型只读,超时时间等等。

3、事务拦截器 + 事务管理器,代理对象运行的时候会触发拦截器,拦截器先读取事务元数据,再调用对应的事务管理器(比如 JDBC 用特定的事务管理器) ,然后由事务管理器把Spring容器的生命周期和事务的生命周期结合一起,开启,执行,提交或回滚事务的全流程。

事务回滚

上面说到我们的事务的生命周期交给了事务管理器,那么事务管理器会自动帮我们回滚,这里讲一下事务管理器回滚机制。

触发机制

Spring默认只对未检查异常也就是RuntimException 和 Error 自动回滚事务。

受检查异常 不会触发回滚,除非显示配置。

声明式回滚配置方式

通过注解方式在@Transactional 里面配置

  • rollbackFor / noRollbackFor:传入具体的异常 Class 对象(类型安全)。
  • rollbackForClassName / noRollbackForClassName:传入异常的 全限定类名字符串(基于模式匹配,需谨慎使用)。

Spring5.2+支持Vavr Try

@Transactional 使用细节

这是个元数据,本身不执行事务逻辑,而是为Spring提供事务语义的生命,实际事务行为由Spring的事务基础设施在运行的时候根据这些元数据自动应用。

1、基本用法

  • 可在 类级别方法级别 使用@Transactional
    • 类级别:为所有public方法提供默认事务配置
    • 方法级别:覆盖类级别的设置 (方法优先级别更高)
  • 需配合启用注解启动

默认事务设置

属性 默认值 说明
传播行为(propagation) REQUIRED 若存在事务则加入,否则新建
隔离级别(isolation) DEFAULT 使用数据库默认隔离级别
读写模式(readOnly) false 读写事务
超时(timeout) -1 使用底层事务系统的默认超时(若不支持则无超时)
回滚规则 RuntimeExceptionError 触发回滚 受检异常(checked exception)不会自动回滚

其他可自定义属性

属性 类型 作用
value / transactionManager String 指定使用的事务管理器 Bean 名称或 qualifier
label String[] 为事务添加标签(可用于监控、重试等扩展逻辑)
propagation Propagation 枚举 设置事务传播行为(如 REQUIRES_NEW
isolation Isolation 枚举 设置隔离级别(仅当传播行为为 REQUIREDREQUIRES_NEW 时有效
timeout / timeoutString int / String 设置事务超时时间(秒),同样仅对新建事务有效
readOnly boolean 声明为只读事务(可优化性能),也仅在新建事务时生效
rollbackFor / rollbackForClassName Class[] / String[] 指定哪些异常(包括 checked)应触发回滚
noRollbackFor / noRollbackForClassName Class[] / String[] 指定哪些异常不应触发回滚(即使它们是 RuntimeException)

2、关键限制

首先他是用AOP代理机制的,所以自调用无效就是用this调用方法就会失效。仅 public 方法有效 其他的加了注解也不会生效

如果一定要用的话:使用AspectJ 编译时或者加载时植入 ,可支持任意方法。

AspectJ

这个是独立的、功能更强的AOP框架核心能力是支持编译时植入,加载时织入,简单说就是 “在代码运行前 / 运行时,把 AOP 逻辑(比如事务拦截)直接嵌入到目标类的字节码中”—— 不用依赖动态代理,自然能捕获到自调用、private 方法。

3、多事务管理器

大多数应用只需一个事务管理器,但是复杂场景可能需要多个。

就是不同的数据库需要特定的事务管理器,如果一个项目里,既要用到JDBC又要用到 JPA ,那么这时候需要配置多事务管理器。

核心流程:配置多个事务管理器Bean,并用@Qualifier 标记唯一标识,然后再@Transactional 中通过value 或者 tansactionManager属性,指定使用哪个事务管理器。

先配置Java Config

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Configuration
@EnableTransactionManagement // 开启事务支持
public class MultiTxManagerConfig {

// -------------------------- 订单库(JDBC)事务管理器 --------------------------
// 1. 配置订单库数据源(独立数据源)
@Bean("orderDataSource")
public DataSource orderDataSource() {
// 配置订单库连接池(如Druid),省略细节
return new com.alibaba.druid.pool.DruidDataSource();
}

// 2. 配置订单库事务管理器,用@Qualifier标记标识:orderTxManager
@Bean("orderTxManager")
public PlatformTransactionManager orderTxManager(
@Qualifier("orderDataSource") DataSource dataSource) {
// JDBC对应的事务管理器,绑定订单库数据源
return new DataSourceTransactionManager(dataSource);
}

// -------------------------- 用户库(JPA)事务管理器 --------------------------
// 1. 配置用户库EntityManagerFactory(JPA核心组件)
@Bean("userEmf")
public EntityManagerFactory userEntityManagerFactory() {
// 配置JPA(如Hibernate),绑定用户库数据源,省略细节
return new org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean().getObject();
}

// 2. 配置用户库事务管理器,用@Qualifier标记标识:userTxManager
@Bean("userTxManager")
public PlatformTransactionManager userTxManager(
@Qualifier("userEmf") EntityManagerFactory emf) {
// JPA对应的事务管理器,绑定用户库EntityManagerFactory
return new JpaTransactionManager(emf);
}
}

然后再@Transactional里面引用对应的Bean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Service
public class MultiDbService {

// --------------- 操作订单库:指定订单库事务管理器 ---------------
// 方式1:用value属性(简写)指定事务管理器标识
@Transactional(value = "orderTxManager", rollbackFor = Exception.class)
public void createOrder(Order order) {
// 操作订单库(JDBC/MyBatis),事务由orderTxManager管理
orderMapper.insert(order);
}

// --------------- 操作用户库:指定用户库事务管理器 ---------------
// 方式2:用transactionManager属性(全称)指定
@Transactional(transactionManager = "userTxManager", readOnly = true)
public User getUserById(Long userId) {
// 操作用户库(JPA),事务由userTxManager管理
return userRepository.findById(userId).orElse(null);
}

// --------------- 同时操作两个库:两个独立事务(非分布式) ---------------
public void createOrderAndUser(Order order, User user) {
// 调用两个带事务的方法,各自用自己的事务管理器,互不影响
createOrder(order); // 订单库事务:失败回滚订单操作
getUserById(user.getId()); // 用户库事务:失败回滚用户操作
// 注意:如果createOrder成功,getUserById失败,订单库事务已提交,不会回滚!
// 若需“要么都成要么都败”,需用分布式事务(如JTA),而非两个独立事务管理器
}

如果担心注解长度过长可以自定义注解。

事务传播配置

1、 PROPAGATION_REQUIRED(默认行为)

  • 行为:如果当前存在事务,则加入该事务;否则新建一个事务。
  • 物理事务:所有嵌套调用共享同一个物理事务
  • 逻辑事务:每个方法有自己的逻辑事务作用域,可独立设置回滚状态。
  • 关键特性
    • 内部方法标记回滚 → 整个物理事务都会回滚
    • 如果外部不知道内部已标记回滚,仍尝试提交 → Spring 会抛出 UnexpectedRollbackException,防止误以为提交成功。
  • 注意:内部事务的隔离级别、超时、只读等设置通常被忽略(除非开启 validateExistingTransactions = true 来严格校验)。

  1. PROPAGATION_REQUIRES_NEW
  • 行为总是挂起(suspend)当前事务,并启动一个全新的、独立的物理事务
  • 特点
    • 内外事务完全隔离:各自拥有独立的提交/回滚、锁、隔离级别、超时等。
    • 内部事务回滚 不会影响 外部事务。
    • 适用于需要“无论外部如何,我都要独立完成”的场景(如日志记录、审计)。

  1. PROPAGATION_NESTED
  • 行为:在同一个物理事务中创建保存点(savepoint),实现部分回滚。
  • 机制
    • 若内部失败,可回滚到保存点,外部事务仍可继续并最终提交。
    • 成功时,保存点释放,变更合并到主事务。
  • 限制

    • 仅支持 JDBC 资源事务(如 DataSourceTransactionManager)。
    • 不适用于 JTA 或 Hibernate 等不支持保存点的事务管理器。
  • REQUIRED 共享事务(回滚全局生效),

  • REQUIRES_NEW 完全独立事务,
  • NESTED 通过保存点实现局部回滚(仅限 JDBC)。