spring boot 核心概念
IoC 控制反转 和 DI 依赖注入
IOC 是一种设计思想,是将对象的创建和管理权转给了 spring 容器,让 spring 容器来管理对象,也即控制权反转。
DI (依赖注入,Dependency Injection)是 IoC 的实现方式之一,由 spring 容器来创建并注入所依赖的对象,如下(UserService 就是依赖):
@Service
class OrderService {
private UserService userService;
}
在类上添加 @Controller、@Service 、@Component 等注解,将其声明为 Bean ,让 spring 来管理,就能实现自动注入。
DI工作流程
如下,如果某个依赖有多个实现,则必须通过 @Primary 来声明默认优先的实现;否则在依赖注入时,就会因为无法确定使用哪一个实现而报错;
public interface UserService {}
@Service
public class UserServiceImpl1 implements UserService {}
@Primary
@Service
public class UserServiceImpl2 implements UserService {}
如下,也可以在主动指定依赖注入是使用哪个一个实现
publicOrderService(@Qualifier("userServiceImpl2") UserServiceuserService) {
this.userService=userService;
}
如下,也明确声明要注入的实现
private final UserServiceuserServiceImpl1;
如下,如果依赖对象有多个构造器,可通过 @Autowred 来指定默认的构造器
@Autowired
publicOrderService(UserServiceuserService) {
this.userService=userService;
}
Spring 判断寻找依赖的顺序如下:
1>. 按类型找
2>. 如果多个 → 按 @Qualifier
3>. 如果没写 → 按变量名
4>. 再不行 → 报错
AOP
AOP 就是在业务逻辑前、后插入“通用功能”:
@Aspect
@Component
public class LogAspect {
@Before("execution(* com.example.service.*.*(..))")
public void before() {
System.out.println("方法执行前");
}
@After("execution(* com.example.service.*.*(..))")
public void after() {
System.out.println("方法执行后");
}
}
@Transactional 就是 AOP 的典型实现;即,在方法前后自动包一层事务逻辑。具体流程是:方法调用 → AOP代理 → 开启事务 → 执行业务 → 提交/回滚
@Transactional 事务
@Transactional 通常用在 Service 层类的方法上,如下:
@Transactional
public void createOrder() {}
执行流程如下:
调用 createOrder()
↓
进入代理对象
↓
TransactionInterceptor 拦截
↓
1>. 开启事务(begin)
2>. 执行业务方法
3>. 判断是否异常
- 无异常 → commit
- 有异常 → rollback
1>. 默认只回滚 RuntimeException
默认情况下,Spring 只对“运行时异常”(RuntimeException)及其子类,以及“错误”(Error)进行回滚。在 Java 中,Exception 是所有异常的父类,它包含 受检异常(Checked Exception)。手动抛出一个显式的 Exception(属于受检异常)时,Spring 的事务拦截器会捕获它,但发现它不在回滚列表中,因此依然会提交事务,导致数据没有回滚。(受检异常 (Checked Exception) 是指在编译阶段必须处理的异常,否则无法通过编译)
@Transactional
public void test() throws Exception {
throw new Exception(); // ❌ 不会回滚
}
正确写法
## 显式指示 Spring 框架,只要方法抛出 Exception(及其任何子类),就立即回滚当前数据库事务
@Transactional(rollbackFor = Exception.class)
2>. 传播行为(Propagation)
传播行为(Propagation) 定义了当一个事务方法被另一个事务方法调用时,事务该如何生存;它决定了业务方法之间是共用一个事务,还是开启新事务。
@Transactional(propagation = Propagation.REQUIRED)
👉 含义:
有事务 → 加入 没事务 → 创建
常见几种类型说明:
- REQUIRED 默认:必须有事务;如果调用者已经开启了事务,则被调用者方法直接加入这个事务,所有参与的方法都在同一个事务里,只要任何一个地方报错,整个链路全部回滚。如果调用者没有事务,则被调用者就自己开启一个独立的新事务,且不会将调用者包含到事务中去。
被调用者开启新事务的前提是,被调用者方法有 @Transactional 声明,否则被调用者就是一个普通方法。
如果调用者有事务,但被调用都没有事务,则被调用者会包含到调用者的事务中。如果调用者没有事务,但被调用者有 @Transactional 声明,则被调用者会开启自己的独立事务,且不会将调用者包含到事务中去。
- REQUIRES_NEW :不管调用者有没有事务,被调用都要新开一个独立事务;
- SUPPORTS :如果调用者有事务,则在事务里运行;否则就以非事务(普通逻辑)方式运行;
3>. 隔离级别(Isolation)
隔离级别(Isolation) 是定义“一个事务对数据库的操作,在什么程度上对其他事务可见”;也即决定了当多个事务并发执行时,它们之间如何“防干扰”。
脏读 (Dirty Read):是指事务 A 改了数据但还没提交,事务 B 就读到了这个“脏数据”;如果事务 A 随后回滚了,事务 B 读到的就是不存在的假数据。
不可重复读 (Non-repeatable Read):是事务 A 读了一行数据,事务 B 随后修改并提交了这行;事务 A 再次读,发现数据变了;针对的是同一条数据的 Update(修改)。
幻读 (Phantom Read):事务 A 查了一次范围,事务 B 随后插入并提交了新记录;事务 A 再次查时,发现多出了一行;针对的是 Insert/Delete(增删) 导致的行数变化。
// READ_COMMITTED 大多数主流数据库的默认级别,意思是一个事务只能读取到其他事务已经提交的数据。它彻底解决了 脏读,但无法防止不可重复读 和 幻读。
@Transactional(isolation = Isolation.READ_COMMITTED)
4>. 同类方法调用失效
@Service
public class OrderService {
public void A() {
B(); // ❌ 事务不会生效
}
@Transactional
public void B() {}
}
Spring 的 @Transactional 是通过 AOP(面向切面编程) 实现的。正常情况,当从外部调用 orderService.B() 时,你拿到的其实是 Spring 生成的代理对象。代理对象会先开启事务,再调用实际的 B() 方法。但如果在 A() 内部调用 B() 时,本质上执行的是 this.B()。this 指向的是目标类实例本身,而不是 Spring 的 代理对象;因此,Spring 拦截不到 B() 的执行,B() 的注解自然就成了摆设,事务也就不会生效。这种情况下,最为 A() 加注解 @Transactional 。
5> private 方法无效
AOI 无法代理 private 方法,因为 spring 的代理对象无法调用目标实例的私有方法;
@Transactional
private void doSomething() {} ❌
6> try-catch 吃掉异常
如果在事务方法中自行捕获异常,会导致异常传导至 AOP 代理对象,则事务就无法回滚,如下:
@Transactional
public void test() {
try {
// 异常
} catch (Exception e) {
// 吞掉异常 ❌
}
}
7> 多线程失效
Spring 的事务管理是基于线程绑定的(ThreadLocal),跨线程调用会导致事务上下文丢失。 ThreadLocal 里的变量是线程隔离的,当开启 new Thread() 时,新线程里没有任何事务信息,也拿不到主线程的那个数据库连接。新线程会去自己当前线程中的连接池中申请一个新的数据库连接;也就是主线程和子线程完全不在一个数据库连接中,子线程的回滚主线程也无法感知的到。
@Transactional
public void test() {
new Thread(() -> {
// ❌ 不在事务中
}).start();
}
6. 只读事务误用
不要在读取事务中做写操作(有些数据库会优化甚至忽略写)
@Transactional(readOnly = true)
当数据库检测到 readOnly = true 时就会跳过某些为了支持“写操作”而设计的内部锁逻辑,甚至优化事务 ID 的分配,跳过修改检查,从而减轻数据库负担,提升查询速度;同时,readOnly = true 也能让语义清晰。
7. 大事务(性能问题)
尽量不要在一个事务中进行大量操作,这会导致锁时间长,回滚成本高;因此尽量要拆成小事务(批处理)
@Transactional
public void bigTask() {
// 处理1万条数据
}