SpringBoot 学习第六天:全局异常处理 + 数据校验 + 事务管理 + 分布式锁基础 + 新面试题(无重复)

SpringBoot 学习第六天:全局异常处理 + 数据校验 + 事务管理 + 分布式锁基础 + 新面试题(无重复)

一、今日核心目标

  • 掌握 SpringBoot 全局异常统一处理,实现标准化接口响应

  • 精通 Validator 参数校验,完成入参合法性自动校验

  • 深入 Spring 事务底层原理、传播机制、失效场景

  • 理解分布式锁核心思想,掌握 Redis 分布式锁实现

  • 掌握 Day6 专属高频面试题,与 Day1-Day5 无任何重复

二、全局异常统一处理(生产必备)

2.1 核心注解与实战代码

核心注解:

  • @RestControllerAdvice:全局异常切面,拦截所有 Controller 层抛出的异常

  • @ExceptionHandler:指定当前方法捕获的异常类型(可精准匹配单一异常或异常父类)

实战代码(可直接复制到项目中使用):

import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import lombok.extern.slf4j.Slf4j;

// 全局异常处理切面,结合SLF4J日志
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    // 捕获业务异常(自定义异常)
    @ExceptionHandler(BusinessException.class)
    public ResultVO handleBusinessException(BusinessException e) {
        log.error("业务异常:{}", e.getMessage(), e);
        return ResultVO.fail(e.getCode(), e.getMessage());
    }

    // 捕获参数校验异常
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResultVO handleValidException(MethodArgumentNotValidException e) {
        // 获取校验失败的提示信息
        String msg = e.getBindingResult().getFieldErrors().get(0).getDefaultMessage();
        log.error("参数校验异常:{}", msg);
        return ResultVO.fail(400, msg);
    }

    // 捕获全局异常(兜底)
    @ExceptionHandler(Exception.class)
    public ResultVO handleException(Exception e) {
        log.error("系统异常:{}", e.getMessage(), e);
        // 隐藏系统敏感信息,返回通用提示
        return ResultVO.fail(500, "系统繁忙,请稍后再试");
    }
}

// 自定义业务异常(可根据项目调整)
public class BusinessException extends RuntimeException {
    private Integer code;

    public BusinessException(Integer code, String message) {
        super(message);
        this.code = code;
    }

    // getter/setter
    public Integer getCode() {
        return code;
    }
}

// 统一响应结果(配合异常处理使用)
public class ResultVO {
    private Integer code;
    private String message;
    private Object data;

    // 成功/失败静态方法
    public static ResultVO success(Object data) {
        return new ResultVO(200, "操作成功", data);
    }

    public static ResultVO fail(Integer code, String message) {
        return new ResultVO(code, message, null);
    }

    // 构造器、getter/setter
}

2.2 底层原理

SpringMVC 启动时,会自动扫描并注册带有 @RestControllerAdvice 注解的类作为「全局异常解析器」(实现 HandlerExceptionResolver 接口)。

当 Controller 方法执行过程中抛出异常时,SpringMVC 会通过「异常匹配器」找到对应 @ExceptionHandler 注解指定的异常类型,执行该方法并返回统一的 JSON 响应结果,避免代码中零散的 try/catch 冗余,同时隐藏系统敏感异常信息。

2.3 核心作用

  • 统一接口异常返回格式,提升接口规范性,便于前端统一处理

  • 集中记录异常日志,方便项目上线后定位问题

  • 隐藏系统底层敏感信息(如数据库异常、代码报错详情),提升系统安全性

全局异常处理逻辑图:

