SpringBoot
快来分享你的内容吧~
- 05-30 11:40·国双大专立志上岸双非一本
- 2025-11-05·Java后端
2025-10-28·Java后端- 2025-10-02·Java后端
springboot项目使用lua脚本的流程概览
在 Spring Boot 3.x 项目中,利用已有的 Redis 依赖(通常是 `spring-boot-starter-data-redis`)来使用 Lua 脚本非常简单。使用 Lua 脚本最大的好处是可以保证多个 Redis 操作的**原子性**,并且能够显著**减少网络开销**。 下面为你整理了一套标准的实战步骤,教你如何在项目中优雅地集成和调用 Lua 脚本: ### 第一步:编写 Lua 脚本文件 首先,在你的 Spring Boot 项目的资源目录下创建一个 Lua 脚本文件。 建议路径:`src/main/resources/lua/your_script.lua` 假设我们写一个简单的“获取值,若不存在则设置默认值”的脚本: ```lua -- get_or_set_default.lua local key = KEYS[1] local defaultVal = ARGV[1] local value = redis.call('GET', key) if not value then redis.call('SET', key, defaultVal) value = defaultVal end return value ``` * **KEYS**:用于接收你传入的 Redis 键(数组形式)。 * **ARGV**:用于接收你传入的其他业务参数(数组形式)。 * **redis.call()**:在 Lua 脚本中执行 Redis 命令的标准方式。 ### 第二步:将 Lua 脚本配置为 Bean 在 Spring Boot 3.x 中,推荐将 Lua 脚本封装成一个 `RedisScript` 的 Bean,交由 Spring 容器管理。这样不仅加载一次即可复用,Spring Data Redis 还会自动帮你处理脚本的 SHA1 缓存(即底层自动使用 `EVALSHA` 命令,极大提升性能)。 创建一个配置类: ```java import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.ClassPathResource; import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.data.redis.core.script.RedisScript; import org.springframework.scripting.support.ResourceScriptSource; @Configuration public class RedisLuaConfig { @Bean public RedisScript<String> getOrDefaultScript() { DefaultRedisScript<String> redisScript = new DefaultRedisScript<>(); // 加载类路径下的 lua 脚本 redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/get_or_set_default.lua"))); // 指定脚本返回值的类型(支持 String, Long, Boolean, List 等) redisScript.setResultType(String.class); return redisScript; } } ``` ### 第三步:在业务代码中调用脚本 在你的 Service 或 Component 中,直接注入 `RedisTemplate` 和刚刚配置好的 `RedisScript` Bean,然后通过 `execute` 方法执行即可。 ```java import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.script.RedisScript; import org.springframework.stereotype.Service; import java.util.Collections; import java.util.List; @Service public class RedisLuaService { @Autowired private RedisTemplate<String, Object> redisTemplate; @Autowired private RedisScript<String> getOrDefaultScript; public String getValueOrDefault(String key, String defaultValue) { // 调用 execute 方法执行 Lua 脚本 // 参数依次为:脚本对象、键的列表、其余参数(可变长参数) return redisTemplate.execute( getOrDefaultScript, Collections.singletonList(key), defaultValue ); } } ``` ### 💡 核心注意事项 1. **序列化问题**: `RedisTemplate` 默认的 Key 和 Value 序列化方式可能会影响 Lua 脚本的参数传递。如果你的 Key 是普通的字符串,建议确保 `RedisTemplate` 的 Key 序列化器使用的是 `StringRedisSerializer`,否则传入脚本的 Key 可能会带有额外的序列化前缀(如 `\xac\xed\x00\x05t\x00...`)。 2. **参数传递规则**: 在 `redisTemplate.execute()` 方法中,`KEYS` 必须通过 `List` 传入,而 `ARGV` 则是跟在后面的可变长参数。在 Lua 脚本中分别通过 `KEYS[1]`, `ARGV[1]` 等下标来获取。 3. **返回值类型**: 在配置 `DefaultRedisScript` 时,务必通过 `setResultType` 明确指定返回值的 Java 类型,否则可能会导致类型转换异常。 按照以上三步,你就可以在 Spring Boot 3.x 项目中轻松、高性能地使用 Redis Lua 脚本来处理复杂的原子性业务逻辑了。
Day29 补一下Spring Boot
## 1. Spring Boot 概述 ### 1.1 什么是 Spring Boot Spring Boot 是基于 Spring 框架的开发框架,它通过”约定优于配置”的理念,简化了 Spring 应用的初始搭建和开发过程。Spring Boot 并不是对 Spring 功能的增强,而是提供了一种快速使用 Spring 的方式。 **核心设计理念:** - **约定优于配置**:提供合理的默认值,减少手动配置 - **开箱即用**:集成常用第三方库的默认配置 - **自动配置**:根据类路径自动配置 Spring 应用 ### 1.2 Spring Boot vs Spring Framework | 特性 | Spring Framework | Spring Boot | | --- | --- | --- | | 配置 | 繁琐的 XML/Java 配置 | 自动配置,约定优于配置 | | 部署 | 需要外置 Tomcat | 内嵌 Tomcat,直接运行 | | 依赖 | 手动管理版本冲突 | Starter 依赖管理,自动版本管理 | | 开发效率 | 较低,配置繁琐 | 极高,快速启动 | | 监控 | 需要手动集成 | 内置 Actuator 监控端点 | | 生产支持 | 需要额外配置 | 内置生产级特性 | ### 1.3 Spring Boot 核心特性 ```mermaid graph TB A[Spring Boot] --> B[自动配置] A --> C[Starter 依赖] A --> D[内嵌服务器] A --> E[生产级特性] A --> F[无代码生成] A --> G[XML 配置可选] B --> B1[根据类路径自动配置] C --> C1[简化依赖管理] D --> D1[Tomcat/Jetty/Undertow] E --> E1[监控/健康检查/Metrics] ``` **特性详解:** 1. **自动配置**:Spring Boot 根据类路径中的依赖自动配置 Bean,大大减少了手动配置的工作量。 2. **Starter 依赖**:提供了一系列开箱即用的依赖包,每个 Starter 都包含了一组相关的依赖和自动配置。 3. **内嵌服务器**:内嵌了 Tomcat、Jetty 或 Undertow,无需部署 WAR 包,直接运行 JAR 包。 4. **生产级特性**:内置了健康检查、指标监控、外部化配置等生产级特性。 5. **无代码生成**:Spring Boot 不会生成代码,而是通过字节码增强和动态代理实现功能。 ## 核心原理 ### 2.1 Spring Boot 启动流程 ```mermaid sequenceDiagram participant Main as main() participant SA as SpringApplication participant EP as Environment participant AC as ApplicationContext participant RC as RefreshContext Main->>SA: 创建 SpringApplication SA->>SA: 推断应用类型 SA->>SA: 加载 BootstrapRegister SA->>EP: 准备 Environment SA->>SA: 打印 Banner SA->>SA: 创建 ApplicationContext SA->>AC: 加载自动配置 SA->>AC: 刷新上下文 AC->>RC: 调用 refresh() RC->>RC: 扫描 Bean 定义 RC->>RC: 实例化 Bean RC->>RC: 执行后置处理器 SA->>Main: 启动完成 ``` **启动流程详细解析:** 1. **创建 SpringApplication**:根据类路径推断应用类型(SERVLET、REACTIVE 等) 2. **准备 Environment**: - 加载配置文件(application.yml/properties) - 加载系统属性和环境变量 - 加载命令行参数 3. **创建 ApplicationContext**:根据应用类型创建合适的容器 4. **刷新上下文**: - 执行 BeanFactoryPostProcessor - 注册 BeanPostProcessor - 实例化单例 Bean 5. **启动应用**:调用 CommandLineRunner 和 ApplicationRunner ### 2.2 @SpringBootApplication 注解 ```java @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @SpringBootConfiguration // 标识为配置类 @EnableAutoConfiguration // 启用自动配置 @ComponentScan( // 组件扫描 excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) } ) public @interface SpringBootApplication { // ... } ``` **注解分解:** 1. **@SpringBootConfiguration**:本质上就是 @Configuration,标识这是一个配置类 2. **@EnableAutoConfiguration**:开启自动配置,是 Spring Boot 的核心 3. **@ComponentScan**:自动扫描当前包及其子包下的 @Component、@Service、@Repository 等 ### 2.3 SpringApplication 核心方法 ```java public class SpringApplication { /** * 创建 SpringApplication 实例 */ public SpringApplication(Class<?>... primarySources) { this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources)); this.webApplicationType = WebApplicationType.deduceFromClasspath(); this.bootstrapRegistryInitializers = new ArrayList<>( getSpringFactoriesInstances(BootstrapRegistryInitializer.class)); setInitializers(getSpringFactoriesInstances(ApplicationContextInitializer.class)); setListeners(getSpringFactoriesInstances(ApplicationListener.class)); } /** * 运行应用 */ public ConfigurableApplicationContext run(String... args) { long startTime = System.currentTimeMillis(); // 1. 创建 BootstrapContext DefaultBootstrapContext bootstrapContext = createBootstrapContext(); // 2. 准备 Environment ConfigurableEnvironment environment = prepareEnvironment(bootstrapContext, args); // 3. 创建 ApplicationContext prepareContext(bootstrapContext, environment, args); // 4. 刷新上下文 context = refreshContext(context); // 5. 调用 Runner callRunners(context, args); return context; } } ``` --- ## 3. 自动配置机制 ### 3.1 自动配置原理 ```mermaid graph TD A[启动应用] --> B[加载主配置类] B --> C["@EnableAutoConfiguration"] C --> D[Import AutoConfigurationImportSelector] D --> E[读取 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports] E --> F[获取所有候选配置类] F --> G["@Conditional 注解过滤"] G --> H[过滤掉不符合条件的配置] H --> I[应用生效的配置] I --> J[注册到 Spring 容器] ``` **自动配置原理详解:** Spring Boot 的自动配置通过以下步骤实现: 1. **@EnableAutoConfiguration** 注解导入 `AutoConfigurationImportSelector` 2. `AutoConfigurationImportSelector` 从 `META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports` 文件中读取所有自动配置类 3. 对每个自动配置类,根据 `@Conditional` 系列注解进行过滤,只注册符合条件的 Bean 4. 将过滤后的自动配置类注册到 Spring 容器 ### 3.2 条件注解体系 | 注解 | 说明 | 应用场景 | | --- | --- | --- | | `@ConditionalOnClass` | 类路径中存在指定类 | 检测是否引入了某个依赖 | | `@ConditionalOnMissingClass` | 类路径中不存在指定类 | 避免与已有配置冲突 | | `@ConditionalOnBean` | 容器中存在指定 Bean | 依赖其他组件存在 | | `@ConditionalOnMissingBean` | 容器中不存在指定 Bean | 提供默认实现,允许覆盖 | | `@ConditionalOnProperty` | 配置文件中有指定属性 | 根据配置开启/关闭功能 | | `@ConditionalOnExpression` | SpEL 表达式为 true | 复杂条件判断 | | `@ConditionalOnWebApplication` | Web 应用环境 | 仅在 Web 应用中生效 | | `@ConditionalOnNotWebApplication` | 非 Web 应用环境 | 仅在非 Web 应用中生效 | | `@ConditionalOnJava` | 指定 Java 版本 | 版本兼容性控制 | | `@ConditionalOnResource` | 资源存在 | 检查配置文件是否存在 | ### 3.3 条件注解匹配原理 ```java /** * 条件注解的底层实现 */ public interface Condition { /** * 判断条件是否匹配 */ boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata); } /** * 具体实现示例:@ConditionalOnClass */ class OnClassCondition extends SpringBootCondition { @Override public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { // 获取注解属性 List<String> onClasses = getClasses(metadata, "value"); List<String> onMissingClasses = getClasses(metadata, "missing"); // 检查类是否存在 boolean match = checkClasses(context.getClassLoader(), onClasses, true) && checkClasses(context.getClassLoader(), onMissingClasses, false); return match ? ConditionOutcome.match() : ConditionOutcome.noMatch("Required classes not found"); } } ``` ### 3.4 自定义自动配置 ```java @Configuration @ConditionalOnClass(DataSource.class) @EnableConfigurationProperties(DataSourceProperties.class) public class DataSourceAutoConfiguration { @Bean @ConditionalOnMissingBean public DataSource dataSource(DataSourceProperties properties) { HikariDataSource dataSource = new HikariDataSource(); dataSource.setJdbcUrl(properties.getUrl()); dataSource.setUsername(properties.getUsername()); dataSource.setPassword(properties.getPassword()); dataSource.setDriverClassName(properties.getDriverClassName()); return dataSource; } } ``` ### 3.5 禁用自动配置 ```java @SpringBootApplication( exclude = {DataSourceAutoConfiguration.class, RedisAutoConfiguration.class} ) public class DemoApplication { // ... } ``` ```yaml # application.yml 方式 spring: autoconfigure: exclude: - org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration - org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration ``` ### 3.6 自动配置报告 启用自动配置报告,查看哪些配置生效/未生效: ```yaml # application.yml debug:true ``` --- ## 4. Starter 机制 ### 4.1 Starter 原理 ```mermaid graph LR A[Starter 依赖] --> B[引入自动配置] A --> C[引入所需依赖] B --> D[自动配置类] B --> E[条件注解] C --> F[第三方库] C --> G[Spring 组件] D --> H[配置类注册] E --> I[按需生效] ``` **Starter 设计原理:** 1. **依赖聚合**:Starter 是一个空的 JAR 包,主要用于聚合相关依赖 2. **自动配置**:通过 `META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports` 文件注册自动配置类 3. **条件装配**:自动配置类通过 `@Conditional` 注解实现按需装配 **命名规范:** - 官方 Starter:`spring-boot-starter-*` - 第三方 Starter:`*-spring-boot-starter` ### 4.2 常用 Starter | Starter | 功能描述 | | --- | --- | | spring-boot-starter-web | Web 应用开发,包含 Tomcat、Spring MVC | | spring-boot-starter-data-jpa | JPA 数据访问 | | spring-boot-starter-data-redis | Redis 数据访问 | | spring-boot-starter-security | 安全认证 | | spring-boot-starter-aop | AOP 面向切面编程 | | spring-boot-starter-test | 测试支持 | | spring-boot-starter-actuator | 监控和健康检查 | | spring-boot-starter-validation | 数据校验 | | mybatis-spring-boot-starter | MyBatis 集成 | ### 4.3 自定义 Starter ### 目录结构 ``` my-spring-boot-starter/ ├── pom.xml ├── src/ │ └── main/ │ ├── java/ │ │ └── com/example/starter/ │ │ ├── MyStarterAutoConfiguration.java │ │ ├── MyStarterProperties.java │ │ └── MyService.java │ └── resources/ │ └── META-INF/ │ └── spring/ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports ``` ### 引入依赖文件 创建Maven项目,pom.xml里引入spring-boot-starter作为基础依赖 ```xml <project ...> <groupId>com.example</groupId> <artifactId>my-starter</artifactId> <version>1.0.0</version> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies> </project> ``` ### 自动配置类 ```java @Configuration @EnableConfigurationProperties(MyStarterProperties.class) @ConditionalOnClass(MyService.class) @ConditionalOnProperty( prefix = "my.starter", name = "enabled", havingValue = "true", matchIfMissing = true ) public class MyStarterAutoConfiguration { @Bean @ConditionalOnMissingBean public MyService myService(MyStarterProperties properties) { return new MyService(properties); } } ``` ### 属性配置类 ```java @ConfigurationProperties(prefix = "my.starter") @Data public class MyStarterProperties { private String name = "default"; private Integer maxRetries = 3; private Boolean enabled = true; } ``` ### 注册文件 ``` com.example.starter.MyStarterAutoConfiguration ``` --- ## 5. 配置管理 ### 5.1 配置文件优先级 ```mermaid graph TD A[配置优先级] --> B[命令行参数] A --> C[操作系统环境变量] A --> D[Java 系统属性] A --> E[JAR 外部配置文件] A --> F[JAR 内部配置文件] A --> G["@PropertySource 注解"] A --> H[默认配置] B --> I[优先级最高] H --> J[优先级最低] ``` **配置加载顺序(从高到低):** 1. 命令行参数 2. 操作系统环境变量 3. Java 系统属性 4. JAR 外部配置文件 5. JAR 内部配置文件 6. `@PropertySource` 注解指定的配置文件 7. 默认属性 ### 5.2 配置文件格式 ```yaml # application.yml server: port:8080 servlet: context-path: /api spring: application: name: demo-application profiles: active: dev # 自定义配置 app: name: Demo App description: Spring Boot Demo config: timeout:3000 retry: max:3 delay:1000 ``` ### 5.3 多环境配置 ``` src/main/resources/ ├── application.yml # 默认配置 ├── application-dev.yml # 开发环境 ├── application-test.yml # 测试环境 └── application-prod.yml # 生产环境 ``` ```yaml # application.yml spring: profiles: active: ${ENV:dev} # 默认 dev,可通过环境变量覆盖 ``` ### 5.4 配置属性绑定 ```java /** * 方式1: @ConfigurationProperties * 适用场景:复杂配置,多级属性 */ @Component @ConfigurationProperties(prefix = "app") @Data @Validated public class AppConfig { @NotBlank(message = "应用名称不能为空") private String name; private String description; @Valid private Config config = new Config(); @Data public static class Config { @Min(value = 100, message = "超时时间至少100ms") private Integer timeout; @Valid private Retry retry = new Retry(); @Data public static class Retry { @Min(value = 1, message = "最大重试次数至少1次") @Max(value = 10, message = "最大重试次数不超过10次") private Integer max; private Long delay; } } } ``` ### 5.5 配置加密 ```xml <dependency> <groupId>com.github.ulisesbocchio</groupId> <artifactId>jasypt-spring-boot-starter</artifactId> <version>3.0.5</version> </dependency> ``` ```yml jasypt: encryptor: password: ${JASYPT_PASSWORD} # 从环境变量获取 algorithm: PBEWithMD5AndDES app: password: ENC(加密后的密码) ```
MyBatis-Plus 分页查询报错 BadSqlGrammarException:SELECT COUNT() 异常排查与解决
### 1. 现象描述 在进行优惠券业务开发时,调用分页查询接口 `/coupons/page` 突然抛出 `BadSqlGrammarException`。查看后台日志,发现 MyBatis-Plus 自动生成的 `COUNT` 语句出现了明显的语法错误: **报错核心信息:** ```text 根本原因:SQL 语法错误:SELECT COUNT() FROM coupon 中 COUNT() 缺少参数 数据库报错位置:near ') FROM coupon' at line 1 技术栈:Spring Boot + MyBatis-Plus (3.4.3) + MySQL ``` 正常的统计语句应该是 `SELECT COUNT(*)` 或 `SELECT COUNT(1)`,但程序生成的 SQL 括号内空空如也,导致 MySQL 解析失败。 --- ### 2. 原因分析 经过对代码和 MyBatis-Plus 源码逻辑的排查,该问题主要由以下两个因素共同导致: #### 2.1 MyBatis-Plus Count SQL 优化失败 MyBatis-Plus 在进行分页查询时,默认会开启 `optimizeCountSql`(Count 优化)。它会尝试通过 `jsqlparser` 解析原始 SQL,将其转化为更高效的统计语句。 然而,在 **MyBatis-Plus 3.4.3** 版本中,当实体类(PO)中包含特殊的字段映射(如使用了反引号包裹的保留字 `` `name` ``、`` `specific` ``)时,优化器可能无法正确推断出统计参数,从而生成了非法的 `COUNT()`。 #### 2.2 实体类中的保留字冲突 在项目的Coupon类po中可以看到,表中多个字段使用了 MySQL 关键字: ```java @TableField("`name`") private String name; @TableField("`specific`") private Boolean specific; ``` 虽然在 SQL 中使用了反引号规避,但在复杂的自动生成场景下,这增加了解析器出错的概率。 --- ### 3. 解决方案 #### 禁用 Count 优化 最直接且稳妥的解决办法是针对该分页请求禁用 Count 优化。禁用后,MyBatis-Plus 将不再尝试简化统计 SQL,而是使用最通用的 `SELECT COUNT(*) FROM (原查询SQL) AS total` 方式进行统计。 --- ### 4. 代码实现 以下是重构后的分页查询逻辑: ```java @Override public PageDTO<CouponPageVO> pageQuery(CouponQuery query) { // 1. 构建分页对象 Page<Coupon> page = query.toMpPageDefaultSortByCreateTimeDesc(); // 2. 关键修复:针对某些场景下 COUNT() 生成非法 SQL 的问题,手动禁用 count 优化 page.setOptimizeCountSql(false); // 3. 构建查询条件 String name = query.getName(); Integer type = query.getType(); Integer status = query.getStatus(); LambdaQueryWrapper<Coupon> wrapper = new LambdaQueryWrapper<>(); wrapper.like(StringUtils.isNotBlank(name), Coupon::getName, name) .eq(type != null, Coupon::getDiscountType, type) .eq(status != null, Coupon::getStatus, status); // 4. 执行分页查询 this.page(page, wrapper); // 5. 封装结果返回 if (CollUtils.isEmpty(page.getRecords())) { return PageDTO.empty(page); } return PageDTO.of(page, CouponPageVO.class); } ``` --- ### 5. 总结 1. **谨慎对待关键字**:数据库设计时应尽量避开 `name`, `status`, `specific` 等关键字。若必须使用,务必在实体类映射时加上反引号。 2. **版本避坑**:MyBatis-Plus 3.4.x 系列的 Count 优化在某些复杂场景下确实存在不稳定性。如果遇到 `BadSqlGrammarException` 且指向 Count 语句,优先考虑通过 `page.setOptimizeCountSql(false)` 绕过。
手写 Spring AI Advisor:解耦 ChatMemory 与历史记录,实现聊天记录持久化存储
> **前言**: > 在上一篇文章 [《Spring AI + MySQL 实现会话记忆持久化》](https://www.codefather.cn/post/2011766454579478529) 中,我们成功实现了将 ChatMemory 接入 MySQL,让对话数据不再随服务重启而丢失。 > > 但随着项目测试运行,我发现了一个**致命问题**:**数据库里的聊天记录竟然变少了!** 当对话轮数超过我们在 `MessageWindowChatMemory` 中设置的 `maxMessages`(例如 50 条)时,最早的记录竟然从数据库中凭空消失了。 > > **这不仅不符合“持久化”的初衷,更导致我们无法进行后续的数据分析和历史回溯。** > > 本文将带你深入 Spring AI 源码,揭秘这个“数据消失术”的根本原因,并手把手教你通过重写核心组件,实现 **“数据库永久保存全量历史”** 与 **“大模型只传最近 N 条上下文”** 的完美平衡。 --- ## 一、 数据消失原因 在 Spring AI 的默认设计中,我们通常使用 `MessageWindowChatMemory` 来管理会话上下文。它的核心作用是**限制发送给大模型的 Token 数量**,防止上下文超长。 但是,Spring AI 默认保存的是短期记忆,是大模型上下文里的内容 让我们直接打开 `MessageWindowChatMemory.java` 的源码(位于 `org.springframework.ai.chat.memory` 包下),看看 `add` 方法到底干了什么: ### 1.1 源码分析:`add` 方法 ```java @Override public void add(String conversationId, List<Message> messages) { Assert.hasText(conversationId, "conversationId cannot be null or empty"); Assert.notNull(messages, "messages cannot be null"); Assert.noNullElements(messages, "messages cannot contain null elements"); // 1. 从数据库(Repository)取出当前会话的所有消息 List<Message> memoryMessages = this.chatMemoryRepository.findByConversationId(conversationId); // 2. 【关键点】调用 process 方法处理消息(合并新消息 + 截断) List<Message> processedMessages = process(memoryMessages, messages); // 3. 将处理后的结果“覆盖”回数据库 this.chatMemoryRepository.saveAll(conversationId, processedMessages); } ``` **深度解读:** 1. **`findByConversationId`**:首先,它从底层的 Repository(比如 MySQL)中取出了当前会话的所有历史消息。假设现在数据库里有 50 条。 2. **`process`**:接着,它调用了内部的 `process` 方法。这里是逻辑的核心,它将旧消息和新消息合并,并根据窗口大小进行处理。 3. **`saveAll`**:最后,也是最关键的一步,它将 `process` 处理后的结果**全量覆盖**回数据库。注意这里是覆盖操作,如果 `process` 方法丢弃了某些消息,那么这些消息在数据库里也会被同步删除。 ### 1.2 源码分析:`process` 方法(罪魁祸首) ```java private List<Message> process(List<Message> memoryMessages, List<Message> newMessages) { // ... 省略部分合并逻辑 ... // 合并旧消息和新消息 processedMessages.addAll(newMessages); // 如果总数没超过 maxMessages,直接返回 if (processedMessages.size() <= this.maxMessages) { return processedMessages; } // 【致命逻辑】如果超过了 maxMessages,开始移除旧消息! int messagesToRemove = processedMessages.size() - this.maxMessages; List<Message> trimmedMessages = new ArrayList<>(); int removed = 0; for (Message message : processedMessages) { // 如果是 SystemMessage 或者 已经删够了数量,才保留 if (message instanceof SystemMessage || removed >= messagesToRemove) { trimmedMessages.add(message); } else { removed++; // 计数器+1,这条消息被丢弃了,不会加入 trimmedMessages } } return trimmedMessages; // 返回的是“阉割”后的列表 } ``` **深度解读:** 假设我们将 `maxMessages` 设置为 **50**,且数据库中已经存储了 **50** 条历史消息。此时,用户发送了 **1** 条新消息。 1. **全量聚合**: * `processedMessages.addAll(newMessages)`:代码首先将新消息追加到旧消息列表中。 * **现状**:此时内存中的 `processedMessages` 列表共有 **51** 条消息(Index 0 ~ 50)。 2. **阈值检查**: * `if (processedMessages.size() <= this.maxMessages)`:这是流程的分水岭。 * **判定**:51 > 50,条件不满足。程序意识到消息超载,必须启动“裁员”流程。 3. **计算“裁员”指标**: * `int messagesToRemove = 51 - 50 = 1;` * **目标**:必须从列表中剔除 **1** 条最旧的消息,才能满足窗口限制。 4. **滑动窗口筛选**: * 这是最核心的逻辑,代码使用 `for` 循环从头(最旧的消息)开始遍历,配合 `removed` 计数器决定每条消息的命运。 * **第一轮循环(Index 0,最旧的一条消息)**: * 它是 `SystemMessage` 吗?**否**(假设是普通对话)。 * `removed` (0) >= `messagesToRemove` (1) 吗?**否**(还没删够)。 * **结局**:进入 `else` 分支,`removed` 自增变为 1,**该消息被丢弃**,没有加入 `trimmedMessages`。 * **第二轮循环(Index 1,次旧消息)**: * 它是 `SystemMessage` 吗?**否**。 * `removed` (1) >= `messagesToRemove` (1) 吗?**是**(指标已达标)。 * **结局**:进入 `if` 分支,**该消息被保留**,加入 `trimmedMessages`。 * **后续循环**:因为 `removed` 已经达标,后续所有消息都会满足 `removed >= messagesToRemove` 条件,从而被全部保留。 5. **最终审判**: * 方法返回的 `trimmedMessages` 仅包含 **后 50 条** 消息。 * **致命后果**:这个“阉割版”的列表随后会在 `add` 方法中被直接 `saveAll` 回数据库。**这意味着,数据库中存储的最早那 1 条历史记录,被物理删除了!** 这就是“数据消失术”的底层真相。 流程闭环了:`add` 方法取出了 50 条,合并了 1 条新消息变成 51 条,传给 `process` 方法。`process` 方法一看超了,把第 1 条删了,返回后 50 条。最后 `add` 方法把这后 50 条存回数据库。 ### 1.3 痛点总结 看懂了吗?流程是这样的: 1. **读出来**:把数据库里的 50 条记录读出来。 2. **加进去**:加上用户刚发的 1 条新消息,现在有 51 条。 3. **切一刀**:`process` 方法发现 51 > 50,于是把**第 1 条**旧消息删掉了,只保留后 50 条。 4. **存回去**:调用 `repository.saveAll`。对于 `JdbcChatMemoryRepository` 来说,它会把这个会话 ID 下的数据更新为这 50 条。 **结果:** 数据库里永远只有最近的 50 条,第 51 条之前的历史记录**永久丢失**。这对于需要审计、回溯、数据分析的业务系统来说,是不可接受的。 --- ## 二、 思路分析 ### 2.1 官方文档的启示 Spring AI 官方文档中其实隐晦地提到过:**ChatMemory 的设计初衷并不是作为持久化的聊天记录存储方案**。  这种方案的设计目的是**管理模型上下文的短期记忆(Short-term Memory)**,而不是**整个聊天记录(Chat History / Audit Log)**。 * **短期记忆 (Short-term Memory)**:这是**模型层面**的概念。短期记忆存在于模型上下文中的,受限于 LLM 的 Context Window(上下文窗口,如 4k, 8k, 128k Token),我们无法将所有历史对话都喂给模型。因此,必须通过 `MessageWindowChatMemory` 等机制进行“截断”,只保留最近的 N 轮对话,拼接在 Prompt 中供模型即时调用,以维持对话的连贯性。 * **聊天记录 (Chat History)**:这是**业务层面**的概念。它是项目业务需求的一部分,类似于系统日志或审计日志。它的要求是**全量保存、永久存储、不可丢失**,用于后续的用户历史查看、数据分析、模型调优等。它不应该受到模型 Token 限制的影响。 **结论**:**我们不应该试图修改 Spring AI 实现 `ChatMemory` 的源码**(如修改 `process` 方法不去删除旧数据),因为那违背了它的设计初衷(控制上下文大小)。如果强行修改,会导致发送给大模型的 Prompt 无限膨胀,最终撑爆 Token 限制或消耗巨额 Token 费用。 ### 2.2 解决思路:AOP 与 Advisor 既然我们明确了目标——**在业务层面实现聊天记录的持久化**,那么我们完全可以跳出 `ChatMemory` 的圈子,从大模型交互的本质入手。 **大模型本质上就是一个输入(Input)输出(Output)的函数工具。**  Spring AI 的核心架构设计中,大量使用了 **Advisor(增强器)** 模式。这是一种典型的 AOP(面向切面编程)思想。通过 Advisors,我们可以拦截对大模型的每一次请求和每一次响应,对其进行增强、修改或**记录**。  上图是 Spring AI 文档中对于 Advisors 的介绍。可以看到,Advisor 处于 `ChatClient` 和底层 `ChatModel` 之间,像关卡一样层层拦截递归执行。 **核心思路**: 我们不需要动 `ChatMemory`,而是编写一个自定义的 **Advisor**。 1. **拦截请求**:在用户发送 Prompt 给大模型之前,拦截请求,提取出用户的提问,保存到数据库。 2. **拦截响应**:在大模型生成回复返回给用户之后(或流式传输过程中),拦截响应,提取出 AI 的回答,保存到数据库。 这样,**“短期记忆管理”交给 `ChatMemory`(负责截断),“长期历史记录”交给 `Advisor`(负责全量落库)**。两者职责分离,互不干扰,完美解决了我们的问题。 --- ## 三、 自定义 Advisor 实战 ### 3.1 Advisor 接口详解 Spring AI 的文档中详细介绍了如何实现一个自定义 Advisor。 > **Implementing an Advisor** > To create an advisor, implement either `CallAdvisor` or `StreamAdvisor` (or both). The key method to implement is `nextCall()` for non-streaming or `nextStream()` for streaming advisors. 我们需要实现两个核心接口(通常为了同时支持流式和非流式调用,两个都要实现): * **`CallAdvisor`**:用于拦截普通的同步调用(`call`)。核心方法是 `adviseCall`。 * **`StreamAdvisor`**:用于拦截流式调用(`stream`)。核心方法是 `adviseStream`。 ### 3.2 官方示例解读:SimpleLoggerAdvisor Spring AI 提供了一个 `SimpleLoggerAdvisor` 的示例,用于打印请求和响应日志。这正是我们需要参考的模板,因为“打印日志”和“保存日志到数据库”在逻辑上是完全一样的,只是输出目的地不同。  让我们深入解析一下官方的这个示例代码: ```java public class SimpleLoggerAdvisor implements CallAdvisor, StreamAdvisor { private static final Logger logger = LoggerFactory.getLogger(SimpleLoggerAdvisor.class); @Override public String getName() { return this.getClass().getSimpleName(); // Advisor 的唯一名称 } @Override public int getOrder() { return 0; // 执行顺序,数字越小越先执行 } // 1. 拦截非流式调用 @Override public ChatClientResponse adviseCall(ChatClientRequest chatClientRequest, CallAdvisorChain callAdvisorChain) { // 在调用大模型之前:记录请求 logRequest(chatClientRequest); // 执行链条中的下一个 Advisor 或最终的大模型调用 ChatClientResponse chatClientResponse = callAdvisorChain.nextCall(chatClientRequest); // 在大模型返回之后:记录响应 logResponse(chatClientResponse); return chatClientResponse; } // 2. 拦截流式调用 @Override public Flux<ChatClientResponse> adviseStream(ChatClientRequest chatClientRequest, StreamAdvisorChain streamAdvisorChain) { // 在流开始之前:记录请求 logRequest(chatClientRequest); // 执行流式请求,得到响应流 (Flux) Flux<ChatClientResponse> chatClientResponses = streamAdvisorChain.nextStream(chatClientRequest); // 【关键点】聚合流式响应 // 流式响应是一段一段回来的,我们无法直接打印完整的回复。 // ChatClientMessageAggregator 工具类可以“旁路”收集所有的流片段,拼成一个完整的 Response,供我们处理。 // 注意:这里的操作是异步的,不会阻塞返回给前端的流。 return new ChatClientMessageAggregator().aggregateChatClientResponse(chatClientResponses, this::logResponse); } // ... 省略 logRequest 和 logResponse 的具体实现 ... } ``` **深度解析:** 1. **`getOrder()`**:控制 Advisor 的执行顺序。这在多个 Advisor 串联时非常重要(后面我们会利用这一点)。 2. **`adviseCall`**:这是经典的“环绕通知”。你可以在 `nextCall` 之前拿到 `chatClientRequest`(用户的输入),在 `nextCall` 之后拿到 `chatClientResponse`(AI 的输出)。 3. **`adviseStream` 与 `MessageAggregator`**:这是难点。流式调用返回的是 `Flux<ChatClientResponse>`,里面的内容是破碎的字符(Chunk)。如果我们想保存完整的 AI 回复,不能每收到一个字就存一次数据库。`ChatClientMessageAggregator` 是 Spring AI 提供的神器,它能帮我们在流传输的同时,在后台悄悄把碎片拼凑起来,等流结束时触发回调(`this::logResponse`),让我们拿到完整的文本。 --- ## 四、 核心实现:ChatHistoryRecordAdvisor 基于上述分析,我们现在来实现自己的 **`ChatHistoryRecordAdvisor`**。它的目标是:**拦截对话,将“用户提问”和“AI 回答”持久化到 MySQL 的 `spring_ai_chat_message_log` 表中**。 ### 4.1 完整代码 ```java import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.chat.client.ChatClientMessageAggregator; import org.springframework.ai.chat.client.ChatClientRequest; import org.springframework.ai.chat.client.ChatClientResponse; import org.springframework.ai.chat.client.advisor.api.CallAdvisor; import org.springframework.ai.chat.client.advisor.api.CallAdvisorChain; import org.springframework.ai.chat.client.advisor.api.StreamAdvisor; import org.springframework.ai.chat.client.advisor.api.StreamAdvisorChain; import org.springframework.ai.chat.memory.ChatMemory; import org.springframework.ai.chat.messages.Message; import org.springframework.ai.chat.messages.MessageType; import org.springframework.core.Ordered; import org.springframework.stereotype.Component; import reactor.core.publisher.Flux; import java.time.LocalDateTime; /** * 聊天历史记录增强器 * 用于拦截 ChatClient 的请求和响应,并将对话记录持久化到数据库中。 */ @Component @RequiredArgsConstructor @Slf4j public class ChatHistoryRecordAdvisor implements CallAdvisor, StreamAdvisor, Ordered { private final ISpringAiChatMessageLogService messageLogService; private final ISpringAiChatRecordService chatRecordService; @Override public String getName() { return this.getClass().getSimpleName(); } /** * 设置最高优先级 (HIGHEST_PRECEDENCE) * 目的:确保在其他 Advisor(如 RAG 的 QuestionAnswerAdvisor)之前执行。 * 这样我们可以获取到用户最原始的 Prompt,而不是被 RAG 修改/拼接后的 Prompt。 */ @Override public int getOrder() { return Ordered.HIGHEST_PRECEDENCE; } /** * 拦截非流式调用 */ @Override public ChatClientResponse adviseCall(ChatClientRequest chatClientRequest, CallAdvisorChain callAdvisorChain) { // 1. 提取原始的用户请求文本(在 RAG 修改之前) String originalUserText = extractUserText(chatClientRequest); // 2. 放行请求,继续执行后续的 Advisor 链和 AI 调用 ChatClientResponse response = callAdvisorChain.nextCall(chatClientRequest); // 3. AI 响应回来后,记录日志(包括用户问题和 AI 回答) saveLog(chatClientRequest, response, originalUserText); return response; } /** * 拦截流式调用 */ @Override public Flux<ChatClientResponse> adviseStream(ChatClientRequest chatClientRequest, StreamAdvisorChain streamAdvisorChain) { // 1. 同样先提取原始用户文本 String originalUserText = extractUserText(chatClientRequest); // 2. 执行流式请求 Flux<ChatClientResponse> responseFlux = streamAdvisorChain.nextStream(chatClientRequest); // 3. 聚合流式响应,以便获取完整的 AI 回答内容进行存储 // 注意:这里不会阻塞流的返回,而是利用 Reactor 的副作用进行异步记录 return new ChatClientMessageAggregator().aggregateChatClientResponse(responseFlux, aggregatedResponse -> { saveLog(chatClientRequest, aggregatedResponse, originalUserText); }); } /** * 从请求中提取最后一条用户消息的内容 */ private String extractUserText(ChatClientRequest request) { try { return request.prompt().getInstructions().stream() .filter(m -> m.getMessageType() == MessageType.USER) .reduce((first, second) -> second) // 获取最后一条,通常是当前用户的提问 .map(Message::getText) .orElse(""); } catch (Exception e) { log.warn("Failed to extract user text", e); return ""; } } /** * 保存聊天日志到数据库 * * @param request 请求对象,包含上下文信息(如 sessionId) * @param response 响应对象,包含 AI 的回答 * @param userText 提取出的用户原始问题 */ private void saveLog(ChatClientRequest request, ChatClientResponse response, String userText) { try { // 1. 获取会话 ID String sessionId = (String) request.context().get(ChatMemory.CONVERSATION_ID); if (sessionId == null) { return; } // 2. 尝试获取用户 ID (从 ChatRecord 表中查找) Long userId = null; SpringAiChatRecord chatRecord = chatRecordService.getById(sessionId); if (chatRecord != null && chatRecord.getUserId() != null) { try { userId = Long.parseLong(chatRecord.getUserId()); } catch (NumberFormatException e) { log.warn("Invalid user ID format: {}", chatRecord.getUserId()); } } // 3. 保存用户提问日志 if (userText != null && !userText.isEmpty()) { SpringAiChatMessageLog userLog = new SpringAiChatMessageLog() .setSessionId(sessionId) .setUserId(userId) .setMessageType("USER") .setContent(userText) .setCreateTime(LocalDateTime.now()); messageLogService.save(userLog); } // 4. 保存 AI 回复日志 if (response.chatResponse() != null && response.chatResponse().getResult() != null && response.chatResponse().getResult().getOutput() != null) { String assistantContent = response.chatResponse().getResult().getOutput().getText(); SpringAiChatMessageLog assistantLog = new SpringAiChatMessageLog() .setSessionId(sessionId) .setUserId(userId) .setMessageType("ASSISTANT") .setContent(assistantContent) .setCreateTime(LocalDateTime.now()); messageLogService.save(assistantLog); } } catch (Exception e) { log.error("Error saving chat history log", e); } } } ``` ### 4.2 深度代码解析 这段代码虽然不长,但每一处设计都暗藏玄机。 #### 1. 为什么 `getOrder()` 要返回 `Ordered.HIGHEST_PRECEDENCE`? ```java @Override public int getOrder() { return Ordered.HIGHEST_PRECEDENCE; } ``` **这是最关键的一点!** 在 Spring AI 中,Advisor 是链式执行的。如果你使用了 RAG(检索增强生成),通常会有一个 `QuestionAnswerAdvisor`。这个 RAG Advisor 做的事情是:拿到你的问题 -> 去向量数据库搜索 -> 把搜索结果拼接成一段很长的 Prompt -> 替换掉你的原始问题 -> 传给大模型。 如果我们不设置最高优先级(让我们的 Advisor **第一个**执行),那么我们拦截到的 `ChatClientRequest` 里包含的可能就不是用户原本写的“你好”,而是一大段包含了上下文文档的 RAG Prompt。保存那样的日志对用户来说是不可读的。 设置 `HIGHEST_PRECEDENCE` 确保了我们在任何 Prompt 修改发生**之前**,就截获了用户的原始输入。 #### 2. `extractUserText` 的逻辑 ```java request.prompt().getInstructions().stream() .filter(m -> m.getMessageType() == MessageType.USER) .reduce((first, second) -> second) ``` 一个 Prompt 请求中可能包含多条消息(System Message, User Message, Assistant Message...)。我们只关心**用户当前说的这句话**。 * `.filter(...)`:只筛选用户消息。 * `.reduce(...)`:取最后一条。因为在携带历史上下文的情况下,Prompt 里可能有之前几轮的 User Message,但最后一条肯定才是当前最新的提问。 #### 3. `adviseStream` 的无感聚合 ```java return new ChatClientMessageAggregator().aggregateChatClientResponse(responseFlux, aggregatedResponse -> { saveLog(chatClientRequest, aggregatedResponse, originalUserText); }); ``` 这里使用了 Reactor 的响应式编程技巧。`aggregateChatClientResponse` 方法会返回一个新的 Flux,这个 Flux 对前端来说和原来一样,依然是流式的。但在服务器端内部,它挂载了一个“钩子”,等所有流数据跑完后,它会把拼好的完整结果传给我们的 lambda 表达式。 这样做的好处是:**日志记录完全不影响用户的首字延迟(Time to First Token)**,用户依然可以秒看流式输出,而我们的落库操作是在流结束后的异步线程中完成的。 ##### 注意:这段代码中没有实现Function Calling调用的存储逻辑,可以自行实现。 --- ## 五、 数据库设计与配置 ### 5.1 数据库表结构 为了配合上述 Advisor,我们需要一张表来存储日志。这里提供一个标准的 SQL 建表语句: ```sql -- 用户聊天记录日志表 create table spring_ai_chat_message_log ( id bigint auto_increment comment '主键ID' primary key, session_id varchar(50) not null comment '会话ID,对应 ChatMemory 中的 conversationId', user_id bigint unsigned null comment '用户ID,用于关联业务用户', message_type varchar(20) not null comment '消息类型: USER (提问), ASSISTANT (回答)', content longtext null comment '消息内容,使用 LongText 防止长文本截断', tool_calls longtext null comment '工具调用信息(JSON),预留字段', create_time timestamp default CURRENT_TIMESTAMP not null comment '创建时间' ) comment '用户聊天记录日志表'; -- 建立索引,加速查询 create index idx_session_id on spring_ai_chat_message_log (session_id); create index idx_user_id on spring_ai_chat_message_log (user_id); ``` **设计要点**: * **`session_id`**:这是关联上下文的核心,必须有索引。 * **`content`**:一定要用 `longtext`。大模型的回复(特别是写代码或写文章时)很容易超过 `varchar` 的限制。 * **`tool_calls`**:这是一个预留字段。如果你的大模型使用了 Function Calling(工具调用),你可能还想记录它调用了什么工具、传了什么参数。目前的实现暂未包含此逻辑,可根据业务自行扩展。 ### 5.2 配置 ChatClient 万事俱备,只欠东风。我们需要将写好的 `ChatHistoryRecordAdvisor` 注册到全局的 `ChatClient` 中。 ```java @Configuration public class ChatConfig { @Bean public ChatClient serviceChatClient(AlibabaOpenAiChatModel model, ChatMemory chatMemory, VectorStore vectorStore, ChatHistoryRecordAdvisor chatHistoryRecordAdvisor) { // 注入我们的 Advisor return ChatClient.builder(model) .defaultAdvisors( // 1. 日志 Advisor (官方) SimpleLoggerAdvisor.builder().build(), // 2. 【核心】我们要添加的历史记录 Advisor chatHistoryRecordAdvisor, // 3. 上下文记忆 Advisor (负责短期记忆截断) MessageChatMemoryAdvisor.builder(chatMemory).build(), // 4. RAG 检索增强 Advisor QuestionAnswerAdvisor.builder(vectorStore) .searchRequest(SearchRequest.builder().similarityThreshold(0.5d).topK(1).build()) .build() ) // 设置系统 Prompt .defaultSystem("你是一个智能助手...") .build(); } } ``` **配置详解**: * **注入顺序**:虽然我们在 `ChatHistoryRecordAdvisor` 代码里写了 `HIGHEST_PRECEDENCE`,但在 `defaultAdvisors` 方法中显式添加它依然是必要的。 * **共存关系**:请注意,`ChatHistoryRecordAdvisor` 和 `MessageChatMemoryAdvisor` 是**同时存在**的。 * `ChatHistoryRecordAdvisor`:负责把**每一句话**都完整地记入数据库,不做任何删除。 * `MessageChatMemoryAdvisor`:负责维护一个**滑动窗口**(比如最近 10 条),只把这 10 条发给大模型。 * 两者配合,既保证了大模型不会上下文溢出,又保证了数据库里有永久的查阅记录。 --- ## 六、 总结 通过本文的探索,我们纠正了一个常见的误区:**不要试图用 ChatMemory 来做持久化的业务日志**。 1. **ChatMemory (短期记忆)**:它是给**大模型**看的。为了节省 Token,它必须健忘,必须丢弃旧消息。 2. **Advisor (长期日志)**:它是给**人**看的。利用 AOP 切面思想,我们在大模型的输入输出关口设立“哨兵”,忠实地记录下每一次对话。 这种**读写分离**的架构,不仅解决了“数据消失”的 Bug,还解耦了业务逻辑与 AI 框架逻辑,是构建生产级 AI 应用的最佳实践。 现在,你的数据库里不仅有了永远不会消失的对话历史,你的大模型也依然跑得飞快。这,就是架构的艺术。 --- > **相关阅读**: > [Spring AI + MySQL 实现会话记忆持久化:彻底搞懂 ChatMemoryRepository](https://www.codefather.cn/post/2011766454579478529) > [SpringAI1.1.2官方文档](https://docs.spring.io/spring-ai/reference/)
SpringBoot接收前端请求的参数方式
在 SpringBoot 中,后端接收前端请求的过程涉及请求类型、参数位置、数据格式等多个维度,核心是通过 Spring MVC 的参数绑定机制,将前端传递的数据自动映射到后端方法的参数中。以下从**请求方式、参数位置、数据格式**三个角度详细说明接收逻辑,并结合示例分析。 ### 一、先明确前端请求的核心要素 前端发送请求时,后端需要关注的关键信息: 1. **请求方式**:GET、POST、PUT、DELETE 等(决定参数传递的默认位置)。 2. **参数位置**:URL 路径(如 `/user/123`)、URL 查询参数(如 `?name=xxx`)、请求体(Body,如 JSON / 表单数据)。 3. **数据格式**: - 表单默认格式:`application/x-www-form-urlencoded`(键值对,如 `name=xxx&age=18`)。 - 表单文件格式:`multipart/form-data`(用于上传文件,可包含键值对 + 文件)。 - JSON 格式:`application/json`(主流,适合复杂数据结构)。 ``` { "username": "zhangsan", "password": "123456", "age": 20 } ``` - XML 格式:`application/xml`(较少用)。 ``` <class name="一班"> <students> <student> <name>张三</name> <age>18</age> </student> <student> <name>李四</name> <age>19</age> </student> </students> </class> ``` ### 二、按参数位置分类:接收不同位置的参数 #### 1. 接收 URL 路径中的参数(`@PathVariable`) 适用于 RESTful 风格接口,参数直接嵌入 URL 路径中(如 `/user/{id}`),需用 `@PathVariable` 注解绑定。 **示例**: - 前端请求:`GET /user/123/detail?type=simple`(路径中的 `123` 是参数) - 后端接收: ```java @GetMapping("/user/{userId}/detail") public String getUserDetail( @PathVariable("userId") Integer id, // 绑定路径中的 userId,映射到参数 id @RequestParam String type // 顺便接收查询参数 type ) { return "用户ID:" + id + ",类型:" + type; } ``` - 关键点: - 路径中的占位符 `{userId}` 必须与 `@PathVariable("userId")` 名称一致,若参数名与占位符相同,可省略括号内的名称(如 `@PathVariable Integer userId`)。 - 支持多个路径参数(如 `/user/{id}/order/{orderId}`)。 #### 2. 接收 URL 查询参数(`@RequestParam`) 参数以 `?key=value&key2=value2` 形式拼接在 URL 后,常见于 GET 请求,也可用于 POST 等请求。需用 `@RequestParam` 注解(参数名匹配时可省略)。 **示例**: - 前端请求:`GET /search?name=张三&page=1&size=10` - 后端接收: ```java @GetMapping("/search") public String search( @RequestParam(required = false) String name, // required=false:可选参数(默认 true) @RequestParam(defaultValue = "1") Integer page, // 默认值 Integer size // 省略 @RequestParam,自动匹配参数名 size ) { return "查询:" + name + ",页码:" + page + ",条数:" + size; } ``` `@RequestParam:` `value/name`:指定前端传递的参数名(解决前后端参数名不一致问题)。 示例:前端传 `user_name`,后端参数名是 `username`: ``` @GetMapping("/user") public String getUser(@RequestParam("user_name") String username) { return "用户名:" + username; // 正确接收 user_name 的值 } ``` - 关键点: - 若前端传递的参数名与后端参数名不一致,需用 `@RequestParam("前端参数名")` 映射(如前端传 `user_name`,后端用 `@RequestParam("user_name") String userName`)。 - 支持数组 / 集合(如前端传 `ids=1&ids=2`,后端用 `@RequestParam List<Integer> ids` 接收)。 #### 3. 接收请求体(Body)中的参数 参数放在请求体中,常见于 POST、PUT 等请求,根据数据格式不同,接收方式不同。 ##### (1)JSON 格式(`application/json`):必须用 `@RequestBody` 前端传递 JSON 字符串(适合复杂对象、嵌套结构),后端用 `@RequestBody` 将 JSON 自动映射为 Java 对象。 **示例**: - 前端请求:`POST /user/save`,请求体为 JSON: ```json { "name": "张三", "age": 20, "address": { "city": "北京", "street": "长安街" } } ``` - 后端定义实体类: ```java @Data // Lombok 注解,自动生成 getter/setter class User { private String name; private Integer age; private Address address; // 嵌套对象 } @Data class Address { private String city; private String street; } ``` - 后端接口接收: ```java @PostMapping("/user/save") public String saveUser(@RequestBody User user) { // 自动映射 JSON 到 User 对象 return "保存用户:" + user.getName() + ",城市:" + user.getAddress().getCity(); } ``` - 关键点: - JSON 字段名必须与 Java 对象属性名一致(大小写敏感),不一致可通过 `@JsonProperty` 注解映射(如 JSON 是 `user_name`,属性用 `@JsonProperty("user_name") String userName`)。 - 支持 JSON 数组(如 `[1,2,3]`,后端用 `@RequestBody List<Integer>` 接收)。 ##### (2)表单格式(`application/x-www-form-urlencoded`):无需 `@RequestBody` 前端传递键值对(如表单提交),参数在请求体中但格式为 `name=xxx&age=18`,后端可直接绑定到参数或对象(无需注解)。 **示例**: - 前端请求:`POST /user/login`,请求体为 `username=zhangsan&password=123`(Content-Type: application/x-www-form-urlencoded) - 后端接收: ```java // 方式1:绑定到单个参数 @PostMapping("/user/login") public String login(String username, String password) { return "用户名:" + username + ",密码:" + password; } // 方式2:绑定到对象(属性名与参数名一致) @PostMapping("/user/login") public String login(UserLoginDTO dto) { // dto 包含 username 和 password 属性 return "用户名:" + dto.getUsername(); } ``` - 关键点: - 若用 `@RequestBody` 接收该格式,会报错(`@RequestBody` 仅处理 JSON/XML 等格式)。 ##### (3)文件上传格式(`multipart/form-data`):`@RequestParam` + `MultipartFile` 用于上传文件,同时可包含普通键值对参数,后端用 `MultipartFile` 接收文件,普通参数用 `@RequestParam` 接收。 **示例**: - 前端请求:`POST /file/upload`,Content-Type: multipart/form-data,包含: - 普通参数:`desc=用户头像` - 文件:`file`(选择的图片文件) - 后端接收: ```java @PostMapping("/file/upload") public String uploadFile( @RequestParam String desc, // 普通参数 @RequestParam("file") MultipartFile file // 文件参数 ) throws IOException { String filename = file.getOriginalFilename(); // 获取文件名 file.transferTo(new File("D:/" + filename)); // 保存文件 return "上传成功:" + desc + ",文件名:" + filename; } ``` - 关键点: - 若上传多个文件,用 `List<MultipartFile>` 接收(如 `@RequestParam List<MultipartFile> files`)。 - 需在配置类中设置文件大小限制(默认有限制):yaml ```yaml spring: servlet: multipart: max-file-size: 10MB # 单个文件大小 max-request-size: 100MB # 总请求大小 ``` ### 三、特殊场景:参数绑定的进阶用法 #### 1. 接收请求头参数(`@RequestHeader`) 获取请求头中的信息(如 `token`、`User-Agent` 等)。 **示例**: ```java @GetMapping("/header") public String getHeader( @RequestHeader("token") String token, // 获取 token 头 @RequestHeader("User-Agent") String userAgent ) { return "token:" + token + ",浏览器:" + userAgent; } ``` #### 2. 接收 Cookie 参数(`@CookieValue`) 获取请求中的 Cookie 值。 **示例**: ```java @GetMapping("/cookie") public String getCookie(@CookieValue("sessionId") String sessionId) { return "SessionID:" + sessionId; } ``` #### 3. 自动绑定复杂对象(含嵌套 + 数组) 对于表单或查询参数中的复杂结构(如数组、嵌套对象),Spring 会自动映射到对应的 Java 对象。 **示例**: - 前端请求:`GET /query?name=张三&hobbies=篮球&hobbies=足球&address.city=北京` - 后端实体类: ```java @Data class QueryDTO { private String name; private List<String> hobbies; // 数组参数 private Address address; // 嵌套对象(address.city 对应参数) } ``` - 后端接口: ```java @GetMapping("/query") public String query(QueryDTO dto) { // 自动绑定所有参数 return "爱好:" + dto.getHobbies() + ",城市:" + dto.getAddress().getCity(); } ``` ### 四、核心原理:Spring 的参数解析机制 SpringBoot 能自动接收参数,本质是通过 **HandlerMethodArgumentResolver(方法参数解析器)** 完成的: 1. 当请求到达后端,DispatcherServlet 找到对应的 Controller 方法。 2. 遍历该方法的参数,根据参数类型、注解(如 `@RequestBody`、`@PathVariable`)匹配对应的解析器。 3. 解析器从请求中提取数据(路径、查询参数、请求体等),转换为参数所需的类型(如 JSON → 对象、字符串 → 整数)。 4. 将转换后的值注入方法参数,执行方法。 ### 总结 后端接收前端请求的核心是**明确参数位置和格式**,对应使用正确的注解和类型: - **URL 路径参数**:`@PathVariable` - **URL 查询参数 / 表单键值对**:`@RequestParam`(可省略)或直接绑定对象 - **JSON 格式请求体**:`@RequestBody` + 实体类 - **文件上传**:`@RequestParam` + `MultipartFile` - **请求头 / Cookie**:`@RequestHeader` / `@CookieValue`
Spring进阶 - Spring事务理论+实战,一文吃透事务
本文系统性介绍Spring事务,一贯坚持知识关联性的叙述风格。 首先要了解 Spring 事务在 Spring 架构中的定位:  可以看出来,Spring事务属于 Data Acess(数据访问模块) 的知识点。 Data Acess 是在**应用程序(Spring程序)**中,访问持久化数据存储(如关系型数据库、NoSQL、缓存)与业务/服务层之间进行解耦、简化样板代码、**统一事务**与异常处理的通用抽象层。形象理解 @Transactionnal 注解是无侵入的给业务代码加上了事务功能,这就是 Data Acess 模块带来的快乐之一,与外部中间件打交道进行了封装。 文本的目标是介绍 Data Acess 的 Transactions 。 fency: 从图中可以看出来,DataAccess 在 Core 模块之上,其实 Data Access 将 Core 模块作为底层能力支撑,而 Spring 事务是依赖于 `**spring-core**` 和 `**spring-beans**` 等核心模块,并与 `**spring-aop**` 模块紧密协作来实现 Spring 事务 ## 事务 ### 事务介绍 在计算机科学/数据管理系统中,一个“事务(transaction)”是指一系列操作组成的逻辑单元,它要么全部成功提交,要么全部失败回滚,从而保证系统状态从一个一致状态变到另一个一致状态。 举例:银行转账操作 —— 扣款和入账两个子操作构成一个事务。若其中一个失败,则两个操作都应回滚。 事务的四大特性(ACID):原子性、一致性、隔离性、持久性 - **原子性( Atomicity )**: 事务是一个不可分割的整体。事务中的所有操作要么全部成功,要么全部失败,不能只成功部分。若事务执行过程中某个操作失败,则要将之前已做的操作回滚,如同未执行过。 - **隔离性 (Isolation)**:当多个事务并发执行时(尤其在高并发系统中),一个事务不能看到其他事务未提交的中间状态;不同事务之间的执行效果应该与串行执行一致。这样可防止脏读、幻读、不可重复读等问题。 - **持久性 (Durability)**:一旦事务提交,其变更就应永久保存在系统中,即便系统崩溃、断电也不能丢失。通常通过日志、磁盘持久化机制实现。 - **一致性(Consistency)**:事务执行结束后,数据必须保持一致性状态。在事务执行期间,数据库中的数据可以处于中间状态,但在事务完成时必须保证数据的一致性。 落地技术: **原子性:**由 MySql 的 undolog 实现,undolog 可以在 发生错误/死锁中断 、 用户执行 ROLLBACK 时,将已做修改撤销,回滚操作就是由 undolog 实现的,因此可以**保证要么全成功,要么全失败**。 **持久性**:由 MySql 的 redolog 实现,注意 mysql 也是有数据缓冲区的,插入的数据放到数据缓冲区,定时或内存满了才会刷盘持久化(性能提升),如果还没持久化断电了,此时 redolog 就会发挥作用,redolog 记录了所有插入、更新、修改的操作,并在事务结束前刷盘 redolog , 确保redolog 必须持久化,从而保证了 MySQL 数据的持久化。 **隔离性:** 由 MySQL 的 MVCC 版本控制实现,有四种隔离级别,分别是读未提交、读已提交、可重复读、串行化,隔离级别决定了事务中间数据的脏读、不可重复读、幻读等问题。MySQL 默认隔离级别是 MVCC + 间隙锁能够解决幻读。 **一致性:**因为实现了以上三种特性,所以一定满足一致性,这是一种因果关系,数据一致性是事务的终极目标。 ### 事务类型:本地事务 VS 分布式事务 我们讨论的经常是本地事务,也就是单个数据源内执行的事务,这种事务由数据库自行管理,为开发者提供事务能力。 分布式事务涉及多个数据源(包括数据库、消息队列等),需要跨多个资源管理器(如数据库)进行协调,实现起来比较复杂,通常需要应用服务器(如Spring Cloud Gateway)或者JTA(Java Transcation API)支持 事务的概念不仅仅应用于 MySQL 数据,在以下系统和组件都有应用: | 系统/组件 | 事务支持 | 说明 | | ----------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | | **关系型数据库** | **完全支持** ACID 特性。 | 如 MySQL、PostgreSQL、Oracle 等,是事务最经典的应用场景。 | | **消息队列** | 部分支持(如 RocketMQ 的事务消息)。 | 保证消息发送与本地事务的原子性,确保消息不丢失或重复消费。 | | **Redis** | 提供**有限的事务支持**(通过 `**MULTI**`/`**EXEC**`命令) 。 | **不保证原子性**(命令执行失败不会回滚)和**隔离性**(事务执行期间其他客户端命令可能插入)。通常用于简单批量操作,或结合 Lua 脚本实现原子操作 。 | | **Elasticsearch** | **不支持**传统意义上的 ACID 事务。 | 更侧重于最终一致性和文档级别的原子操作。 | ### 事务传播行为的概念 事务传播行为: 一个事务调用另一个事务如何处理(加入 , 挂起,禁止加入,嵌套) [维基百科](https://en.wikipedia.org/wiki/Database_transaction?utm_source=chatgpt.com)对事务的定义重点关注的是事务作为“独立单位”的概念,并没有牵扯事务传播行为,所以本质上事务传播行为不属于**标准事务**的知识领域,而是在应用层框架中运用的一种机制,在Spring中我们会看到“ 一个事务方法调用另一个事务方法,该子方法应如何参与或者不参与父事务 ”的情境。 注意区分,**事务传播行为是 Spring 事务的独有概念**,并不是数据库级事务定义的原生特性 。 ## Spring 事务 ### 介绍 Spring 事务是 **对底层事务 API(如 JDBC、JPA、Hibernate、JTA 等)的一层统一封装**。 通过它,开发者不需要关心底层数据库或框架的差异,只需用统一的编程方式来声明和管理事务。 **名词解释:** **JDBC**: JDBC 是 Java 提供给关系数据库访问的一个标准 API,定义了如何建立连接、发送 SQL、检索结果,仅仅定义了抽象接口,实现交给具体的关系数据库。 Spring 的事务管理机制能帮你管理 JDBC 的事务的提交、回滚,不必手写这些细节。 **JTA** : JTA 是 Java 规范中用于“分布式事务/多资源管理器事务” 的 API。 在一个事务内可能涉及多个资源(比如多个数据库、消息队列等)时,JTA 提供一个标准接口让事务管理器协调这些资源 。 **JPA** : JPA 是 Java 的一个对象–关系映射(ORM)规范/API,定义如何将 Java 对象映射为数据库表、如何做持久化、查询、事务等。 开发者能通过实体类(POJO)操作数据库,正是JPA发挥了作用。Spring 的事务适配 JPA 提供的事务模型 , 让你用 `@Transactional` 等方式控制 JPA 操作的事务边界 **Hibernate**:Hibernate 是一个最著名的 ORM 框架之一,在 Java 世界中用于将 Java 对象映射到关系数据库,并处理所需的 CRUD、查询、缓存、事务整合等。 Spring 的事务抽象也支持 Hibernate 的事务管理 **Spring 事务的优势** - **跨不同事务API的一致编程模型**:支持JTA、JDBC、Hibernate、JPA等 。 - **支持声明式事务管理**:通过AOP实现,极大简化事务管理 。 - **简化的编程式事务管理API**:比JTA等复杂API更易用 。 - **与Spring数据访问抽象完美集成**:无缝整合各种数据访问技术 。 ### Spring 事务的管理方式 Spring 提供了两种主要的事务管理方式:声明式事务管理,编程式事务管理。 #### 1、声明式事务管理(注解,xml) 基于注解或 XML 配置实现,无需在代码中显式编写事务逻辑, XML 声明事务过时了。 **常用注解:** ```java @Transactional public void transferMoney() { accountDao.debit("A", 100); accountDao.credit("B", 100); } ``` - 默认是 **运行时异常(RuntimeException)回滚** - 可以通过属性定制行为: ```java @Transactional( propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED, timeout = 30, rollbackFor = Exception.class ) ``` @Transactional 更多属性: (按功能分类讲,表格后我还会带例子) | 属性 | 作用 | 默认值 | 说明 | | --------------- | -------------------- | ------------------------------------------ | ------------------------------------------------------ | | **propagation** | 事务传播行为 | `Propagation.REQUIRED` | 指定当前方法在事务中的传播方式(是否新建事务等)。 | | **isolation** | 事务隔离级别 | `Isolation.DEFAULT` | 控制并发事务间的隔离强度。 | | **timeout** | 超时时间(秒) | `-1`(不超时) | 超过时间未完成则回滚。 | | **readOnly** | 是否只读事务 | `false` | 可用于优化查询性能(提示数据库不用加锁)。 | | **rollbackFor** | 指定哪些异常触发回滚 | 无(仅对 `RuntimeException`/ `Error`回滚) | 支持多个异常类,如 `rollbackFor = {Exception.class}`。 | #### 2、编程式事务管理 通过 `**TransactionTemplate**` (**重点**)或 `PlatformTransactionManager` 手动控制事务。比声明式事务更灵活、可控,但是代码入侵高。 示例: ```java @Autowired private TransactionTemplate transactionTemplate; public void transferMoney() { transactionTemplate.execute(status -> { accountDao.debit("A", 100); accountDao.credit("B", 100); return null; }); } ``` 编程事务的 return 值一般不重要,用不到。 ### Spring 事务传播行为 事务传播行为定义了**当一个事务方法调用另一个事务方法时,事务如何在这些方法间传播,**传播行为是Spring独有的概念,并非事务通用概念。 **示例:** 以下面代码为例,createUserAndOrder() 方法本身有事务,方法内调用了另一个有事务的 OrderService() 方法,这就涉及到事务传播的知识范畴,事务传播有七种行为。 ```java @Transactional(propagation = Propagation.REQUIRED) public void createUserAndOrder() { // 1. 插入用户 insertUser(); // 2. 调用 OrderService orderService.createOrder(); } @Transactional(propagation = Propagation.REQUIRED) public void createOrder() { System.out.println("订单已创建"); // 这里如果抛异常,则事务回滚 // throw new RuntimeException("订单创建失败"); } ``` **propagation** 是事务传播行为的属性,可以指定以下七个值,代表了七种行为。 主要要站在内层事务的角度(createOrder方法)思考这七种行为,我将事务传播分为了四个类别:内层事务加入外层事务(当前事务),挂起当前事务,禁止当前事务,嵌套事务。 | 分类 | 传播行为 | 存在事务时行为 | 无事务时行为 | 核心特点与典型场景 | | ---------------- | ------------------- | ---------------------------------- | ---------------- | ------------------------------------------------------------ | | **加入当前事务** | `**REQUIRED**` | 加入当前事务 | 创建新事务 | **默认选择**。适用于绝大多数业务场景,保证操作在同一事务中。 | | | `**SUPPORTS**` | 加入当前事务 | 以非事务方式执行 | **可选支持**。适用于查询方法,可根据调用方决定是否启用事务。 | | | `**MANDATORY**` | 加入当前事务 | 抛出异常 | **强制要求**。确保方法必须在事务中运行,否则报错。用于内部核心方法,防止意外脱离事务。 | | **挂起当前事务** | `**REQUIRES_NEW**` | **挂起当前事务**,创建新事务 | 创建新事务 | **独立事务**。新事务与外层事务完全独立,必须单独提交/回滚。用于日志记录、发送通知等需与主事务隔离的操作。 | | | `**NOT_SUPPORTED**` | **挂起当前事务**,以非事务方式执行 | 以非事务方式执行 | **强制非事务**。明确不需要事务的操作,如复杂查询,避免事务开销。 | | **禁止当前事务** | `**NEVER**` | 抛出异常 | 以非事务方式执行 | **禁止事务**。绝对不允许在事务中运行,否则报错。用于纯计算或外部调用等场景。 | | **嵌套事务** | `**NESTED**` | 在**嵌套事务**中执行 | 创建新事务 | **部分回滚**。基于保存点机制,内层事务失败可单独回滚,不影响外层事务。适用于大操作中需独立回滚的部分步骤(如批量操作中的单条失败)。 | 面试官问到事务传播行为有哪些,你能说出四个种类来,也表现出你是理解的,每一个详细讲出来确实恶心。实际开发中,有意识的区分四个种类,也就够了。 ### Spring 事务隔离级别 事务隔离级别定义了**并发事务之间的隔离程度**,以防止数据不一致性问题(如脏读、不可重复读、幻读)Spring 支持以下 5 种隔离级别,与标准 JDBC 隔离级别对应: | 隔离级别 | 说明 | 可能引发的问题 | 数据库默认示例 | | ------------------------------------------- | ---------------------------------------------- | -------------------------- | --------------------------------- | | **ISOLATION_DEFAULT** **默认隔离级别** | 使用**底层数据库的默认隔离级别** 。 | 取决于数据库 | MySQL: `**REPEATABLE_READ**` | | **ISOLATION_READ_UNCOMMITTED** **读未提交** | 允许读取**未提交**的数据变更 。 | 脏读、不可重复读、幻读 | 较少使用 | | **ISOLATION_READ_COMMITTED** **读已提交** | 只允许读取**已提交**的数据变更 。 | 不可重复读、幻读 | Oracle、SQL Server 默认 | | **ISOLATION_REPEATABLE_READ** **可重复读** | 确保同一事务中多次读取同一数据的结果**一致**。 | 幻读 | MySQL 默认,但MySQL不会有幻读问题 | | **ISOLATION_SERIALIZABLE** **串行化** | 最高的隔离级别,**事务串行执行** 。 | 无并发问题,但**性能极低** | 金融等高一致性要求场景 | 当你在Spring应用层指定了事务的隔离级别,就相当于给数据库指定了隔离级别。 ### Spring 事务的原理 fency :对于程序技术而言,原理就是看程序的调用链路,看源码是怎么写的,分析背后的设计思想,不过这里看看调用链路,简单学一点原理就行了。 Spring 事务的原理核心是 **AOP(面向切面编程)** 和 **事务管理器** 的协同工作。它通过代理模式在方法执行前后插入事务控制逻辑,从而实现声明式事务管理。下面我用一张图帮你直观理解其整体流程,然后再分步解析。 **客户端**:理解成前端发送请求,访问到后端的一个事务方法,开始调用事务方法,你看的是调用了一个带有 @Transitionnal 注解的方法,实际上呢? **Spring AOP 代理**:Spring 容器在启动时,会检查所有 Bean,如果发现某个类(或其方法)上带有 `**@Transactional**` 注解,Spring 会通过 **AOP 框架** 为其创建一个**代理对象**(基于 JDK 动态代理或 CGLIB),这个代理对象包装了原始的目标对象(方法)。 所以实际上程序调用了一个 AOP 代理。 **TransactionManager** : AOP 代理会调用Spring事务管理器(理解成一个类就行),帮你在背后对接数据库,帮你开启事务,告知 Spring 最新的数据库事务状态。 所以 Spring 事务的能力是取决于数据库,如果数据库使用 MyISAM 存储引擎,那么 Spring 将丧失事务能力。  ## Spring 事务实战 技术栈选用:**Spring Boot 3.x** + MyBatis plus + MySQL 场景是经典转账场景,周杰伦给昆凌转账 ### 初始化项目 项目依赖: ```xml <dependencies> <!-- Spring Boot Starter --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <!-- MySQL Driver --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!-- MyBatis-Plus Starter --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-spring-boot3-starter</artifactId> <version>3.5.14</version> </dependency> <!-- Lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!-- Spring Boot Test --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> ``` 库表设计: ```plsql CREATE DATABASE IF NOT EXISTS spring_tx_demo; USE spring_tx_demo; CREATE TABLE users ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL, balance DECIMAL(10,2) NOT NULL ); INSERT INTO users(name, balance) VALUES ('周杰伦', 1000.00), ('昆凌', 1000.00); ``` 配置文件: ```yaml spring: datasource: url: jdbc:mysql://localhost:3306/spring_tx_demo?useSSL=false&serverTimezone=UTC username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # SQL 输出 global-config: db-config: id-type: auto table-underline: true ``` MyBatis X 插件生成代码:    实体类: ```java @TableName(value ="users") @Data public class Users { /** * */ @TableId(type = IdType.AUTO) private Integer id; /** * */ private String name; /** * */ private BigDecimal balance; } ``` ### 事务基本功能示例 周杰伦给昆凌转账 , 我们模拟两次,一次是转账成功,一次是转账失败回滚,来验证Spring事务是正常工作的。 注意代码不用看的太仔细,一会跟我分析日志,日志是精髓,代码精髓就是一个 @Transactional ,代码逻辑无所谓。 Service 代码: ```java @Service public class UsersService { @Autowired private UsersMapper usersMapper; /** * 场景一:成功的转账 * @param fromName 转出人 * @param toName 转入人 * @param amount 金额 */ @Transactional // <-- 关键!开启事务 public void transferMoneySuccessfully(String fromName, String toName, BigDecimal amount) { System.out.println("--------- 开始成功转账 ---------"); // 1. 查询转出用户 Users fromUser = usersMapper.selectOne(new QueryWrapper<Users>().eq("name", fromName)); // 使用 Users // 2. 查询转入用户 Users toUser = usersMapper.selectOne(new QueryWrapper<Users>().eq("name", toName)); // 使用 Users // 3. 转出用户扣款 fromUser.setBalance(fromUser.getBalance().subtract(amount)); usersMapper.updateById(fromUser); System.out.println(fromName + " 扣款 " + amount + " 成功,余额: " + fromUser.getBalance()); // 4. 转入用户收款 toUser.setBalance(toUser.getBalance().add(amount)); usersMapper.updateById(toUser); System.out.println(toName + " 收款 " + amount + " 成功,余额: " + toUser.getBalance()); System.out.println("--------- 成功转账完成 ---------"); } /** * 场景二:失败的转账(模拟异常,触发回滚) * @param fromName 转出人 * @param toName 转入人 * @param amount 金额 */ @Transactional // <-- 关键!开启事务 public void transferMoneyWithException(String fromName, String toName, BigDecimal amount) { System.out.println("--------- 开始模拟异常转账 ---------"); // 1. 查询转出用户 Users fromUser = usersMapper.selectOne(new QueryWrapper<Users>().eq("name", fromName)); // 使用 Users // 2. 查询转入用户 Users toUser = usersMapper.selectOne(new QueryWrapper<Users>().eq("name", toName)); // 使用 Users // 3. 转出用户扣款 fromUser.setBalance(fromUser.getBalance().subtract(amount)); usersMapper.updateById(fromUser); System.out.println(fromName + " 扣款 " + amount + " 成功,余额: " + fromUser.getBalance()); // 4. 模拟系统在转账过程中突然发生异常! System.out.println("系统发生异常,转账中断..."); throw new RuntimeException("模拟转账过程中发生异常!"); // 下面的代码永远不会被执行 // toUser.setBalance(toUser.getBalance().add(amount)); // usersMapper.updateById(toUser); } } ``` 在启动类中直接调用了,不用 web 的 controller 调用。 ```java @SpringBootApplication @MapperScan("com.feng.springtransactiondemo.mapper") public class SpringTransactionDemoApplication implements CommandLineRunner { @Autowired private UsersService usersService; // 注入 UsersService @Autowired private UsersMapper usersMapper; // 注入 UsersMapper,用于查询最终结果 public static void main(String[] args) { SpringApplication.run(SpringTransactionDemoApplication.class, args); } @Override public void run(String... args) throws Exception { System.out.println("========================================"); System.out.println("Spring 事务实战演示开始"); System.out.println("========================================"); // --- 场景一:演示成功的事务 --- System.out.println("\n--- 场景一:Alice 给 Bob 转账 200 元 ---"); usersService.transferMoneySuccessfully("Alice", "Bob", new BigDecimal("200")); printAllUsersBalance(); // --- 场景二:演示失败的事务(回滚)--- System.out.println("\n--- 场景二:Alice 给 Bob 转账 300 元,但中途发生异常 ---"); try { usersService.transferMoneyWithException("Alice", "Bob", new BigDecimal("300")); } catch (Exception e) { System.err.println("捕获到异常: " + e.getMessage()); } printAllUsersBalance(); System.out.println("\n========================================"); System.out.println("Spring 事务实战演示结束"); System.out.println("========================================"); } private void printAllUsersBalance() { System.out.println("--- 查询当前所有用户余额 ---"); List<Users> users = usersMapper.selectList(null); // 使用 Users users.forEach(user -> System.out.println(user.getName() + " 的余额是: " + user.getBalance())); System.out.println("---------------------------"); } } ``` 最终得到日志就很有用了: ```bash ======================================== Spring 事务实战演示开始 ======================================== --- 场景一:周杰伦 给 昆凌 转账 200 元 --- 2025-10-28T18:50:28.216+08:00 INFO 29736 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting... 2025-10-28T18:50:28.344+08:00 INFO 29736 --- [ main] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Added connection com.mysql.cj.jdbc.ConnectionImpl@294f9d50 2025-10-28T18:50:28.345+08:00 INFO 29736 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed. --------- 开始成功转账 --------- Creating a new SqlSession Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6baf25d7] JDBC Connection [HikariProxyConnection@718057154 wrapping com.mysql.cj.jdbc.ConnectionImpl@294f9d50] will be managed by Spring ==> Preparing: SELECT id,name,balance FROM users WHERE (name = ?) ==> Parameters: 周杰伦(String) <== Columns: id, name, balance <== Row: 1, 周杰伦, 1000.00 <== Total: 1 Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6baf25d7] Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6baf25d7] from current transaction ==> Preparing: SELECT id,name,balance FROM users WHERE (name = ?) ==> Parameters: 昆凌(String) <== Columns: id, name, balance <== Row: 2, 昆凌, 1000.00 <== Total: 1 Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6baf25d7] Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6baf25d7] from current transaction ==> Preparing: UPDATE users SET name=?, balance=? WHERE id=? ==> Parameters: 周杰伦(String), 800.00(BigDecimal), 1(Integer) <== Updates: 1 Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6baf25d7] 周杰伦 扣款 200 成功,余额: 800.00 Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6baf25d7] from current transaction ==> Preparing: UPDATE users SET name=?, balance=? WHERE id=? ==> Parameters: 昆凌(String), 1200.00(BigDecimal), 2(Integer) <== Updates: 1 Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6baf25d7] 昆凌 收款 200 成功,余额: 1200.00 --------- 成功转账完成 --------- Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6baf25d7] Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6baf25d7] Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6baf25d7] --- 查询当前所有用户余额 --- Creating a new SqlSession SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6f25bf88] was not registered for synchronization because synchronization is not active JDBC Connection [HikariProxyConnection@2069678360 wrapping com.mysql.cj.jdbc.ConnectionImpl@294f9d50] will not be managed by Spring ==> Preparing: SELECT id,name,balance FROM users ==> Parameters: <== Columns: id, name, balance <== Row: 1, 周杰伦, 800.00 <== Row: 2, 昆凌, 1200.00 <== Total: 2 Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6f25bf88] 周杰伦 的余额是: 800.00 昆凌 的余额是: 1200.00 --------------------------- --- 场景二:周杰伦 给 昆凌 转账 300 元,但中途发生异常 --- --------- 开始模拟异常转账 --------- Creating a new SqlSession Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2c719bd4] JDBC Connection [HikariProxyConnection@933293116 wrapping com.mysql.cj.jdbc.ConnectionImpl@294f9d50] will be managed by Spring ==> Preparing: SELECT id,name,balance FROM users WHERE (name = ?) ==> Parameters: 周杰伦(String) <== Columns: id, name, balance <== Row: 1, 周杰伦, 800.00 <== Total: 1 Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2c719bd4] Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2c719bd4] from current transaction ==> Preparing: SELECT id,name,balance FROM users WHERE (name = ?) ==> Parameters: 昆凌(String) <== Columns: id, name, balance <== Row: 2, 昆凌, 1200.00 <== Total: 1 Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2c719bd4] Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2c719bd4] from current transaction ==> Preparing: UPDATE users SET name=?, balance=? WHERE id=? ==> Parameters: 周杰伦(String), 500.00(BigDecimal), 1(Integer) <== Updates: 1 Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2c719bd4] 周杰伦 扣款 300 成功,余额: 500.00 系统发生异常,转账中断... Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2c719bd4] Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2c719bd4] --- 查询当前所有用户余额 --- Creating a new SqlSession SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@456f7d9e] was not registered for synchronization because synchronization is not active JDBC Connection [HikariProxyConnection@1976788674 wrapping com.mysql.cj.jdbc.ConnectionImpl@294f9d50] will not be managed by Spring ==> Preparing: SELECT id,name,balance FROM users ==> Parameters: <== Columns: id, name, balance <== Row: 1, 周杰伦, 800.00 <== Row: 2, 昆凌, 1200.00 <== Total: 2 Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@456f7d9e] 周杰伦 的余额是: 800.00 昆凌 的余额是: 1200.00 --------------------------- ======================================== Spring 事务实战演示结束 ======================================== ``` 日志看完了,发现我们打上的注解确实有用的,在场景二中,我们执行了两个更新金额的操作,根据所学知识,这需要开启事务,所以我们用Spring事务快速开启 MySQL 的事务,而不用编写 sql 语句,一个注解搞定。最终也确实回滚了,转账失败。 ### 脏读示例 这里给出一个脏读示例,别的也是类似的。 在原本的service上加以下两个方法 上面的方法是默认的隔离级别(可重复读),下面的方法是`isolation = Isolation.READ_UNCOMMITTED`读未提交 ```java /** * 模拟周杰伦:增加余额,但最后会回滚(事务未成功提交) * @param name 用户名 * @param amount 增加的金额 */ @Transactional // 使用默认的隔离级别即可 public void addBalanceWithRollback(String name, BigDecimal amount) { System.out.println("\n[" + Thread.currentThread().getName() + "] " + name + " 开始事务,准备增加余额 " + amount); Users jay = usersMapper.selectOne(new QueryWrapper<Users>().eq("name", name)); jay.setBalance(jay.getBalance().add(amount)); usersMapper.updateById(jay); System.out.println("[" + Thread.currentThread().getName() + "] " + name + " 余额已更新为: " + jay.getBalance() + ",但事务尚未提交!"); try { // 模拟耗时操作,让其他事务有机会读取 Thread.sleep(3000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } System.out.println("[" + Thread.currentThread().getName() + "] " + name + " 的操作发生异常,事务即将回滚!"); throw new RuntimeException("模拟系统异常,导致回滚"); } /** * 模拟昆凌:使用 READ_UNCOMMITTED 隔离级别进行查询,可能发生脏读 * @param name 要查询的用户名 */ @Transactional(isolation = Isolation.READ_UNCOMMITTED) // 关键:设置隔离级别为读未提交 public void readWithDirtyRead(String name) { System.out.println("\n[" + Thread.currentThread().getName() + "] 昆凌开始查询 " + name + " 的余额..."); try { // 稍微等待一下,确保周杰伦已经更新了数据但还没回滚 Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } Users jay = usersMapper.selectOne(new QueryWrapper<Users>().eq("name", name)); System.out.println("[" + Thread.currentThread().getName() + "] 昆凌第一次读取到 " + name + " 的余额是: " + jay.getBalance() + " (这可能是脏数据!)"); try { // 等待周杰伦的事务回滚 Thread.sleep(4000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } // 在同一个事务中再次读取 Users jayAgain = usersMapper.selectOne(new QueryWrapper<Users>().eq("name", name)); System.out.println("[" + Thread.currentThread().getName() + "] 昆凌第二次读取到 " + name + " 的余额是: " + jayAgain.getBalance() + " (周杰伦的事务已回滚)"); } ``` 启动类要注释刚刚的run方法相关代码,用以下的代码: ```java @Override public void run(String... args) throws Exception { System.out.println("========================================"); System.out.println("Spring 事务并发问题演示:脏读"); System.out.println("========================================"); // 为了演示效果,先重置周杰伦的余额 resetJayBalance(); // 创建一个包含2个线程的线程池 ExecutorService executor = Executors.newFixedThreadPool(2); System.out.println("\n--- 场景:昆凌在周杰伦未提交的事务中读取了脏数据 ---"); // 线程1:模拟周杰伦的操作(更新并回滚) executor.submit(() -> { try { usersService.addBalanceWithRollback("周杰伦", new BigDecimal("5000")); } catch (Exception e) { // 捕获异常,防止线程池报错 System.err.println("[" + Thread.currentThread().getName() + "] 捕获到异常: " + e.getMessage()); } }); // 线程2:模拟昆凌的查询(脏读) executor.submit(() -> { usersService.readWithDirtyRead("周杰伦"); }); // 关闭线程池 executor.shutdown(); } private void resetJayBalance() { Users jay = usersMapper.selectOne(new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<Users>().eq("name", "周杰伦")); if (jay != null) { jay.setBalance(new BigDecimal("1000.00")); usersMapper.updateById(jay); System.out.println("周杰伦的余额已重置为: " + jay.getBalance()); } } ``` 最终得到日志是这样的,这一次简化给大家看: ```bash ======================================== Spring 事务并发问题演示:脏读 ======================================== 周杰伦的余额已重置为: 1000.00 --- 场景:昆凌在周杰伦未提交的事务中读取了脏数据 --- [pool-1-thread-1] 周杰伦 开始事务,准备增加余额 5000 [pool-1-thread-1] 周杰伦 余额已更新为: 6000.00,但事务尚未提交! [pool-1-thread-2] 昆凌开始查询 周杰伦的余额... [pool-1-thread-2] 昆凌第一次读取到 周杰伦 的余额是: 6000.00 (这可能是脏数据!) [pool-1-thread-1] 周杰伦 的操作发生异常,事务即将回滚! [pool-1-thread-1] 捕获到异常: 模拟系统异常,导致回滚 [pool-1-thread-2] 昆凌第二次读取到 周杰伦 的余额是: 1000.00 (周杰伦的事务已回滚) ``` 系统给周杰伦转 5000 块钱,周杰伦的事务还没提交,昆凌就开始查账 ,查到了6000 , 然后系统发生异常,正常回滚,周杰伦的金额回到了 1000, 昆凌再查一次账变成了 1000! 昆凌在一次事务中查询同样的数据,结果却不一致,这就是脏读,如果昆凌基于读取到的 6000 余额做了某些业务决策(比如批准了一笔大额贷款),那么当周杰伦的事务回滚后,这个决策就建立在了一个不存在的“假数据”之上,可能导致严重的业务问题。 ### 事务传播示例用法 我们演示两个传播行为:【**默认事务传播行为】** 和 【**挂起当前事务并开启新事务】**(REQUIRES_NEW) 场景周杰伦下单,下单包含多个步骤,每个步骤单独开启事务,便能演示一个事务调用另一个事务。 1. 创建订单 2. 扣减库存 3. 扣减用户余额 我们需要增加产品表和订单表: ```sql -- 商品表 CREATE TABLE product ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) NOT NULL, stock INT NOT NULL ); -- 订单表 CREATE TABLE orders ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, product_id INT NOT NULL, amount DECIMAL(10, 2) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 插入初始数据 INSERT INTO product(name, stock) VALUES ('iPhone 15', 10); -- 确保“周杰伦”的账户余额足够 UPDATE users SET balance = 8000.00 WHERE name = '周杰伦'; ``` 实体类: ```java @Data @TableName("orders") public class Order { @TableId(type = IdType.AUTO) private Long id; private Long userId; private Long productId; private BigDecimal amount; private LocalDateTime createTime; } @Data @TableName("product") @AllArgsConstructor public class Product { @TableId(type = IdType.AUTO) private Long id; private String name; private Integer stock; } ``` mapper 类: ```java @Mapper public interface OrderMapper extends BaseMapper<Order> {} @Mapper public interface ProductMapper extends BaseMapper<Product> {} ``` 对应的mapper.xml ```xml <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.feng.springtransactiondemo.mapper.OrderMapper"> <resultMap id="BaseResultMap" type="com.feng.springtransactiondemo.domain.Order"> <id property="id" column="id" jdbcType="INTEGER"/> <result property="userId" column="user_id" jdbcType="INTEGER"/> <result property="productId" column="product_id" jdbcType="INTEGER"/> <result property="amount" column="amount" jdbcType="DECIMAL"/> <result property="createTime" column="create_time" jdbcType="TIMESTAMP"/> </resultMap> <sql id="Base_Column_List"> id,user_id,product_id, amount,create_time </sql> </mapper> <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.feng.springtransactiondemo.mapper.ProductMapper"> <resultMap id="BaseResultMap" type="com.feng.springtransactiondemo.domain.Product"> <id property="id" column="id" jdbcType="INTEGER"/> <result property="name" column="name" jdbcType="VARCHAR"/> <result property="stock" column="stock" jdbcType="INTEGER"/> </resultMap> <sql id="Base_Column_List"> id,name,stock </sql> </mapper> ``` Service 类 ```java @Service public class OrderService { @Autowired private OrderMapper orderMapper; @Autowired private ProductMapper productMapper; @Autowired private UsersMapper usersMapper; // 用于记录日志的Mapper,我们直接用OrderMapper来模拟一个日志表 @Autowired private OrderMapper logMapper; // 假设这是一个独立的日志Mapper /** * 场景一:REQUIRED (默认值) * 外部方法事务,内部方法加入外部事务。任何一个失败,整体回滚。 */ @Transactional(propagation = Propagation.REQUIRED) public void placeOrder_REQUIRED(Long userId, Long productId, BigDecimal amount) { System.out.println("\n--- 场景一:REQUIRED 传播 ---"); System.out.println("[外部事务] 开始下单..."); // 1. 创建订单 createOrder(userId, productId, amount); // 2. 扣减库存 reduceStock(productId); // 3. 扣减余额 (这里会模拟失败) deductBalance(userId, amount); System.out.println("[外部事务] 下单成功!"); } /** * 场景二:REQUIRES_NEW * 内部方法开启一个新事务 REQUIRES_NEW,独立于外部事务。即使外部事务回滚,内部事务也提交。 */ @Transactional(propagation = Propagation.REQUIRED) // 外部事务设置为 REQUIRED,表示如果当前存在事务,就加入该事务 public void placeOrder_REQUIRES_NEW(Long userId, Long productId, BigDecimal amount) { System.out.println("\n--- 场景二:REQUIRES_NEW 传播 ---"); System.out.println("[外部事务] 开始下单..."); try { // 1. 创建订单,调用 createOrder 方法,该方法使用 REQUIRES_NEW 传播行为,会开启一个新事务 createOrder(userId, productId, amount); // 2. 扣减库存,调用 reduceStock 方法,该方法使用 REQUIRES_NEW 传播行为,会开启一个新事务 reduceStock(productId); // 3. 扣减余额 (这里会模拟失败) deductBalance(userId, amount); System.out.println("[外部事务] 下单成功!"); } catch (Exception e) { System.err.println("[外部事务] 下单失败: " + e.getMessage()); // 无论成功失败,都要记录一条日志 recordLog(userId, productId, "下单失败"); } } // ============== 内部私有方法 ============== private void createOrder(Long userId, Long productId, BigDecimal amount) { Order order = new Order(); order.setUserId(userId); order.setProductId(productId); order.setAmount(amount); orderMapper.insert(order); System.out.println("[内部方法] 创建订单成功,订单ID: " + order.getId()); } @Transactional(propagation = Propagation.REQUIRED) // 加入到外部事务 void reduceStock(Long productId) { Product product = productMapper.selectById(productId); if (product.getStock() <= 0) { throw new RuntimeException("库存不足!"); } product.setStock(product.getStock() - 1); productMapper.updateById(product); System.out.println("[内部方法] 扣减库存成功,剩余库存: " + product.getStock()); } @Transactional(propagation = Propagation.REQUIRED) // 加入到外部事务 void deductBalance(Long userId, BigDecimal amount) { Users user = usersMapper.selectById(userId); if (user.getBalance().compareTo(amount) < 0) { throw new RuntimeException("用户余额不足!"); } user.setBalance(user.getBalance().subtract(amount)); usersMapper.updateById(user); System.out.println("[内部方法] 扣减余额成功,用户余额: " + user.getBalance()); // 模拟一个系统异常,导致事务回滚 if (amount.compareTo(new BigDecimal("5000")) > 0) { throw new RuntimeException("模拟系统异常:大额订单需要人工审核!"); } } /** * 记录日志,使用 REQUIRES_NEW 开启一个独立的事务 */ @Transactional(propagation = Propagation.REQUIRES_NEW) public void recordLog(Long userId, Long productId, String status) { System.out.println("[独立事务] 开始记录日志..."); Order logEntry = new Order(); // 借用Order表做日志 logEntry.setUserId(userId); logEntry.setProductId(productId); // 在订单表中金额 -1 表示日志,执行一次你会在订单表中看到两条记录,一条是订单,另一条是日志 logEntry.setAmount(new BigDecimal("-1")); // 这里可以设置一个状态字段,为了简化,我们就不加了 logMapper.insert(logEntry); System.out.println("[独立事务] 日志记录成功!这条日志不会因为外部事务回滚而消失。"); } } ``` 启动类: ```java package com.feng.springtransactiondemo.service; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.feng.springtransactiondemo.domain.Order; import com.feng.springtransactiondemo.domain.Product; import com.feng.springtransactiondemo.domain.Users; import com.feng.springtransactiondemo.mapper.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; @Service public class OrderService { @Autowired private OrderMapper orderMapper; @Autowired private ProductMapper productMapper; @Autowired private UsersMapper usersMapper; // 用于记录日志的Mapper,我们直接用OrderMapper来模拟一个日志表 @Autowired private OrderMapper logMapper; // 假设这是一个独立的日志Mapper // 使用 @Lazy 是一个好习惯,可以防止在某些Bean初始化顺序下可能出现的循环依赖问题 @Autowired @Lazy private OrderService self; // 注入的是被Spring AOP代理过的对象 /** * 场景一:REQUIRED (默认值) * 外部方法事务,内部方法加入外部事务。任何一个失败,整体回滚。 */ @Transactional(propagation = Propagation.REQUIRED) public void placeOrder_REQUIRED(Long userId, Long productId, BigDecimal amount) { System.out.println("\n--- 场景一:REQUIRED 传播 ---"); System.out.println("[外部事务] 开始下单..."); // 1. 创建订单 createOrder(userId, productId, amount); // 2. 扣减库存 reduceStock(productId); // 3. 扣减余额 (这里会模拟失败) deductBalance(userId, amount); System.out.println("[外部事务] 下单成功!"); } /** * 场景二:REQUIRES_NEW * 内部方法开启一个新事务 REQUIRES_NEW,独立于外部事务。即使外部事务回滚,内部事务也提交。 */ @Transactional(propagation = Propagation.REQUIRED) // 外部事务设置为 REQUIRED,表示如果当前存在事务,就加入该事务 public void placeOrder_REQUIRES_NEW(Long userId, Long productId, BigDecimal amount) { System.out.println("\n--- 场景二:REQUIRES_NEW 传播 ---"); System.out.println("[外部事务] 开始下单..."); try { // 1. 创建订单,调用 createOrder 方法,该方法使用 REQUIRES_NEW 传播行为,会开启一个新事务 createOrder(userId, productId, amount); // 2. 扣减库存,调用 reduceStock 方法,该方法使用 REQUIRES_NEW 传播行为,会开启一个新事务 reduceStock(productId); // 3. 扣减余额 (这里会模拟失败) , 这里的异常会开启新的子事务,子事务吞掉了一场,外部异常就没有回滚了 deductBalance(userId, amount); System.out.println("[外部事务] 下单成功!uuid"); } catch (Exception e) { System.err.println("[外部事务] 下单失败: " + e.getMessage()); // 这里一定使用 self , 否则在同一个对象内,AOP 代理不会触发,自然不会开启一个新事务。 // 只有走了 AOP 代理,才会让 @Transactional 注解生效,从而开启recordLog的子事务。 self.recordLog(userId, productId, "下单失败"); // 抛出异常,重新触发外部事务回滚 throw e; } } // ============== 内部私有方法 ============== private void createOrder(Long userId, Long productId, BigDecimal amount) { Order order = new Order(); order.setUserId(userId); order.setProductId(productId); order.setAmount(amount); orderMapper.insert(order); System.out.println("[内部方法] 创建订单成功,订单ID: " + order.getId()); } @Transactional(propagation = Propagation.REQUIRED) // 加入到外部事务 void reduceStock(Long productId) { Product product = productMapper.selectById(productId); if (product.getStock() <= 0) { throw new RuntimeException("库存不足!"); } product.setStock(product.getStock() - 1); productMapper.updateById(product); System.out.println("[内部方法] 扣减库存成功,剩余库存: " + product.getStock()); } @Transactional(propagation = Propagation.REQUIRED) // 加入到外部事务 void deductBalance(Long userId, BigDecimal amount) { Users user = usersMapper.selectById(userId); if (user.getBalance().compareTo(amount) < 0) { throw new RuntimeException("用户余额不足!"); } user.setBalance(user.getBalance().subtract(amount)); usersMapper.updateById(user); System.out.println("[内部方法] 扣减余额成功,用户余额: " + user.getBalance()); // 模拟一个系统异常,导致事务回滚 if (true) { throw new RuntimeException("模拟系统异常:大额订单需要人工审核!"); } } /** * 记录日志,使用 REQUIRES_NEW 开启一个独立的事务 */ @Transactional(propagation = Propagation.REQUIRES_NEW) public void recordLog(Long userId, Long productId, String status) { System.out.println("[独立事务] 开始记录日志..."); Order logEntry = new Order(); // 借用Order表做日志 logEntry.setUserId(userId); logEntry.setProductId(productId); // 在订单表中金额 -1 表示日志,执行一次你会在订单表中看到两条记录,一条是订单,另一条是日志 logEntry.setAmount(new BigDecimal("-1")); // 这里可以设置一个状态字段,为了简化,我们就不加了 logMapper.insert(logEntry); System.out.println("[独立事务] 日志记录成功!这条日志不会因为外部事务回滚而消失。"); } } ``` 最终日志: REQUIRED 传播是默认传播行为,子事务加入当前事务。REQUIRES_NEW 是子事务不加入当前事务,开启新事务。 ```java ======================================== Spring 事务传播行为实战演示 ======================================== ========== 场景一:REQUIRED 传播 ========== --- 重置数据 --- 用户余额、商品库存、订单记录已重置。 --- 场景一:REQUIRED 传播 --- [外部事务] 开始下单... [内部方法] 创建订单成功,订单ID: 1 [内部方法] 扣减库存成功,剩余库存: 9 [内部方法] 扣减余额成功,用户余额: 2000.00 主程序捕获到异常: 模拟系统异常:大额订单需要人工审核! --- 查看最终状态 --- 用户余额: 8000.00 <-- 余额回滚了 商品库存: 10 <-- 库存回滚了 订单记录数量: 0 <-- 订单也回滚了 ------------------- ========== 场景二:REQUIRES_NEW 传播 ========== --- 重置数据 --- 用户余额、商品库存、订单记录已重置。 --- 场景二:REQUIRES_NEW 传播 --- [外部事务] 开始下单... [内部方法] 创建订单成功,订单ID: 2 [内部方法] 扣减库存成功,剩余库存: 9 [内部方法] 扣减余额成功,用户余额: 2000.00 [外部事务] 下单失败: 模拟系统异常:大额订单需要人工审核! [独立事务] 开始记录日志... [独立事务] 日志记录成功!这条日志不会因为外部事务回滚而消失。 主程序捕获到异常: 模拟系统异常:大额订单需要人工审核! --- 查看最终状态 --- 用户余额: 8000.00 <-- 余额回滚了 商品库存: 10 <-- 库存回滚了 订单记录数量: 1 <-- 注意!这里有一条记录,就是日志! 订单记录: [Order{id=3, ...}] ------------------- ``` #### 结果分析 - **场景一 (**`**REQUIRED**`**)**: - - `**placeOrder_REQUIRED**` 开启了一个外部事务。 - `**reduceStock**` 和 `**deductBalance**` 都加入了这个外部事务。 - 当 `**deductBalance**` 抛出异常时,整个外部事务被标记为回滚。 - **结果**:创建订单、扣减库存、扣减余额这三个操作**全部被回滚**。数据库恢复到下单前的状态。这保证了业务的原子性。 - **场景二 (**`**REQUIRES_NEW**`**)**: - - `**placeOrder_REQUIRES_NEW**` 开启了外部事务。 - 当 `**deductBalance**` 抛出异常,外部事务被标记为回滚。 - 在 `**catch**` 块中,调用了 `**recordLog**` 方法。因为它使用了 `**REQUIRES_NEW**`,它会**挂起**当前失败的外部事务,并**开启一个全新的、独立的事务**。 - `**recordLog**` 执行成功,**新事务被提交**。 - `**recordLog**` 执行完毕,被挂起的外部事务恢复,然后执行回滚。 - **结果**:下单相关的操作(订单、库存、余额)全部回滚,但 `**recordLog**` 的操作**被成功提交**。这对于需要记录失败日志的场景非常有用,**即使主业务失败被回滚了,子事务也能保留关键的错误信息。**  #### 有意思的异常 在最后的例子中,场景二发生了异常交给 catch 处理,如果在catch中,你不抛出异常给外部,那么本次事务算成功!发生了异常事务还算做成功?这是为什么呢? (关注最后的 throw e; 如果没有它,主业务会正常提交,不会回滚,结果不符合预期) ```java /** * 场景二:REQUIRES_NEW * 内部方法开启一个新事务 REQUIRES_NEW,独立于外部事务。即使外部事务回滚,内部事务也提交。 */ @Transactional(propagation = Propagation.REQUIRED) // 外部事务设置为 REQUIRED,表示如果当前存在事务,就加入该事务 public void placeOrder_REQUIRES_NEW(Long userId, Long productId, BigDecimal amount) { System.out.println("\n--- 场景二:REQUIRES_NEW 传播 ---"); System.out.println("[外部事务] 开始下单..."); try { // 1. 创建订单,调用 createOrder 方法,该方法使用 REQUIRES_NEW 传播行为,会开启一个新事务 createOrder(userId, productId, amount); // 2. 扣减库存,调用 reduceStock 方法,该方法使用 REQUIRES_NEW 传播行为,会开启一个新事务 reduceStock(productId); // 3. 扣减余额 (这里会模拟失败) , 这里的异常会开启新的子事务,子事务吞掉了一场,外部异常就没有回滚了 deductBalance(userId, amount); System.out.println("[外部事务] 下单成功!uuid"); } catch (Exception e) { System.err.println("[外部事务] 下单失败: " + e.getMessage()); // 这里一定使用 self , 否则在同一个对象内,AOP 代理不会触发,自然不会开启一个新事务。 // 只有走了 AOP 代理,才会让 @Transactional 注解生效,从而开启recordLog的子事务。 self.recordLog(userId, productId, "下单失败"); // 抛出异常,重新触发外部事务回滚 throw e; } } ``` 这里要了解异常的生命周期了,如果异常被 catch 处理,那么异常就销毁了,程序又恢复正常健康的继续执行,只不过try里面的代码块一定没有全部执行完。 **分析结果** 1. **异常产生**:`deductBalance` 方法内抛出一个 `RuntimeException`,这个异常向上传播,被 `placeOrder_REQUIRES_NEW` 方法中的 `catch (Exception e)` 块捕获。 ```java @Transactional(propagation = Propagation.REQUIRED) // 加入到外部事务 void deductBalance(Long userId, BigDecimal amount) { // 其它代码 .. // 模拟一个系统异常,导致事务回滚 if (true) { throw new RuntimeException("模拟系统异常:大额订单需要人工审核!"); } } ``` 1. **异常被“处理”**:一旦异常进入 `catch` 块,从 JVM 和调用栈的角度来看,这个异常就已经被“处理”了。程序不会再因为这个异常而崩溃,而是会继续执行 `catch` 块内部的代码。 2. `**catch**` **块执行**:`System.err.println(...)` 和 `self.recordLog(...)` 被执行。注意,`self.recordLog` 方法本身执行是成功的,它没有抛出任何新的异常。 3. `**catch**` **块结束**:如果没有 `throw e;`,`catch` 块就会正常结束。 事务是AOP实现的,那么此时Spring 的事务管理器(AOP 代理)像一个监控员,它在 `placeOrder_REQUIRES_NEW` 方法的外部观察着: - 它只关心一件事:当 `placeOrder_REQUIRES_NEW` 方法执行完毕时,它是正常结束的,还是因为抛出异常而结束的。 **场景分析:如果没有** `**throw e;**` 1. 代理开启事务 T1。 2. 代理调用目标方法 `placeOrder_REQUIRES_NEW`。 3. 方法内部发生异常,但被 `catch` 块捕获了。 4. `self.recordLog` 在新事务 T2 中成功执行并提交。 5. `catch` 块执行完毕,`placeOrder_REQUIRES_NEW` 方法正常结束。 6. 代理看到方法正常结束,它认为:“太好了,一切顺利,没有发生任何错误!” 7. 于是,代理提交事务 T1。 8. 结果:订单、库存、余额的修改全部被保存,这与我们期望的“失败回滚”背道而驰。 **场景分析:如果有** `**throw e;**` 1. 代理开启事务 T1。 2. 代理调用目标方法 `placeOrder_REQUIRES_NEW`。 3. 方法内部发生异常,被 `catch` 块捕获。 4. `self.recordLog` 在新事务 T2 中成功执行并提交。 5. `catch` 块执行到 `throw e;`,将之前捕获的那个异常重新抛出。 6. `placeOrder_REQUIRES_NEW` 方法因为抛出异常而结束。 7. 代理看到方法因异常而结束,它认为:“糟糕,出问题了,需要执行回滚操作!” 8. 于是,代理回滚事务 T1。 9. 结果:T1 中的所有操作(订单、库存、余额)都被撤销,而 T2 中的日志记录因为已经提交而保留下来。这正是我们想要的结果。
SpringBoot 自动装配
SpringBoot 自动配置的原理是什么? ====================== `Spring`在启动的时候会自动扫描外部`jar`包中的`META-INF\spring.factories`,将文件中的配置类型信息加载到`Spring`容器,并且执行类中定义的操作。对于外部的`jar`包来说,只要遵循`SpringBoot`的标准来,就能将自己的功能配置到`SpringBoot`中。 `SpringBoot 3.0`版本开始,自动配置包的路径就将`META-INF\spring.factories`改成了`META-INF\spring\org.springframework.boot.autoconfigure.AutoConfiguration.imports` SpringBoot 是如何实现自动配置的? ====================== `SpringBoot`自动配置主要是通过`@EnableAutoConfiguratio`注解实现的,这个注解包含了`@Import(AutoConfigurationImportSelector.class)`注解,作用是在启动的时候会自动扫描外部`jar`的`classPath`下的所有`META-INF\spring.factories`(以`SpringBoot 2.0`为例),根据文件中的指定配置类加载相应的`Bean`自动配置。 @SpringBootApplication ---------------------- 我们来看看`@SpringBootApplication`注解的源代码注解 ```java @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) }) public @interface SpringBootApplication { // CODE... } ``` * `@SpringBootConfiguration`:允许上下文额外注入的`bean`或者导入其他配置类 * `@EnableAutoConfiguration`:启动自动配置的机制 * `@ComponentScan`:扫描被`@Component/@Service/@Controller`注解的 bean,注解会默认扫描包下的所有 bean,可以自定义哪些不被扫描,例如`TypeExcludeFilter, AutoConfigurationExcludeFilter`  现在,我们重点关注`@EnableAutoConfiguration`注解 @EnableAutoConfiguration ------------------------ 要了解该注解的工作机制,我们首先要从源码开始 源码: ```java ... @AutoConfigurationPackage @Import(AutoConfigurationImportSelector.class) public @interface EnableAutoConfiguration { // CODE... } ``` * `@AutoConfigurationPackage`:负责注册主配置所在包以及子包到`Spring`容器下 * `@Import(AutoConfigurationImportSelector.class)`:导入自动配置选择器,负责加载和过滤所有自动配置类 ### AutoConfigurationImportSelector -- 自动配置导入选择器 1)`AutoConfigurationImportSelector`的继承体制如下:  2)入口函数--`selectImports()` 功能:获取所有符合条件的类的全限定类名,并将这些类需要的加载到`IOC`容器中  3)核心实现--加载自动配置类 ```java protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) { if (!isEnabled(annotationMetadata)) { return EMPTY_ENTRY; } // 1. 获取注解属性 AnnotationAttributes attributes = getAttributes(annotationMetadata); // 2. 加载候选配置类 List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes); // 3. 去重 configurations = removeDuplicates(configurations); // 4. 获取排除的配置 Set<String> exclusions = getExclusions(annotationMetadata, attributes); // 5. 检查排除的类是否有效 checkExcludedClasses(configurations, exclusions); // 6. 移除排除的配置 configurations.removeAll(exclusions); // 7. 过滤配置(条件注解处理) configurations = getConfigurationClassFilter().filter(configurations); // 8. 触发事件 fireAutoConfigurationImportEvents(configurations, exclusions); return new AutoConfigurationEntry(configurations, exclusions); } ``` 关键点说明: 获取所有自动装配的配置类,读取`META-INF\spring.factories` ```java // 2. 加载候选配置类 List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes); protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) { List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader()); Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you " + "are using a custom packaging, make sure that file is correct."); return configurations; } ``` 留下需要的配置类: 经过`getCandidateConfigurations`读取`META-INF\spring.factories`获取到所有自动装配的配置类之后,由于加载太多的配置类会影响`Spring`容器的性能,因此需要对这些配置类进行排除过滤。 ```java // 4. 获取排除的配置 Set<String> exclusions = getExclusions(annotationMetadata, attributes); // 5. 检查排除的类是否有效 checkExcludedClasses(configurations, exclusions); // 6. 移除排除的配置 configurations.removeAll(exclusions); // 7. 过滤配置(条件注解处理) configurations = getConfigurationClassFilter().filter(configurations); ``` 实操一遍: 1)判断自动配置是否打开  2)获取`@EnableAutoConfiguration`注解中的`exclude`和`excludeName` ```java ... public @interface EnableAutoConfiguration { // 排除特定的自动配置类,确保永远不会被应用 Class<?>[] exclude() default {}; // 排除特定的自动配置类名,确保永远不会被应用 String[] excludeName() default {}; } ``` 效果如下  3)获取所有自动装配的配置类,读取`META-INF\spring.factories`  4)去重`removeDuplicates(configurations)` 通过哈希集合`LinkedHashSet<>()`去掉重复的配置类 5)获取排除的配置类 比如我们在配置文件中写上: ```yaml spring: autoconfigure: exclude: org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration ``` 指`org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration`是要被排出的  说明已经排除成功 6)检查是否排除生效 -- `checkExcludedClasses(configurations, exclusions)` 7)移除要排除的配置类 -- `configurations.removeAll(exclusions)` 8)过滤出需要的配置类 -- 按需加载  9)触发自动导入事件 ```java fireAutoConfigurationImportEvents(configurations, exclusions) ``` 总结 == `SpringBoot`通过`@EnableAutoConfiguration`注解自动装配,使用`SpringFactoriesLoader`加载`META-INF/spring.factories`中的自动装配的配置类。 end...
Spring 核心 - AOP 面向切面编程入门, 醒醒国庆假期第二天了
撰写本文目的只有一个,让你畅快阅读 AOP 知识,并搞定以下几个问题。 + AOP 面向切面编程到底是什么? + AOP 术语:连接点,切入点,通知,切面,织入是什么东西? + @Aspect 注解为啥用的是 aspectj 包,不是 AOP 包?它们啥关系? + Spring 框架是如何实现 AOP ? **代理技术**。 不急解释 AOP 的概念, 我们先来看 AOP 解决了什么问题。 ## 引言 在写项目的时,你是否遇到过随着系统体积增大,有一些新的需求——比如日志、用户鉴权、指标上报、redis 缓存填充/失效、事务等——需要对系统的多个类、方法改动,这种大量**人工改动**代码的行为容易遗漏、逻辑改动大,侵入风险高。可见,在这种场景下,手动改写代码必然是下下策。 很明显本文讲的是 AOP , AOP 必然可以解决此类问题,做到**不侵入**原有代码新增日志、用户鉴权等功能。 + 举一个例子,让场景更形象化。 ```java public class UserServiceImpl implements UserService { /** * find user list. * * @return user list */ @Override public List<User> findUserList() { System.out.println("执行方法: findUserList()"); return Collections.singletonList(new User("fency", 18)); } /** * add user */ @Override public void addUser() { System.out.println("执行方法: addUser()"); // do something } @Override public void deleteUser() { System.out.println("执行方法: deleteUser()"); } } ``` 要想对这三个方法加日志和鉴权,逐个添加是下策。如图:  下面有请主角登场。 ## AOP ### 介绍 AOP 为 Aspect Oriented Programming 的缩写,意为:面向切面编程。 AOP 最早是 AOP 联盟的组织提出的,指定的一套规范,Spring 将 AOP 的思想引入框架之中,通过**预编译方式**和**运行期间动态代理**实现程序的统一维护的一种技术。 AOP 是设计思想,Spring AOP 、AspectJ 才是技术实现, AOP 思想**本质的作用解耦**。我们将记录日志、鉴权功能解耦为切面,进而引出 AOP 理念:将分散在各个业务逻辑代码中相同的代码,通过**横向切割**的方式抽取到一个独立的模块中。用图表示:  意思是写一次日志代码,即可让三个方法实现日志记录功能。 下面用 Spring AOP 实战演示统一加日志。 ### 实战 - 统一添加日志 创建一个 spring boot 项目,引一个 lombok 依赖。 1)引入 aop 依赖包 ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> ``` 2)新建 UserServiceImpl 类, 我们要对它进行统一添加日志。 ```java @Service public class UserServiceImpl implements UserService { /** * find user list. * * @return user list */ @Override public List<User> findUserList() { System.out.println("执行方法: findUserList()"); return Collections.singletonList(new User("fency", 18)); } /** * add user */ @Override public void addUser() { System.out.println("执行方法: addUser()"); // do something } @Override public void deleteUser() { System.out.println("执行方法: deleteUser()"); } } ``` 单元测试执行 ```java @SpringBootTest class UserServiceImplTest { @Resource private UserService userService; @Test void invokeMethod() { userService.addUser(); userService.findUserList(); userService.deleteUser(); } } ``` 效果如图,没有日志。  3)添加 Aspect 切面类,统一为 UserService 的三个方法添加日志。 ```java @Slf4j @Aspect @Component public class LogAspect { /** * 切点:拦截所有 controller 包下的公共方法 */ @Around("execution(* com.codebear.springboothelloword.service..*(..))") public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable { String methodName = joinPoint.getSignature().toShortString(); Object[] args = joinPoint.getArgs(); log.info("Entering {} with args: {}", methodName, args); try { Object result = joinPoint.proceed(); // 执行业务方法 log.info("Exiting {} with result: {}", methodName, result); return result; } catch (Throwable ex) { log.error("Exception in {}: {}", methodName, ex.getMessage(), ex); throw ex; } } } ``` 再次执行,可以看到每个方法前后都有日志,解决了最初的痛点,避免大量手动修改代码。  初学 AOP 可能会觉得不会灵活,所有的方法都有日志了,如果只想两个方法有日志怎么办? 探索一下切点表达式,便有答案。拿示例的切点表达式来说: ```java // 第一个 * 代表匹配任意方法的返回类型 // service..* 代表匹配这个包下的所有类都是切点 // (..) 代表任意方法的参数类型、个数都匹配 @Around("execution(* com.codebear.springboothelloword.service..*(..))") ``` ### AOP 术语 开始念经:**连接点、切入点、通知、切面、引入、目标对象、织入、AOP代理**,第一次看到这一堆还是会恶心一下吧。 其实这些术语不是 Spring AOP 独有的, 而是 AOP 设计思想的组成,所以这些术语也会比较抽象。 先说明,作为开发者,我认为不需要掌握全部的 AOP 思想,理解切面、通知、切入点、连接点就能写出 `LogAspect` 切面编程。 下面介绍一下,并且把术语代入到 Spring AOP 中。 **连接点(JoinPoint):**上述示例中这里有三个方法,任意**单个**方法就叫一个**连接点**。官方一点的解释,表示需要在程序中插入横切关注点的扩展点,**连接点可能是类初始化、方法执行、方法调用、字段调用或处理异常等等,**在AOP中表示为**在哪里干**。 **** **切入点(Pointcut):**上述示例中,service包下的所有子包、类和方法都是切入点,因为切入点表达式编写成这样,切入点说白了就是表达式规则。官方解释:选择一组相关连接点,即可以认为连接点的集合,在AOP中表示为**在哪里干的集合**。 **通知(Advice)**:真正要织入的代码逻辑,比如上述例子,我们要给切入点织入日志记录逻辑,这段日志逻辑便是通知,为什么叫通知?那么抽象,因为有前置通知(before advice)、后置通知(after advice)、环绕通知(around advice),允许你在原有方法执行前、后、环绕等加入日志逻辑。在AOP中表示为**干什么**。 **切面(Aspect)**: 面向切面编程AOP的主角就是这位,以上述例子来讲,打上 @Aspect 注解的 `LogAspect` 类就是切面,目前这个类中只有一个环绕通知 `logAround()` , 当然可以增加通知,所以这一整个类**编程范式**就是切面编程,那么切面就是 `LogAspect`。 **引入(Introduction)**: 允许一个切面**为目标对象(被通知的对象)声明新的接口,并提供一个默认实现**。作用是**让一个原本没有实现某个接口的类,在运行时“假装”实现了这个接口,从而可以调用这个接口中的方法。** **目标对象(Target Object):**需要被织入横切关注点的对象,即该对象是切入点选择的对象,需要被通知的对象,从而也可称为被通知对象;由于Spring AOP 通过代理模式实现,从而这个对象永远是被代理对象。在AOP中表示为**对谁干**。 **织入(Weaving)**:把切面连接到其它的应用程序类型或者对象上,并创建一个被通知的对象。这些可以在编译时(例如使用AspectJ编译器),类加载时和运行时完成。Spring 和其他纯 Java AOP 框架一样,在运行时完成织入,运行时生成动态代理代码的这个过程叫做织入。在 AOP 中表示为**怎么实现的**。 **AOP 代理(AOP)**: AOP 框架使用**代理模式**创建的对象,从而实现在连接点处插入通知(即应用切面),就是通过代理来对目标对象应用切面。在 Spring 中,AOP 代理可以用 JDK 动态代理或 CGLIB 代理实现,而通过拦截器模型应用切面。在AOP中表示为**怎么实现的一种典型方式**; ### AOP 术语难点解释 ```java // 1. 定义一个我们想要“赋予”的能力 public interface IsModified { boolean isModified(); } // 实现类,给一个最简单的实现,调用方法只能回复 false public class DefaultIsModifiedImpl implements IsModified { @Override public boolean isModified() { // 它总是返回一个固定的值,比如 false return false; } } // 2. 定义切面,使用引入 @Aspect public class IsModifiedAspect { // @DeclareParents 就是引入的核心注解 // value: 指定要为哪些目标类赋予新能力 // defaultImpl: 指定新能力的默认实现 @DeclareParents( value = "com.example.domain.*+", // 匹配 domain 包下的所有类 defaultImpl = DefaultIsModifiedImpl.class ) public static IsModified isModified; // 字段类型就是要赋予的能力(接口) } // 3. 在业务代码中使用 @Service public class SomeService { @Autowired private UserRepository userRepository; public void processUser() { User user = userRepository.findById(1L); // 关键:User类本身没有isModified方法 // 但通过引入,我们可以把它当作IsModified接口来使用 IsModified modifiedUser = (IsModified) user; System.out.println("Is user modified? " + modifiedUser.isModified()); } } ``` 可以看到 user 可以强转成 IsModified 接口并使用 IsModified 接口的方法,而 User 类并没有实现 IsModified 接口,这便是引入的作用:**让一个原本没有实现某个接口的类,在运行时“假装”实现了这个接口,从而可以调用这个接口中的方法。** 织入和代理应该有点难分清吧? 织入是生成代理对象的过程,有了代理对象,通知功能得以实现。可以看出 Spring AOP 本质上是通过动态代理实现。 一图胜千言  如果坚持读到这里,我相信你搞明白了 AOP 和 Spring AOP 的概念,一个设计思想,一个是具体实现,另外AspectJ 也是实现 AOP 的一门技术。 细心的你已经发现了上述示例 @Aspect 注解来源于 aspectj 包,并不是 aop 包的注解。  如下图所示,我们引入的 starter - aop 依赖包含了 aspectj 和 aop 两个技术,说明 spring 开发小组直接使用了 aspectj 包作为 Spring AOP 实现的一部分。  那必须要讲一讲 Spring AOP 和 AspectJ 之间的瓜葛。 ### Spring AOP 和 AspectJ 的渊源 **Aspect 是什么呢?** AspectJ 是一个 java 语言实现的 AOP 框架,它能够对java代码进行AOP编译(一般在编译期进行),让java代码具有 AspectJ 的 AOP 功能(当然需要特殊的编译器)。 可以这样说 AspectJ 是目前实现 AOP 框架中最成熟,功能最丰富的语言,更幸运的是,AspectJ 与 java 程序完全兼容,几乎是无缝关联,因此对于有 java 编程基础的工程师,上手和使用都非常容易。 **Spring AOP 和 AspectJ 是什么关系**? 1)AspectJ 是更强的 AOP 框架,是实际意义的 **AOP 标准**; 2)Spring 为何不写类似 AspectJ 的框架? Spring AOP 使用纯 Java 实现, 它不需要专门的编译过程, 它一个**重要的原则就是无侵入性(non-invasiveness)**; Spring 小组完全有能力写类似的框架,只是 Spring AOP 从来没有打算通过提供一种全面的 AOP 解决方案来与 AspectJ 竞争。Spring 的开发小组相信无论是基于代理(proxy-based)的框架如 Spring AOP 或者是成熟的框架如 AspectJ 都是很有价值的,他们之间应该是**互补而不是竞争的关系**。 3) Spring 小组喜欢 @AspectJ 注解风格更胜于 Spring XML 配置; 所以**在 Spring 2.0 使用了和 AspectJ 5 一样的注解,并使用 AspectJ 来做切入点解析和匹配**。**但是,AOP 在运行时仍旧是纯的 Spring AOP,并不依赖于AspectJ 的编译器或者织入器(weaver)**, 但是注解和切点解析用的都是 AspectJ 的技术。 4)Spring 2.5 对 AspectJ 的支持:在一些环境下,增加了对 AspectJ 的装载时编织支持,同时提供了一个新的bean切入点。 **看样子 AspectJ 哪哪都好呀,更强大,更全面,完全按照 AOP 标准来实现,为啥 Spring 小组还要开发 Spring AOP 呢?** 以下Spring官方的回答:(总结来说就是 **Spring AOP更易用,AspectJ更强大**)。 + Spring AOP 比完全使用 AspectJ 更加简单, 因为它不需要引入 AspectJ 的编译器/织入器到你开发和构建过程中。 如果你**仅仅需要在 Spring bean 上通知执行操作,那么 Spring AOP 是合适的选择**。 + 如果你需要通知 domain 对象或其它没有在 Spring 容器中管理的任意对象,那么你需要使用 AspectJ。 + 如果你想通知除了简单的方法执行之外的连接点(如:调用连接点、字段get或set的连接点等等), 也需要使用AspectJ。 他们的关系稍微了解即可,回归 Spring AOP 的学习。 ## Spring AOP 的配置方式 Spring AOP 支持对 **XML 模式**和基于 **@AspectJ 注解**的两种配置方式。 本文例子使用的是 **@AspectJ 注解式**开发切面编程**,**XML 就是写一堆 XML 配置,用的很少,知道可以 XML 配置即可,不做实战演示。 Spring 使用了 @AspectJ 框架为 AOP 的实现提供了一套注解,下面归纳一下。 | 注解名称 | 解释 | | --- | --- | | @Aspect | 用来定义一个切面。 | | @pointcut | 用于定义**切入点表达式**。在使用时还需要定义一个包含名字和任意参数的方法签名来表示切入点名称,这个方法签名就是一个返回值为void,且方法体为空的普通方法。 | | @Before | 用于定义**前置通知**,相当于BeforeAdvice。在使用时,通常需要指定一个value属性值,该属性值用于指定一个切入点表达式(可以是已有的切入点,也可以直接定义切入点表达式)。 | | @AfterReturning | 用于定义**后置通知**,相当于AfterReturningAdvice。在使用时可以指定pointcut / value和returning属性,其中pointcut / value这两个属性的作用一样,都用于指定切入点表达式。 | | @Around | 用于定义**环绕通知**,相当于MethodInterceptor。在使用时需要指定一个value属性,该属性用于指定该通知被植入的切入点。 | | @After-Throwing | 用于定义**异常通知**来处理程序中未处理的异常,相当于ThrowAdvice。在使用时可指定pointcut / value和throwing属性。其中pointcut/value用于指定切入点表达式,而throwing属性值用于指定一个形参名来表示Advice方法中可定义与此同名的形参,该形参可用于访问目标方法抛出的异常。 | | @After | 用于定义**最终final 通知**,不管是否异常,该通知都会执行。使用时需要指定一个value属性,该属性用于指定该通知被植入的切入点。 | | @DeclareParents | 用于定义**引介通知**,相当于IntroductionInterceptor (不要求掌握)。 | ## Spring AOP 的实现方式 Spring AOP 的实现方式是动态织入,动态织入的方式是在运行时动态将要增强的代码织入到目标类中,这样往往是通过动态代理技术完成的;**如 Java JDK的动态代理( Proxy,底层通过反射实现)或者 CGLIB 的动态代理(底层通过继承实现)**,Spring AOP 采用的就是基于运行时增强的代理技术。 所以我们看下如下的两个例子 + 基于 JDK 代理例子 + 基于 Cglib 代理例子 > 经典面试题,接口使用 JDK 动态代理,类使用 Cglib 动态代理。Spring Boot 2.x 开始默认优先使用 Cglib 动态代理技术,无论你的类是否实现了接口,Spring Boot 默认都会尝试使用 CGLIB 来创建代理对象。 > 参考文章:[https://www.cnblogs.com/lyh233/p/16008251.html#_label1_1](https://www.cnblogs.com/lyh233/p/16008251.html#_label1_1) spring boot 2.0 前默认优先使用 JDK 代理,2.0 后默认优先使用 Cglib 代理,Spring AOP 的底层实现方式是“同时支持 JDK 动态代理和 CGLIB,并根据配置和目标对象的情况,智能选择其中一种来工作”,不改配置默认就是 Cglib 。 再来一道经典面试题。 ## JDK 动态代理和 Cglib 代理动态代理的区别? 一张表格看懂 | 特性 | JDK 动态代理 | CGLIB 动态代理 | | --- | --- | --- | | **核心原理** | **基于接口** | **基于继承** | | **代理对象类型** | 生成目标接口的**新类** (`**com.sun.proxy.$Proxy**`) | 生成目标类的**子类** (`**...$$EnhancerBySpringCGLIB$$...**`) | | **对目标类的要求** | **必须实现至少一个接口** | **可以是任何类**(但不能是 `**final**` 类) | | **对方法的要求** | 只能代理接口中的方法 | 可以代理类中的 `**public**`/`**protected**`方法,**不能代理 **`**final**`** 或 **`**static**`** 方法** | | **依赖** | JDK 原生支持,无需外部库 | 需要引入 `**cglib**`(或 Spring 内置的 `**spring-core**`) | 下面我们抛开 Spring,用最原生的代码来分别实现这两种代理,你会立刻明白它们的工作方式。 先从 Cglib 开始。 ### Cglib 动态代理技术原理 **CGLIB 动态代理**:它在运行时为你创建一个**目标类的子类**。这个子类会重写父类(你的原始类)的所有非 `**final**` 方法,并在重写的方法中加入额外的逻辑(切面代码)。**它和你的原始类是父子关系。** **** **场景预设** 我们想在调用 `OrderService` 的 `createOrder` 方法前后,打印日志。 创建一个包 cglib ,一顿复制。 1) 添加 CGLIB 依赖,Spring boot 项目不用导入依赖。 ```xml <!-- Maven --> <dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>3.3.0</version> </dependency> ``` 2)创建一个目标类(无需接口) ```java public class OrderService { public void createOrder(String productName) { System.out.println("【核心业务】正在创建订单: " + productName); } } ``` 3)**创建 **`**MethodInterceptor**`**(核心逻辑拦截器)**,下一步发挥作用。 ```java public class LogMethodInterceptor implements MethodInterceptor { @Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { // 1. 前置增强(日志) System.out.println("[CGLIB代理] - 准备执行方法: " + method.getName()); // 2. 调用父类(目标对象)的原方法 // 注意:这里用的是 proxy.invokeSuper,而不是 method.invoke Object result = proxy.invokeSuper(obj, args); // 3. 后置增强(日志) System.out.println("[CGLIB代理] - 方法执行完毕"); return result; } } ``` 4)创建代理对象并使用 ```java public class CglibProxyDemo { public static void main(String[] args) { // 1. 创建 Enhancer 对象,类似于 JDK 中的 Proxy 类 Enhancer enhancer = new Enhancer(); // 2. 设置父类(目标类) enhancer.setSuperclass(OrderService.class); // 3. 设置回调(拦截器) enhancer.setCallback(new LogMethodInterceptor()); // 4. 创建代理对象 OrderService proxyInstance = (OrderService) enhancer.create(); // 5. 使用代理对象调用方法 System.out.println("代理对象的类型: " + proxyInstance.getClass().getName()); proxyInstance.createOrder("iPhone 15"); } } ``` 运行结果:  ### JDK 动态代理技术原理 **JDK 动态代理**:它不关心你的类是什么,只关心你实现了哪些接口。它在运行时**通过反射**为你创建一个**全新的类**,这个新类实现了你指定的所有接口,并把所有方法调用都转发到一个 `**InvocationHandler**` 上。**它和你的原始类没有父子关系。** **** **场景预设** 我们想在调用 `UserService` 的 `addUser` 方法前后,打印日志。 1)定义一个接口 ```java public interface UserService { void addUser(String username); } ``` 2)创建接口的实现类(目标对象) ```java public class UserServiceImpl implements UserService { @Override public void addUser(String username) { System.out.println("【核心业务】正在添加用户: " + username); } } ``` 3)**创建 **`**InvocationHandler**`**(核心逻辑处理器)** ```java import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; public class LogInvocationHandler implements InvocationHandler { // 1. 持有目标对象的引用 private Object target; public LogInvocationHandler(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 2. 前置增强(日志) System.out.println("[JDK代理] - 准备执行方法: " + method.getName()); // 3. 调用目标对象的原方法 Object result = method.invoke(target, args); // 4. 后置增强(日志) System.out.println("[JDK代理] - 方法执行完毕"); return result; } } ``` 4)创建代理对象并使用,可以看到 JDK 是反射技术实现代理。 ```java import java.lang.reflect.Proxy; public class JdkProxyDemo { public static void main(String[] args) { // 1. 创建目标对象 UserServiceImpl target = new UserServiceImpl(); // 2. 创建代理对象 UserService proxyInstance = (UserService) Proxy.newProxyInstance( target.getClass().getClassLoader(), // 类加载器 target.getClass().getInterfaces(), // 目标对象实现的接口 new LogInvocationHandler(target) // 事件处理器 ); // 3. 使用代理对象调用方法 System.out.println("代理对象的类型: " + proxyInstance.getClass().getName()); proxyInstance.addUser("张三"); } } ``` 执行结果: 
日志框架介绍 - Spring Boot 内置日志框架 Logback
SpringBoot支持多种日志框架,包括 Logback、Log4j2 和 Java Util Logging(JUL)。默认情况下,如果你使用SpringBoot 的 starters 启动器,它将使用 **Logback** 作为日志框架。 > 本文重点介绍 spring boot 默认的日志框架 **Logback 、SLF4J 依赖、Lombok 依赖** 日志的核心自然是日志框架。 日志框架对比: | **特性 / 框架** | **Logback** | **Log4j2** | **JUL (java.util.logging)** | | ---------------- | -------------------------------------------------------- | ------------------------------ | --------------------------- | | 官方支持 | 官方推荐 | Apache 官方 | Java 内置 | | 性能 | 高 | 高,异步性能更好 | 中等 | | 配置方式 | XML / Groovy | XML / JSON / YAML | properties / API | | Spring Boot 默认 | ✅ 内置 | ❌ 需要排除默认依赖再添加 | ❌ 可以替换,但不方便 | | 特性 | 滚动策略丰富、异步支持、Spring Boot 支持日志级别动态调整 | 更强大、异步性能好、多线程优化 | 简单、易用但功能有限 | | 学习成本 | 低-中 | 中 | 低 | Spring Boot 的内置日志实现是由 SLF4J + Logback 框架实现, Lombok 仅仅是一个辅助。 先学习 SLF4J ## SLF4J 介绍 **SLF4J** 是一个 **日志门面(Facade),** 它提供 **统一的日志 API**,解耦日志框架实现 , 你用 SLF4J 写日志时,不用关心底层具体用的是 Logback、Log4j2、JUL(Java Util Logging)还是其他实现,统一使用门面类: ```java import org.slf4j.Logger; import org.slf4j.LoggerFactory; private static final Logger log = LoggerFactory.getLogger(MyApp.class); ``` 这也是 Spring boot 的starter 默认集成的依赖,如图,我们就不用手动集成这个依赖了。  SLF4J 仅仅是一个门面类,没有具体实现,统一了API的调用。 日志的具体能力由 Logback 框架提供,接下来学习。 ## Logback 日志框架介绍 **Logback** 是一个 **Java 日志框架**,由 Log4j 的作者 Ceki Gülcü 开发,是 Log4j 的“继任者”, 在 Spring Boot 里,Logback 是 **默认日志实现**,通过 `spring-boot-starter-logging` 自动引入。 一般来说,无论是 Logback , **Log4j2** 还是其他框架,主要都提供三个重要的功能,记录日志,日志级别,日志输出。 **1、日志记录** - 提供一套统一的 API 来打印日志(trace/debug/info/warn/error)。 2、**日志级别控制** - 根据配置决定哪些日志需要打印、哪些忽略。 - 典型例子:生产环境只打印 `WARN` 及以上,开发环境允许 `DEBUG`。 3、**日志路由与输出** - 日志输出到哪里:控制台、文件、数据库、网络、异步队列…… - 日志格式如何定义:时间戳、线程名、日志级别、MDC 上下文。 - 这部分就是“日志落地”的关键。 我们来看看 Logback 框架的三大核心模块: 1. **logback-core**:基础模块(其他两个的依赖) 2. **logback-classic**:完整实现 SLF4J 的日志框架(Spring Boot 默认使用它) 3. **logback-access**:与 Servlet 容器集成,记录 HTTP 访问日志 上面所说的日志三个基本功能:记录日志,日志级别,日志输出,这三个功能仅需一个 **logback-classic** 模块和`logback.xml`配置就能实现,不过它 **依赖 logback-core** 来完成底层实现(Appender、Layout、Filter 这些东西在 core 里) ,**logback-access** 只是加一个“web access log”的功能,不是大多数项目必须的。 作为使用者,我们要知道 SLF4J 门面类的API, 即可使用日志功能,知道了底层是 logback 依赖库,便可以更换别的依赖库,体验其他库的功能。 ### 日志级别 级别从高到低, 生产环境通常只打印 **WARN** 及以上 ```java ERROR > WARN > INFO > DEBUG > TRACE ``` TRACE(ALL):最详细的日志,用来跟踪程序的每一步执行,基本只在本地排查 bug 用。 DEBUG:用于调试信息,通常用于开发和调试阶段。 INFO:提供程序运行时的重要信息,用于指示应用程序正常运行。 WARN:表示潜在的问题,不会导致应用程序失败,但可能需要关注。 ERROR:表示错误事件,可能导致应用程序出现问题。 OFF:关闭日志。 ### 实践 logback 框架 创建一个springboot项目,什么依赖都不用引入,如图,默认有 logback 依赖  创建一个包 controller,包下创建 UserController 类,实现 CommandLineRunner,作用模拟调用 getUser() 接口, 会在 **Spring 容器启动完成**之后,马上执行CommandLineRunner.run() 方法。 ```java package com.codebear.springboothelloword.controller; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.CommandLineRunner; import org.springframework.stereotype.Component; @Component public class UserController implements CommandLineRunner { private static final Logger log = LoggerFactory.getLogger(UserController.class); @Override public void run(String... args) throws Exception { getUser(); log.error("一条 error 日志"); log.warn("一条 warn 日志"); log.info("一条 info 日志"); log.debug("一条 debug 日志"); log.trace("一条 trace 日志"); } public static String getUser() { log.info("getUser方法执行"); return "hello world"; } } ``` 启动就能看到日志输出在控制台  如果想要修改日志级别,使用 Spring boot 配置文件修改 ```yaml logging: level: root: WARN # 全局日志级别,默认是 INFO com.codebear.springboothelloword.controller: WARN # 指定包的日志级别 ``` 测试输出仅有两条日志:  实践发现,默认的日志框架需要创建一个 Logger 对象,有点麻烦,开发中经常引用一个 lombok 依赖,配合 `@Slf4j`注解更方便使用日志功能。 ## 介绍 Lombok **Lombok** 是一个 **编译时代码生成工具**,它本身不实现日志功能,不依赖 Logback。作用是提供注解 `@Slf4j`、`@Log4j2`,注解帮你**自动生成日志对象(Logger 变量)**,避免手动创建 Logger 对象。 另外 这是一个编译时代码生成工具,意思是在编译期会生成代码到 .class 文件中, 生成后的字节码和手写代码效果完全一致 , 所以最终.class 文件包含了 Logger 对象的代码,@Data 等注解也是生成代码,所以在打 jar 包时,可以不需要把 lombok 打入jar 包。 lombok 提供的注解有: | 注解 | 功能 | 举例 | | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------- | | `@Getter` / `@Setter` | 自动生成 getter/setter 方法 | `@Getter @Setter private String name;` | | `@ToString` | 自动生成 `toString()` 方法 | `@ToString` | | `@EqualsAndHashCode` | 自动生成 `equals()` 和 `hashCode()` | `@EqualsAndHashCode` | | `@NoArgsConstructor` / `@AllArgsConstructor` / `@RequiredArgsConstructor` | 自动生成构造器 | `@AllArgsConstructor` | | `@Data` | 综合注解,生成 getter/setter、toString、equals/hashCode、RequiredArgsConstructor | `@Data class User { private String name; }` | | `@Slf4j` / `@Log4j2` / `@Log` | 自动生成日志对象 `private static final Logger log` | `@Slf4j` → `log.info("hello")` | 其他还有 `@Builder`、`@SneakyThrows`、`@Cleanup` 等,用于简化构建对象、异常处理、资源关闭等。 所以lombok依赖仅仅是帮我们简化了代码的编写,本质上日志能力是由 logback 框架提供,并非 lombok 提供。 ### 日志输出目标 日志默认输出在控制台,如同上例。 我们可以设置日志输出到文件、消息队列等地方,。 输出到文件: ```yaml logging: file: name: logs/app.log # 指定日志文件(自动创建目录和文件) ``` 测试效果  输出到消息队列比较复杂,spring 配置文件默认仅支持控制台 + 文件输出 。 如果你要用输出到消息队列 , 就需要用 **Logback 原生配置**,也就是 `logback-spring.xml` 或 `logback.xml`。 新建配置文件 `logback-spring.xml`,路径如下:  Spring Boot 启动时,会自动去 **classpath 根目录**(也就是 `resources/`)下找这个文件。 有此需求再看官方文档或 AI 编写配置吧~
Spring核心 - 控制反转 IOC,用来大量例子来解释, 鱼总为啥推荐用@Resouce 注解?
上节我们通过一个简单示例讲解了 Spring 的使用和控制反转,重点就记住一个创建实例不用手动 new 对象,而是通过 xml 或者 java 配置类 或者 注解配置创建实例,然后getBean 获取实例,体现了依赖倒置原则,对象之间解耦。 这节继续讨论 spring 注册bean的那些事。 1. Spring框架管理这些Bean的创建工作,即由用户管理Bean转变为框架管理Bean,这个就叫**控制反转 - Inversion of Control (IoC)** 2. Spring 框架托管创建的Bean放在哪里呢? 这便是**IoC Container**; 3. pring 框架为了更好让用户配置Bean,必然会引入**不同方式来配置Bean? 这便是xml配置,Java配置,注解配置**等支持 4. Spring 框架既然接管了Bean的生成,必然需要**管理整个Bean的生命周期**等; 5. 应用程序代码从Ioc Container中获取依赖的Bean,注入到应用程序中,这个过程叫 **依赖注入(Dependency Injection,DI)** ; 所以说控制反转是通过依赖注入实现的,其实它们是同一个概念的不同角度描述。通俗来说就是**IoC是设计思想,DI是实现方式** 6. 在依赖注入时,有哪些方式呢?这就是构造器方式,@Autowired, @Resource, @Qualifier... 同时Bean之间存在依赖(可能存在先后顺序问题,以及**循环依赖问题**等) ## 理解IOC 先理解 spring Bean , 这个大家都不陌生,@Bean 注解用的不少,Bean 的概念在 spring 中就是一个对象实例,跟手动 new 出来的对象差不多,区别在于被 spring 容器管理的对象才能叫做 Bean. 手动new 的仅仅只是一个实例,当你给方法打上 @Bean 注解就代表方法的返回对象会交给 Spring 容器创建,管理,销毁。 再来理解 IOC 控制反转,控制反转是一种设计思想,并不是一种技术。什么是思想,比如类的单一职责就是一种思想,一种指导方法,这个方法存在于你的脑海里,实现靠你手写代码,并不是某一种技术帮你实现了单一职责。什么是技术?那就是具体可用的工具,IOC是一种设计思想,那么spring对这种思想的具体实现就是**依赖注入(DI)**。 再换一个大家都熟悉的例子举例思想和技术, 面向对象编程(OOP) 这是一种设计思想,实现这个思想的技术是 Java 的类和接口。 刚开始我会把思想、协议、技术混淆,这里做一个生活的类比。 - **思想** = 哲学(告诉你该怎么思考和设计) - **协议** = 法律(规定参与方必须遵守的规则) - **技术** = 工具(拿来解决问题的手段) 继续 IOC 的学习,控制反转这种思想是什么意思呢? Ioc 意味着将你设计好的对象交给容器控制,而不是传统的在你的对象内部直接控制。 深入分析搞清楚两个问题: - 谁控制谁,控制了什么? - 为何是反转,哪些方面反转了? ### 谁控制谁,控制了什么? 传统Java SE程序设计,我们直接在对象内部通过new进行创建对象,是程序主动去创建依赖对象;而IoC是有专门一个容器来创建这些对象,即由Ioc容器来控制对 象的创建;谁控制谁?当然是IoC 容器控制了对象;控制什么? IoC 容器控制了**对象**及其依赖的整个生命周期:创建、依赖注入、初始化、销毁,以及**资源的统一管理**,比如配置文件application.yml 、 数据库连接池、消息队列连接、文件资源等在 spring 中都交给了 IOC 容器管理。 举例看看 当我们在 spring 框架的配置文件中写的配置信息,我们是可以注入到自己的 java 代码中,要知道配置文件不属于 java 语言的一部分,为什么能注入到java语言的变量中呢?这就是 IOC 容器干的事。 ```yaml app: name: MyApp version: 1.0 ``` 你可以用 `@Value` 或者 `@ConfigurationProperties` 注入 : ```java @Component @ConfigurationProperties(prefix = "app") public class AppConfig { private String name; private String version; // getter/setter } ``` 你在配置文件中写的配置,spring 会帮你加载到 IOC 容器中,然后通过 @ConfigurationProperties 注入给 java 的变量。 在配置文件中写的数据库连接 ```yaml spring: rabbitmq: host: localhost port: 5672 username: guest password: guest ``` 同样交给容器管理, Spring 容器会自动帮你创建一个 **DataSource Bean**(通常是 HikariCP 连接池) ,底层的 spring 帮我们做了不少事。数据库连接池你只需要注入即可: ```java @Service public class UserService { @Autowired private DataSource dataSource; // 连接池 Bean 由 IoC 容器管理 } ``` 不过鱼总经常带我们使用 spring + mybatis plus 开发,对于 **DataSource Bean** 我们是没有感知的, MyBatis-Plus 本身不提供连接池,它只需要一个 **DataSource** 来获取连接, 在 Spring Boot 环境下 ,`MybatisAutoConfiguration` 会自动读取 Spring 容器里的 **DataSource Bean ,**所以我们不需要上面这一段注入连接池的配置**, MyBatis-Plus 自动帮你把 DataSource 用起来了 。** IOC 谁控制了谁、控制了什么资源这个问题大概讲清楚了,就是控制了 Bean , 交给IOC控制省了我们不少事。 IOC 思想下一个要搞明白的问题: 为何是反转,哪些方面反转了? ### 为何是反转,哪些方面反转了? 有反转就有正转,传统应用程序是由我们自己在对象中主动控制(主动 new)去直接获取依赖对象,也就是正转;而反转则是由容器来帮忙创建及注入依赖对象;为何是反转?因为**由容器**帮我们查找及注入依赖对象,对象只是**被动**的接受依赖对象,所以是反转;哪些方面反转了?**依赖对象的获取**被反转了。 传统java程序中,我们要用到另一个程序的方法,那就需要 new 一个实例对象,也就是当前对象依赖了另一个对象,这个依赖对象是当前对象自己管理的。而spring 程序是不用对象自己管理依赖对象,把控制权交给容器,对象自己不创建依赖对象,被迫接收从容器注入来的依赖对象,这就是反转。 用图说明,我们的客户端 App 类,想要调用 UserService 类 和 UserDao 类 , 需要自己 new 对象。  当有了IoC/DI的容器后,在客户端类中不再主动去创建这些对象了,如图  可以看到 IOC 容器主动创建了两个依赖对象(Bean) , 客户端就不用 new 对象了,从容器中获取依赖对象。 反转的问题也讲明白了。 怎么样对于IOC这个设计思想是不是理解了不少,感受到 IOC 容器带来解耦的爽点了吧,加上 spring 的配置文件,把数据库连接,消息队列,redis 各种 bean 都解耦了,我们只需要写个配置文件,IOC 容器就帮我们创建好对象,在spring应用中要创建redis实例,我们使用注解自动注入一个 redis 的 Bean 实例, 就能用 redis 了,太方便了。 我们都试过在某个类中自动注入很多Bean,如果全用 new 来创建对象,对象关系耦合度是不是很高,很难管理。 理解了 IOC 思想,继续探讨 IOC 思想发挥了什么作用。 ### IOC 能做什么? 其实IoC对编程带来的最大改变不是从代码上,而是从思想上,发生了“主从换位”的变化。应用程序原本是老大,要获取什么资源都是主动出击,但是在IoC/DI思想中,应用程序就变成被动的了,被动的等待IoC容器来创建并注入它所需要的资源了。 IoC很好的体现了面向对象设计法则之一—— **好莱坞法则:“别找我们,我们找你”**;即由IoC容器帮对象找相应的依赖对象并注入,而不是由对象主动去找。 ### IoC和DI是什么关系 控制反转是通过依赖注入实现的,其实它们是同一个概念的不同角度描述。文章前面也提到了 **IoC是设计思想,DI是实现方式**。 先来探索一下 DI 。先说明一下,在这里的 spring 容器的语境中, 组件就是容器管理的对象 ,可以是 @Component @Service @Controller 标记的类,甚至容器管理的 **资源类、工具类、连接池等,** 强调它在系统里的角色和功能,而不限定是具体哪种类。 DI—Dependency Injection,即依赖注入:组件之间依赖关系由容器在运行期决定,形象的说,即由容器动态的将某个依赖关系注入到组件之中。依赖注入的目的并非为软件系统带来更多功能,而是为了提升组件重用的频率,并为系统搭建一个灵活、可扩展的平台。通过依赖注入机制,我们只需要通过简单的配置,而无需任何代码就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心具体的资源来自何处,由谁实现。 我们来深入分析一下: - **谁依赖于谁**? 当然是应用程序依赖于IoC容器; - **为什么需要依赖**? 应用程序需要IoC容器来提供对象需要的外部资源; - **谁注入谁**? 很明显是IoC容器注入应用程序某个对象,应用程序依赖的对象; - **注入了什么**? 就是注入某个对象所需要的外部资源(包括对象、资源、常量数据)。 - **IoC和DI有什么关系呢**? 其实它们是同一个概念的不同角度描述,由于控制反转概念比较含糊(可能只是理解为容器控制对象这一个层面,很难让人想到谁来维护对象关系),所以2004年大师级人物Martin Fowler又给出了一个新的名字:“依赖注入”,相对IoC 而言,“依赖注入”明确描述了“被注入对象 **依赖** IoC容器 **配置** 依赖对象”。通俗来说就是**IoC是设计思想,DI是实现方式**。 回到实践的角度,帮助大家理解 IOC 与依赖注入,其实大家都往 IOC 容器配置过Bean, 也从使用过依赖注入,接下来就是归纳一下用法。 ## Ioc 配置的三种方式 IOC 三种配置方式: - xml 配置 - 注解 - Java 配置 上一篇文章讲述了三种配置方式了,这里不过再次归纳而已,总体上目前的主流方式是 **注解 + Java 配置**。我们使用spring boot 开发经常用的注解 @Component 就是注解方式给 IOC 容器配置/注册Bean。 @Bean 是 **java 配置类** 方式。 ### 1、XML 配置 顾名思义,就是将bean的信息配置.xml文件里,通过Spring加载文件为我们创建bean。这种方式出现很多早前的SSM项目中,将第三方类库或者一些配置工具类都以这种方式进行配置,主要原因是由于第三方类不支持Spring注解。 - **优点**: 可以使用于任何场景,结构清晰,通俗易懂 - **缺点**: 配置繁琐,不易维护,枯燥无味,扩展性差 **举例**: 1. 配置xx.xml文件 2. 声明命名空间和配置bean ```xml <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <!-- services --> <bean id="userService" class="com.fency.springframework.service.UserServiceImpl"> <property name="userDao" ref="userDao"/> <!-- additional collaborators and configuration for this bean go here --> </bean> <!-- more bean definitions for services go here --> </beans> ``` ### 2、注解配置 通过在类上加注解的方式,来声明一个类交给Spring管理,Spring会自动扫描带有@Component,@Controller,@Service,@Repository这四个注解的类,然后帮我们创建并管理,前提是需要先配置Spring的注解扫描器。 - **优点**:开发便捷,通俗易懂,方便维护。 - **缺点**:具有局限性,对于一些第三方资源,无法添加注解。只能采用XML或JavaConfig的方式配置 **举例**: 1. 对类添加@Component相关的注解,比如@Controller,@Service,@Repository 2. 设置ComponentScan的basePackage, 比如`<context:component-scan base-package='com.fency.springframework'>`, 或者`@ComponentScan("com.fency.springframework")`注解,或者 `new AnnotationConfigApplicationContext("com.fency.springframework")`指定扫描的basePackage. ```java @Service public class UserServiceImpl { /** * user dao impl. */ @Autowired private UserDaoImpl userDao; /** * find user list. * * @return user list */ public List<User> findUserList() { return userDao.findUserList(); } } ``` ### 3、Java 配置 将类的创建交给我们配置的JavcConfig类来完成,Spring只负责维护和管理,采用纯Java创建方式。其本质上就是把在XML上的配置声明转移到Java配置类中 - **优点**:适用于任何场景,配置方便,因为是纯Java代码,扩展性高,十分灵活 - **缺点**:由于是采用Java类的方式,声明不明显,如果大量配置,可读性比较差 **举例**: 1. 创建一个配置类, 添加@Configuration注解声明为配置类 2. 创建方法,方法上加上@bean,该方法用于创建实例并返回,该实例创建后会交给spring管理,方法名建议与实例名相同(首字母小写)。注:实例类不需要加任何注解 ```java @Configuration public class BeansConfig { /** * @return user dao */ @Bean("userDao") public UserDaoImpl userDao() { return new UserDaoImpl(); } /** * @return user service */ @Bean("userService") public UserServiceImpl userService() { UserServiceImpl userService = new UserServiceImpl(); userService.setUserDao(userDao()); return userService; } } ``` ## 依赖注入的三种方式 用的注入方式主要有三种:构造方法注入(Construct注入),setter注入,基于注解的注入(接口注入) ### setter方式 - **在XML配置方式中**,property都是setter方式注入,比如下面的xml: ```xml <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <!-- services --> <bean id="userService" class="com.fency.springframework.service.UserServiceImpl"> <property name="userDao" ref="userDao"/> <!-- additional collaborators and configuration for this bean go here --> </bean> <!-- more bean definitions for services go here --> </beans> ``` 本质上包含两步: 1. 第一步,需要new UserServiceImpl()创建对象, 所以需要默认构造函数 2. 第二步,调用setUserDao()函数注入userDao的值, 所以需要setUserDao()函数 所以对应的service类是这样的: ```java public class UserServiceImpl { /** * user dao impl. */ private UserDaoImpl userDao; /** * init. */ public UserServiceImpl() { } /** * find user list. * * @return user list */ public List<User> findUserList() { return this.userDao.findUserList(); } /** * set dao. * * @param userDao user dao */ public void setUserDao(UserDaoImpl userDao) { this.userDao = userDao; } } ``` - **在注解和Java配置方式下** ```java public class UserServiceImpl { /** * user dao impl. */ private UserDaoImpl userDao; /** * find user list. * * @return user list */ public List<User> findUserList() { return this.userDao.findUserList(); } /** * set dao. * * @param userDao user dao */ @Autowired public void setUserDao(UserDaoImpl userDao) { this.userDao = userDao; } } ``` 在Spring3.x刚推出的时候,推荐使用注入的就是这种, 但是这种方式比较麻烦,所以在Spring4.x版本中推荐构造函数注入。 ### 构造函数 - **在XML配置方式中**,`<constructor-arg>`是通过构造函数参数注入,比如下面的xml: ```xml <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <!-- services --> <bean id="userService" class="com.fency.springframework.service.UserServiceImpl"> <constructor-arg name="userDao" ref="userDao"/> <!-- additional collaborators and configuration for this bean go here --> </bean> <!-- more bean definitions for services go here --> </beans> ``` 本质上是new UserServiceImpl(userDao)创建对象, 所以对应的service类是这样的: ```java public class UserServiceImpl { /** * user dao impl. */ private final UserDaoImpl userDao; /** * init. * @param userDaoImpl user dao impl */ public UserServiceImpl(UserDaoImpl userDaoImpl) { this.userDao = userDaoImpl; } /** * find user list. * * @return user list */ public List<User> findUserList() { return this.userDao.findUserList(); } } ``` - **在注解和Java配置方式下** ```java @Service public class UserServiceImpl { /** * user dao impl. */ private final UserDaoImpl userDao; /** * init. * @param userDaoImpl user dao impl */ @Autowired // 这里@Autowired也可以省略 public UserServiceImpl(final UserDaoImpl userDaoImpl) { this.userDao = userDaoImpl; } /** * find user list. * * @return user list */ public List<User> findUserList() { return this.userDao.findUserList(); } } ``` 在Spring4.x版本中推荐的注入方式就是这种. ### 注解注入 也叫做字段注入,filed 注入 以@Autowired(自动注入)注解注入为例,修饰符有三个属性:Constructor,byType,byName。默认按照byType注入。 - **constructor**:通过构造方法进行自动注入,spring会匹配与构造方法参数类型一致的bean进行注入,如果有一个多参数的构造方法,一个只有一个参数的构造方法,在容器中查找到多个匹配多参数构造方法的bean,那么spring会优先将bean注入到多参数的构造方法中。 - **byName**:被注入bean的id名必须与set方法后半截匹配,并且id名称的第一个单词首字母必须小写,这一点与手动set注入有点不同。 - **byType**:查找所有的set方法,将符合符合参数类型的bean注入。 比如: ```java @Service public class UserServiceImpl { /** * user dao impl. */ @Autowired private UserDaoImpl userDao; /** * find user list. * * @return user list */ public List<User> findUserList() { return userDao.findUserList(); } } ``` 注解注入方式并不是 spring boot 出现才有的哦~ spring 本身支持注解注入。 这三种依赖注入方式,**spring 官方最推荐**的是构造器注入,实践中跟着鱼总开发用的最多的是注解注入,构造器注入有见过几次,这就是典型的 “理论 VS 实践”差异。注解注入太方便了! 不过还是要讲一讲为什么官方推荐构造器注入。 ### 为什么推荐构造器注入方式? 先来看看Spring在文档里怎么说: The Spring team generally advocates constructor injection as it enables one to implement application components as immutable objects and to ensure that required dependencies are not null. Furthermore constructor-injected components are always returned to client (calling) code in a fully initialized state. 简单的翻译一下:这个构造器注入的方式**能够保证注入的组件不可变,并且确保需要的依赖不为空**。此外,构造器注入的依赖总是能够在返回客户端(组件)代码的时候保证完全初始化的状态。 下面来简单的解释一下: - **依赖不可变**:其实说的就是final关键字。 - **依赖不为空**(省去了我们对其检查):当要实例化UserServiceImpl的时候,由于自己实现了有参数的构造函数,所以不会调用默认构造函数,那么就需要Spring容器传入所需要的参数,所以就两种情况:1、有该类型的参数->传入,OK 。2:无该类型的参数->报错。 - **完全初始化的状态**:这个可以跟上面的依赖不为空结合起来,向构造器传参之前,要确保注入的内容不为空,那么肯定要调用依赖组件的构造方法完成实例化。而在Java类加载实例化的过程中,构造方法是最后一步(之前如果有父类先初始化父类,然后自己的成员变量,最后才是构造方法),所以返回来的都是初始化之后的状态。 所以通常是这样的 ```java @Service public class UserServiceImpl { /** * user dao impl. */ private final UserDaoImpl userDao; /** * init. * @param userDaoImpl user dao impl */ public UserServiceImpl(final UserDaoImpl userDaoImpl) { this.userDao = userDaoImpl; } } ``` 如果使用setter注入,缺点显而易见,对于IOC容器以外的环境,除了使用反射来提供它需要的依赖之外,**无法复用该实现类**。而且将一直是个潜在的隐患,因为你不调用将一直无法发现NPE的存在。 那我们通过例子来找出这个 NPE: ```java @Service public class UserService { private UserRepository userRepository; @Autowired public void setUserRepository(UserRepository userRepository) { this.userRepository = userRepository; } public void doSomething() { userRepository.saveUser(); } } ``` 容器会在 **对象创建后** 调用 `setUserRepository()` 方法,把依赖注入进来。也就是说:对象本身一开始是 **半成品**,需要调用 setter 才完整。 如果一个普通的java程序里调用了 doSomething() , 可不会等待容器初始化,类加载直接开始跑。所以@Autowired 还没注入对象呢,就会 NPE ```java UserService userService = new UserService(); userService.doSomething(); // 会报错!userRepository 是 NPE ``` 那么我们用最多的**注解注入(field 字段注入)**有啥问题? 循环依赖,都遇到过哈~ 即A里面注入B,B里面又注入A: ```java public class A { @Autowired private B b; } public class B { @Autowired private A a; } ``` ### @Autowired和@Resource以及@Inject等注解注入有何区别? 经典面试题 首先先来系统学习一下三个注解,了解了这三个注解,区别自然浮现于水面。 #### @Autowired - **Autowired注解源码** 在Spring 2.5 引入了 @Autowired 注解 ```java @Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Autowired { boolean required() default true; } ``` 从Autowired注解源码上看,可以使用在下面这些地方: ```java @Target(ElementType.CONSTRUCTOR) #构造函数 @Target(ElementType.METHOD) #方法 @Target(ElementType.PARAMETER) #方法参数 @Target(ElementType.FIELD) #字段、枚举的常量 @Target(ElementType.ANNOTATION_TYPE) #注解 ``` 还有一个value属性,默认是true。 - **简单总结**: 1、@Autowired是Spring自带的注解,通过AutowiredAnnotationBeanPostProcessor 类实现的依赖注入 2、@Autowired可以作用在CONSTRUCTOR、METHOD、PARAMETER、FIELD、ANNOTATION_TYPE 3、@Autowired默认是根据类型(byType )进行自动装配的 4、如果有多个类型一样的Bean候选者,需要指定按照名称(byName )进行装配,则需要配合@Qualifier。 指定名称后,如果Spring IOC容器中没有对应的组件bean抛出NoSuchBeanDefinitionException。也可以将@Autowired中required配置为false,如果配置为false之后,当没有找到相应bean的时候,系统不会抛异常 - **简单使用代码**: 在字段属性上。 ```java @Autowired private HelloDao helloDao; ``` 或者 ```java private HelloDao helloDao; public HelloDao getHelloDao() { return helloDao; } @Autowired public void setHelloDao(HelloDao helloDao) { this.helloDao = helloDao; } ``` 或者 ```java private HelloDao helloDao; //@Autowired public HelloServiceImpl(@Autowired HelloDao helloDao) { this.helloDao = helloDao; } // 构造器注入也可不写@Autowired,也可以注入成功。 ``` 将@Autowired写在被注入的成员变量上,setter或者构造器上,就不用再xml文件中配置了。 如果有多个类型一样的Bean候选者,则默认根据设定的属性名称进行获取。如 HelloDao 在Spring中有 helloWorldDao 和 helloDao 两个Bean候选者。 ```java @Autowired private HelloDao helloDao; ``` 首先根据类型获取,发现多个HelloDao,然后根据helloDao进行获取,如果要获取限定的其中一个候选者,结合@Qualifier进行注入。 ```java @Autowired @Qualifier("helloWorldDao") private HelloDao helloDao; ``` 注入名称为helloWorldDao 的Bean组件。@Qualifier("XXX") 中的 XX是 Bean 的名称,所以 @Autowired 和 @Qualifier 结合使用时,自动注入的策略就从 byType 转变成 byName 了。 注意:使用@Qualifier 时候,如何设置的指定名称的Bean不存在,则会抛出异常,如果防止抛出异常,可以使用: ```java @Qualifier("xxxxyyyy") @Autowired(required = false) private HelloDao helloDao; ``` 在SpringBoot中也可以使用@Bean+@Autowired进行组件注入,将@Autowired加到参数上,其实也可以省略。 ```java @Bean public Person getPerson(@Autowired Car car){ // @Autowired 其实也可以省略 return new Person(); } ``` #### @Resource - **Resource注解源码** ```java @Target({TYPE, FIELD, METHOD}) @Retention(RUNTIME) public @interface Resource { String name() default ""; // 其他省略 } ``` 从Resource注解源码上看,可以使用在下面这些地方: ```java @Target(ElementType.TYPE) #接口、类、枚举、注解 @Target(ElementType.FIELD) #字段、枚举的常量 @Target(ElementType.METHOD) #方法 ``` name 指定注入指定名称的组件。 - **简单总结**: 1、@Resource是JSR250规范的实现,在javax.annotation包下 2、@Resource可以作用TYPE、FIELD、METHOD上 3、@Resource是默认根据属性名称进行自动装配的,如果有多个类型一样的Bean候选者,则可以通过name进行指定进行注入 - **简单使用代码**: ```java @Component public class SuperMan { @Resource private Car car; } ``` 按照属性名称 car 注入容器中的组件。如果容器中BMW还有BYD两种类型组件。指定加入BMW。如下代码: ```java @Component public class SuperMan { @Resource(name = "BMW") private Car car; } ``` name 的作用类似 @Qualifier #### @Inject - **Inject注解源码** ```java @Target({ METHOD, CONSTRUCTOR, FIELD }) @Retention(RUNTIME) @Documented public @interface Inject {} ``` 从Inject注解源码上看,可以使用在下面这些地方: ```java @Target(ElementType.CONSTRUCTOR) #构造函数 @Target(ElementType.METHOD) #方法 @Target(ElementType.FIELD) #字段、枚举的常量 ``` - **简单总结**: 1、@Inject是JSR330 (Dependency Injection for Java)中的规范,需要导入javax.inject.Inject jar包 ,才能实现注入 2、@Inject可以作用CONSTRUCTOR、METHOD、FIELD上 3、@Inject是根据类型进行自动装配的,如果需要按名称进行装配,则需要配合@Named; - **简单使用代码**: ```java @Inject private Car car; ``` 指定加入BMW组件。 ```java @Inject @Named("BMW") private Car car; ``` @Named 的作用类似 @Qualifier! 有没有看出重点? JSR 标准不理解对吧 **JSR** 是 Java Specification Request(Java 规范提案),它是 Java 社区制定的标准规范,描述如何实现某种功能或接口。 这就是重点区别,`@Autowired` → Spring 专有,只能在 Spring 容器下用 → 没有 **JSR** **标准, 换容器就可能不兼容** **@Resource 是 JSR-250 规范的一部分** → 意味着它是 **Java 官方推荐的标准方式**,任何支持 JSR-250 的容器都能用。 总结, @Resource 注解符合的是 java 规范、JSR 标准,兼容性更强,使用方式也类似于 @Autowired , 可能这就是鱼总推荐 @Resouce注解的原因。