graph TD A[请求进入Controller] --> B[执行目标方法] B -->|无异常| C[正常返回结果] B -->|抛出异常| D[SpringMVC捕获异常] D --> E[匹配@ExceptionHandler注解] E -->|匹配业务异常| F[执行handleBusinessException] E -->|匹配参数校验异常| G[执行handleValidException] E -->|匹配其他异常| H[执行handleException] F & G & H --> I[返回统一ResultVO响应] I --> J[前端接收并处理] ``` # 三、Validator 参数校验(入参安全必备) ## 3\.1 核心注解与依赖 SpringBoot 2\.3\+ 需手动引入依赖(低版本自动集成): ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency>

常用校验注解(直接用在实体类字段上):

  • @NotBlank:字符串非空、非空白(不能是空格)

  • @NotNull:任意类型不能为 null(适用于 Integer、Long 等包装类)

  • @Email:字符串符合邮箱格式

  • @Length\(min = 6, max = 16\):字符串长度在指定范围

  • @Pattern\(regexp = \&\#34;^\\\[0\-9\]\{11\}$\&\#34;\):字符串匹配指定正则(如手机号)

  • @Min\(1\):数字最小值(适用于 Integer、Long)

3.2 实战代码

// 1. 实体类(添加校验注解)
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Length;

public class UserDTO {
    @NotBlank(message = "用户名不能为空")
    @Length(min = 3, max = 10, message = "用户名长度3-10位")
    private String username;

    @NotBlank(message = "密码不能为空")
    @Length(min = 6, max = 16, message = "密码长度6-16位")
    private String password;

    @Email(message = "邮箱格式不正确")
    private String email;

    // getter/setter
}

// 2. Controller(添加@Valid注解触发校验)
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;

@RestController
public class UserController {

    @PostMapping("/register")
    public ResultVO register(@Valid @RequestBody UserDTO userDTO) {
        // 校验通过,执行业务逻辑(无需手动判空)
        return ResultVO.success("注册成功");
    }
}

3.3 底层原理

基于 JSR-380 Java 校验规范,SpringBoot 自动集成 Hibernate Validator 作为具体实现。

当 Controller 方法添加 @Valid 注解后,Spring 会通过 AOP 前置拦截 机制,在目标方法执行前对入参进行校验:

  1. 校验参数是否符合注解规则,若不合法则抛出 MethodArgumentNotValidException异常

  2. 该异常会被全局异常处理器捕获,返回统一的校验失败提示

3.4 核心作用

  • 替代手动判空、格式校验代码,减少冗余,提升开发效率

  • 提前拦截非法参数,避免非法数据进入业务层、数据库,保证数据安全

  • 统一校验提示,提升接口用户体验

Validator 参数校验逻辑图:

graph TD A[前端提交请求参数] --> B[Controller接收请求] B --> C[@Valid注解触发校验] C --> D[Hibernate Validator执行校验] D -->|校验通过| E[执行业务逻辑] D -->|校验失败| F[抛出MethodArgumentNotValidException] F --> G[全局异常处理器捕获] G --> H[提取校验失败提示] H --> I[返回统一失败响应] E --> J[返回成功响应]``` # 四、Spring 事务管理(数据一致性核心) ## 4\.1 核心注解与基础使用 核心注解:`@Transactional`(可加在类上或方法上) - 加在类上:该类中所有 public 方法都将被事务管理 - 加在方法上:仅当前方法被事务管理(方法级别优先级高于类级别) 基础实战代码: ```java import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; @Service public class OrderService { @Resource private OrderMapper orderMapper; @Resource private OrderItemMapper orderItemMapper; // 事务管理:保证下单、扣减库存两步原子性 @Transactional public void createOrder(OrderDTO orderDTO) { // 1. 新增订单 orderMapper.insert(orderDTO); // 2. 新增订单明细(若此处抛异常,订单新增会回滚) orderItemMapper.insertBatch(orderDTO.getOrderItems()); } }

4.2 底层原理

Spring 事务的底层实现基于 AOP 动态代理(与 Day5 学习的 AOP 原理一致),结合 PlatformTransactionManager(事务管理器)实现:

  1. 当方法被 @Transactional 标记时,Spring 会为该方法生成代理对象

  2. 代理对象执行方法前:通过事务管理器开启事务,获取数据库连接,设置事务隔离级别

  3. 执行目标方法:若方法正常执行,事务管理器提交事务,释放连接

  4. 若方法抛出异常:事务管理器回滚事务,释放连接(默认只回滚 RuntimeException 及其子类)

补充:SpringBoot 2.0 后默认使用 CGLIB 动态代理(无需目标类实现接口),可通过配置切换为 JDK 动态代理。

4.3 事务传播机制(重点)

通过 @Transactional\(propagation = Propagation\.XXX\) 设置,控制多个事务方法调用时的事务行为,常用 4 种:

  • REQUIRED(默认):如果当前有事务,就加入该事务;如果没有事务,就新建一个事务(最常用,如下单、支付场景)

  • REQUIRES_NEW:无论当前是否有事务,都新建一个独立事务,原事务挂起;新事务执行完成后,再恢复原事务(如日志记录,即使业务事务回滚,日志也要保留)

  • NESTED:嵌套事务,依赖于主事务;主事务回滚,子事务也会回滚;子事务回滚,主事务可选择不回滚(如订单下单后,扣减库存失败,仅回滚库存,不回滚订单)

  • SUPPORTS:支持当前事务,若当前没有事务,就以非事务方式运行(如查询接口,有无事务都可)

4.4 事务失效场景(高频踩坑点)

以下场景中,@Transactional 注解会失效,事务无法正常提交/回滚:

  1. 方法不是 public 修饰:Spring 事务仅对 public 方法生效(非 public 方法无法被 AOP 代理)

  2. 同类内部方法调用:如 service 中 methodA 调用本类的 methodB(methodB 加了 @Transactional),此时 methodB 的事务失效(因为内部调用不走代理)

  3. 异常被 try/catch 吃掉:方法内抛出异常后,手动 catch 但未重新抛出,事务无法感知异常,会正常提交

  4. 抛出非 RuntimeException 且未指定 rollbackFor:默认只回滚 RuntimeException 及其子类,若抛出 Checked 异常(如 IOException),需设置 @Transactional\(rollbackFor = Exception\.class\)

  5. 多线程调用:两个事务方法分属不同线程,无法共享事务上下文,各自独立执行,无法回滚

  6. 数据库引擎不支持事务:如 MySQL 的 MyISAM 引擎(需切换为 InnoDB 引擎)

Spring 事务执行逻辑图:

graph TD A[调用事务方法] --> B[生成CGLIB/JDK代理对象] B --> C[事务管理器开启事务] C --> D[获取数据库连接,设置隔离级别] D --> E[执行目标业务方法] E -->|正常执行| F[事务管理器提交事务] E -->|抛出异常| G[事务管理器回滚事务] F & G --> H[释放数据库连接] H --> I[返回执行结果] ``` # 五、分布式锁基础(Redis 实现,集群必备) ## 5\.1 核心问题(为什么需要分布式锁) Day5 学习的本地锁(如 synchronized、Lock)仅在单机、单实例环境下有效。当项目部署为集群、多实例时,本地锁无法跨实例共享,会出现并发安全问题: - 超卖问题:多实例同时扣减库存,导致库存负数 - 重复提交:多实例同时接收同一请求,导致重复数据 分布式锁的核心作用:跨实例、跨节点实现全局互斥,保证集群环境下的并发安全。 ## 5\.2 底层原理(Redis 实现) 利用 Redis 的 **SETNX \+ EXPIRE 原子命令**,实现锁的抢占与自动释放,核心逻辑: 1. 抢占锁:执行 `SET lock\_key 唯一值 NX EX 30`,只有当 lock\_key 不存在时,才能设置成功(即抢占到锁) 2. 释放锁:执行 Lua 脚本,判断锁的唯一值是否为当前线程所有,若是则删除锁(保证原子性) 3. 自动释放:通过 EX 指令设置过期时间(如 30 秒),防止服务宕机后锁无法释放,导致死锁 关键说明: - 唯一值:通常用 UUID \+ 线程 ID 生成,确保每个线程的锁唯一,避免误删别人的锁 - 原子性:SETNX 和 EXPIRE 必须原子执行(Redis 3\.0\+ 支持一次性执行),否则会出现“设置了锁但未设置过期时间”的死锁风险 ## 5\.3 实战代码(Redis 分布式锁实现) ```java import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; import javax.annotation.Resource; import java.util.UUID; import java.util.concurrent.TimeUnit; @Component public class RedisDistributedLock { @Resource private StringRedisTemplate stringRedisTemplate; // 锁的过期时间(30秒) private static final long LOCK_EXPIRE = 30; // 抢占锁 public String tryLock(String lockKey) { // 生成唯一值(用于释放锁时校验) String lockValue = UUID.randomUUID().toString() + ":" + Thread.currentThread().getId(); // 原子操作:SETNX + EXPIRE Boolean success = stringRedisTemplate.opsForValue() .setIfAbsent(lockKey, lockValue, LOCK_EXPIRE, TimeUnit.SECONDS); // 抢占成功,返回唯一值;失败返回null return success != null && success ? lockValue : null; } // 释放锁(Lua脚本保证原子性) public void releaseLock(String lockKey, String lockValue) { // Lua脚本:判断锁的value是否等于lockValue,若是则删除 String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; stringRedisTemplate.execute( new DefaultRedisScript<>(luaScript, Integer.class), Collections.singletonList(lockKey), lockValue ); } } // 业务中使用 @Service public class StockService { @Resource private RedisDistributedLock distributedLock; @Resource private StockMapper stockMapper; public void deductStock(Long productId) { // 1. 抢占锁 String lockKey = "stock:lock:" + productId; String lockValue = distributedLock.tryLock(lockKey); if (lockValue == null) { throw new BusinessException(400, "请求过于频繁,请稍后再试"); } try { // 2. 执行业务(扣减库存) Stock stock = stockMapper.selectById(productId); if (stock.getStock() <= 0) { throw new BusinessException(400, "库存不足"); } stock.setStock(stock.getStock() - 1); stockMapper.updateById(stock); } finally { // 3. 释放锁(无论成功失败,都要释放) distributedLock.releaseLock(lockKey, lockValue); } } }

5.4 核心注意事项

  • 必须设置过期时间:防止服务突然宕机,锁无法释放,导致死锁

  • 必须使用唯一值:避免线程 A 加的锁,被线程 B 误删除(如线程 A 超时未释放,线程 B 抢占锁后,线程 A 恢复执行删除锁)

  • 释放锁必须用 Lua 脚本:保证“判断锁归属 + 删除锁”两步是原子操作,避免并发场景下的误删问题

  • 可选优化:锁续期(如业务执行时间超过锁过期时间,通过定时任务延长锁的过期时间)

Redis 分布式锁执行逻辑图:

graph TD A[业务线程发起锁请求] --> B[生成唯一lockValue] B --> C[执行SET lock_key lockValue NX EX 30] C -->|设置成功(抢占锁)| D[执行业务逻辑] C -->|设置失败(未抢到锁)| E[返回请求频繁提示] D --> F[执行Lua脚本释放锁] F --> G[判断lockValue是否匹配] G -->|匹配| H[删除lock_key,释放锁] G -->|不匹配| I[不操作,避免误删] E & H & I --> J[流程结束] ``` # 六、Day6 专属高频面试题(无重复,直接背诵) ## 基础必背题(入门级) 1. 问:SpringBoot 全局异常处理的两个核心注解是什么? 答:`@RestControllerAdvice`(全局异常切面)和 `@ExceptionHandler`(指定捕获的异常类型)。 2. 问:`@Valid` 和 `@Validated` 有什么区别? 答:① `@Valid` 是 JSR\-380 标准注解,支持嵌套校验(如实体类中包含另一个实体类);② `@Validated` 是 Spring 扩展注解,支持分组校验,不支持嵌套校验。 3. 问:Spring 事务的核心注解是什么?默认的事务传播机制是什么? 答:核心注解是 `@Transactional`;默认传播机制是 REQUIRED(有事务则加入,无则新建)。 ## 进阶高频题(开发级) 1. 问:`@Transactional` 的底层原理是什么? 答:基于 AOP 动态代理实现,通过事务管理器(PlatformTransactionManager)控制数据库连接的开启、提交与回滚;目标方法执行前开启事务,正常执行则提交,抛出异常则回滚。 2. 问:请说出 3 种 Spring 事务失效的场景? 答:① 方法不是 public 修饰;② 同类内部方法调用(不走代理);③ 异常被 try/catch 吃掉,未重新抛出;④ 抛出非 RuntimeException 且未指定 rollbackFor(任选 3 种即可)。 3. 问:事务传播机制中,REQUIRED 和 REQUIRES\_NEW 的核心区别是什么? 答:REQUIRED 会加入当前已存在的事务,与原事务共用一个事务(同成功、同回滚);REQUIRES\_NEW 会新建一个独立事务,挂起原事务,新事务与原事务互不影响。 4. 问:为什么需要分布式锁?本地锁在集群环境下为什么会失效? 答:① 分布式锁用于解决集群、多实例环境下的并发安全问题(如超卖、重复提交);② 本地锁(如 synchronized)仅作用于当前 JVM 实例,多实例之间无法共享锁,因此会失效。 ## 原理加分题(面试加分项) 1. 问:Redis 分布式锁为什么必须设置过期时间? 答:防止服务突然宕机(如断电、崩溃),锁无法手动释放,导致锁一直存在,其他线程无法抢占锁,造成死锁。 2. 问:Redis 分布式锁为什么要用唯一值(lockValue)? 答:避免“锁超时”导致的误删问题:比如线程 A 持有锁但执行超时,锁自动过期,线程 B 抢占到锁;此时线程 A 恢复执行,若没有唯一值校验,会误删除线程 B 持有的锁。 3. 问:分布式锁释放锁时,为什么要用 Lua 脚本? 答:为了保证“判断锁归属(锁的 value 是否为当前线程的唯一值)\+ 删除锁”两步操作的原子性;若分开执行,会出现并发安全问题(如判断完归属后,锁过期被其他线程抢占,此时再删除锁,会误删别人的锁)。 4. 问:全局异常处理器为什么能拦截所有 Controller 的异常? 答:`@RestControllerAdvice` 注解会被 SpringMVC 注册为全局异常解析器,实现 HandlerExceptionResolver 接口;当 Controller 抛出异常时,SpringMVC 会优先调用该解析器处理异常,从而实现全局拦截。 # 七、Day6 学习总结 今日学习的知识点,均为 SpringBoot 生产级项目必备的硬核能力,且与 Day1\-Day5(Bean 生命周期、自动配置、日志、AOP、拦截器)无任何重复,重点掌握: - 全局异常处理:解决接口异常返回不统一、敏感信息泄露问题,核心是 `@RestControllerAdvice` \+ `@ExceptionHandler`,底层基于异常解析器。 - 参数校验:减少手动校验代码,保证入参安全,核心是 Validator 注解 \+ `@Valid`,底层基于 JSR\-380 规范和 AOP 前置拦截。 - 事务管理:保证数据一致性,核心是 `@Transactional`,重点掌握传播机制和失效场景,底层基于 AOP 动态代理。 - 分布式锁:解决集群并发安全问题,重点掌握 Redis 实现方式,核心是 SETNX \+ EXPIRE 原子命令和 Lua 脚本释放锁。 建议:手动编写一遍全局异常、参数校验、事务失效场景、Redis 分布式锁的实战代码,结合今日面试题,加深对底层原理的理解,为后续项目开发和面试做好准备。 posted @ 2026\-04\-25 11:04 白鹿为溪 阅读\(0\) 评论\(0\) 收藏 举报 > (注:文档部分内容可能由 AI 生成)
posted @ 2026-04-25 23:00  白鹿为溪  阅读(35)  评论(0)    收藏  举报