Spring AOP 详细讲解
一、AOP 是什么
AOP:Aspect Oriented Programming,面向切面编程。
AOP 的核心原理就是动态生成代理对象,在不改动原业务代码的情况下,通过代理去拦截目标方法,在方法前后插入切面逻辑。
核心思想:业务代码只关心业务,公共逻辑交给切面。
典型通用场景:
日志记录、性能统计、权限校验、事务控制、异常处理、埋点。
例子:Spring 的 @Transactional 底层就是 AOP。
二、AOP 核心术语
JoinPoint 连接点 程序执行过程中可以被切面切入的点。
Spring AOP 只支持方法级别的连接点(只能拦截方法执行,不能拦截字段、构造函数)。
简单说:Spring 里,所有目标对象的方法都是连接点。
Pointcut 切点 匹配规则,用来筛选哪些连接点需要被切面增强。
通过表达式匹配方法,只有匹配上的方法才会执行通知。
JoinPoint 是候选点位,Pointcut 是挑选规则。
- @Before:前置通知,目标方法执行之前执行
- @AfterReturning:返回通知,目标方法正常执行成功返回后执行(异常不会执行)
- @AfterThrowing:异常通知,目标方法抛出异常时执行
- @After:后置通知(最终通知),目标方法执行完毕,不管正常还是异常都会执行(类似 finally)
- @Around:环绕通知,包裹目标方法,可以在方法前后都写逻辑,还能控制目标方法是否执行,功能最强。
Weaving 织入 把切面逻辑应用到目标对象,生成代理对象的过程。 Spring AOP 是运行期织入:程序运行的时候,动态生成代理对象。
AspectJ 支持编译期织入、类加载期织入。
✅ @Aspect、@Pointcut、@Before、@Around 这些注解包名是 org.aspectj.lang.annotation,来自 AspectJ,但是执行引擎是 Spring
⚠️ 重点区分:Spring AOP ≠ AspectJ
Spring AOP 使用 AspectJ 的切点语法和注解,但是动态代理实现;
AspectJ 是完整 AOP 框架,编译期修改字节码。
综合:
切面 (Aspect) 定义切入点 (Pointcut) 与通知 (Advice) ,
织入 (Weaving) 切面(Aspect)到目标 (Target)的连接点 (JoinPoint) 处,
最终生成代理 (Proxy) 对外提供调用。
AOP 核心组件关系图
┌────────────────────── Aspect【切面】 ──────────────────────────┐
│ ├──── Pointcut【切入点】:匹配规则,筛选哪些JoinPoint需要拦截 │
│ └──── Advice【通知】:匹配后要执行的增强代码(before/after等) │
└───────────────────────────────────────────────────────────────┘
↓ Weaving【织入】(把增强代码嵌入目标)
┌────────── JoinPoint【连接点】 ──────────┐
│ Target【目标对象】上可被拦截的执行点(方法) │
└───────────────────────────────────────┘
↓
Proxy【代理对象】(包装Target,对外暴露)
举例子:
Aspect (事务切面)
├ Pointcut:匹配带有 @Transactional 注解的方法
└ Advice:事务管理逻辑(开启、提交、回滚)
Target:目标的 Service
JoinPoint:被匹配到的带注解的方法
三、Spring AOP 底层两种动态代理实现
Spring 会自动选择代理方式,优先级:优先 JDK 动态代理,目标类没有实现接口时使用 CGLIB
SpringBoot 2.x 和3.x默认开启 CGLIB 代理
1. JDK 动态代理
- 要求:目标类必须实现至少一个接口
- 原理:运行时动态生成一个接口的实现类,代理实现接口方法,调用时拦截。
- 限制:只能代理接口定义的方法;目标类自己新增、不在接口里的方法无法拦截。
2. CGLIB 动态代理
- 要求:目标类不需要实现接口
- 原理:运行时生成目标类的子类,重写非 final 方法,通过子类来拦截父类方法。
- 限制:final class、final method 无法代理(不能继承、不能重写)。
常见坑:
同一个类内方法互相调用 this.xxx(),不会触发 AOP 增强!
原因:
this 是原始目标对象,不是代理对象。要自调用触发 AOP,需要拿到代理对象调用。
3. 两个代理对比
- CGLIB 代理:通过继承目标类(extends)生成代理,代理类是目标类的子类;重写非 final 方法实现拦截;不要求目标类实现接口。
- JDK 代理:通过实现接口生成代理,代理类与目标类平级;只能拦截接口中定义的方法;要求目标类必须实现至少一个接口。
CGLIB 代理:例子 + 场景
1. 示例代码
原始目标类(没有实现任何接口)
// Target,没有接口,CGLIB可以代理
public class UserService {
// 普通方法,可以被子类重写,能被AOP拦截
public void addUser() {
System.out.println("新增用户,业务逻辑");
}
// final方法:不能被子类重写!CGLIB无法拦截这个方法
public final void deleteUser() {
System.out.println("删除用户");
}
}
CGLIB 运行时动态生成的子类(伪代码,框架自动生成,我们看不到)
// CGLIB动态生成:继承UserService
public class UserService$$CGLIB extends UserService {
@Override
public void addUser() {
// 【Advice增强逻辑】前置通知:日志/事务
System.out.println("方法执行前:记录日志");
// 调用父类原始业务方法
super.addUser();
// 【Advice增强逻辑】后置通知
System.out.println("方法执行后");
}
// ❗ deleteUser 是final,子类不能重写,这里无法生成增强逻辑
// 不会生成重写的deleteUser方法
}
使用
// Spring拿到的Bean,其实是 UserService$$CGLIB 代理对象
UserService userService = context.getBean(UserService.class);
userService.addUser(); // 走代理,触发AOP增强 ✅
userService.deleteUser(); // 直接调用父类原始方法,**不会触发AOP** ❌
JDK 动态代理
前提:
目标类必须至少实现 1 个接口。
限制:
只能拦截接口中定义的方法;
目标类自己新增的、不在接口里的方法,代理对象调用不到。
1. 完整代码示例
① 先定义接口(JDK 代理离不开这个)
public interface UserService {
void addUser();
}
② 目标实现类(Target,实现上面接口)
// 目标类,实现了UserService接口
public class UserServiceImpl implements UserService {
@Override
public void addUser() {
System.out.println("【业务】新增用户");
}
// 这个方法不在接口里面!重点!
public void hello() {
System.out.println("hello,这是实现类独有的方法");
}
}
③ JDK 代理工厂,生成代理对象
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class JdkProxyFactory {
// target:原始目标对象 UserServiceImpl
public static UserService getProxy(UserService target) {
// InvocationHandler:回调,代理方法被调用时进入这里(对应AOP的Advice)
InvocationHandler handler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 前置增强(Advice)
System.out.println("【JDK代理前置】方法执行前");
// 执行原始目标方法
Object result = method.invoke(target, args);
// 后置增强(Advice)
System.out.println("【JDK代理后置】方法执行后");
return result;
}
};
// 核心:JDK Proxy.newProxyInstance
// 参数:类加载器、【接口数组】、InvocationHandler
return (UserService) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(), // ✅ 传入接口!JDK代理靠这个生成代理类
handler
);
}
}
④ 测试调用
public class Test {
public static void main(String[] args) {
UserService target = new UserServiceImpl();
// 拿到代理对象,注意:代理对象类型是 接口 UserService,不是 UserServiceImpl
UserService proxy = JdkProxyFactory.getProxy(target);
proxy.addUser(); // ✅ 在接口中,走代理,触发增强
// proxy.hello(); // ❗编译报错!
// proxy 是接口类型UserService,接口没有hello方法,调用不到实现类独有的方法
}
}
输出结果
【JDK代理前置】方法执行前
【业务】新增用户
【JDK代理后置】方法执行后
2. JDK 动态生成的代理类伪代码(重点理解)
JDK 在运行时自动生成 $Proxy0,implements 接口,不是 extends 目标类!
// JDK自动生成的代理类 $Proxy0
public class $Proxy0 implements UserService {
private InvocationHandler h;
public $Proxy0(InvocationHandler h) {
this.h = h;
}
@Override
public void addUser() {
// 调用我们写的invoke,执行增强逻辑
h.invoke(this, addUserMethod, null);
}
}
关键点:$Proxy0 implements UserService 代理类和UserServiceImpl是平级,都实现同一个接口,没有父子继承关系!
扩展:
Spring AOP 底层核心是代理模式。
两种代理实现:JDK 动态代理和CGLIB 动态代理
代理模式核心意图:控制对目标对象 (Target) 的访问,在不修改目标类源码的前提下,在方法前后增加额外逻辑(Advice,日志、事务),完美契合 AOP 的需求。
调用者 → Proxy代理对象(增强逻辑) → Target目标对象(原始业务)
但是:Spring AOP 不只用代理模式
还用到别的设计模式,例如:
简单区分
- 代理模式:重点是控制访问,代理和目标实现同一个接口 / 继承同一个类,代理持有目标对象。Spring AOP 属于代理。
- 装饰器模式:重点是动态增强对象功能,通常包装对象层层嵌套。
关于设计模式,我写过一篇博客详解:23种设计模式完整讲解(场景 + 例子 + 代码)
四、AOP切点表达式 execution(最常用)
语法:
execution(修饰符 返回值 包名.类名.方法名(参数列表))
通配符说明:
- *:匹配任意字符(包、类、方法、返回值)
- ..:包路径任意多级;方法参数任意个数任意类型
- +:匹配类及其子类
示例:
// 匹配 com.service 包下所有类的所有public方法
execution(public * com.service.*.*(..))
// 匹配 com.service包及其子包下所有类所有方法
execution(* com.service..*.*(..))
// 匹配 UserService 类下所有方法
execution(* com.service.UserService.*(..))
// 匹配 save开头的方法
execution(* com.service..*.save*(..))
除了 execution,还有其他切点指示器:
- within:限定类
- @annotation:匹配标注了指定注解的方法(最常用,自定义注解做切面)
- args:匹配方法参数类型
- @target / @this:匹配类上注解
@annotation 实战场景:自定义注解@LogRecord,方法打上注解就自动记录操作日志。
五、AOP通知执行顺序
正常执行(无异常)
@Around 前置逻辑 → @Before → 目标方法执行 → @AfterReturning → @After → @Around 后置逻辑
抛出异常
@Around 前置逻辑 → @Before → 目标方法抛出异常 → @AfterThrowing → @After
@AfterReturning 不会执行,环绕通知里如果不 catch 异常,异常继续向上抛出。
多个切面时,可以用@Order(int)指定切面优先级,数字越小优先级越高。
高优先级切面:先进先出,后进先出。
切面 A order=1,切面 B order=2:
A 前置 →
B 前置 → 目标方法 → B 返回 / 后置 →
A 返回 / 后置
对应业务场景理解
假设这个切面做接口耗时统计 + 日志记录
概括:
统计接口耗时放在 Around,
参数校验放 Before,
成功埋点 AfterReturning,
资源释放放 After。
六、简单完整代码示例
<!– spring-boot-starter-aop 自带aspectj注解支持 –>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2. 业务类(目标类)
@Service
public class UserService {
public void addUser(String name){
System.out.println("业务:新增用户 " + name);
}
}
3. 切面类(@Aspect + @Component)
@Aspect
@Component
public class LogAspect {
// 定义切点:匹配UserService所有方法
@Pointcut("execution(* com.service.UserService.*(..))")
public void pointCut(){}
// 前置通知
@Before("pointCut()")
public void before(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
System.out.println("【前置通知】方法:"+methodName + " 参数:"+ Arrays.toString(args));
}
// 返回通知
@AfterReturning(value = "pointCut()", returning = "result")
public void afterReturn(Object result){
System.out.println("【返回通知】方法正常执行,返回值:"+result);
}
// 异常通知
@AfterThrowing(value = "pointCut()", throwing = "ex")
public void afterThrow(Exception ex){
System.out.println("【异常通知】捕获异常:"+ex.getMessage());
}
// 最终通知
@After("pointCut()")
public void after(){
System.out.println("【最终通知】无论是否异常都会执行");
}
// 环绕通知,ProceedingJoinPoint只能用在Around
@Around("pointCut()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("【环绕通知】方法执行前");
// 执行目标方法
Object res = pjp.proceed();
System.out.println("【环绕通知】方法执行后");
return res;
}
}
调用 userService.addUser("张三"),输出顺序:
【环绕通知】方法执行前
【前置通知】方法:addUser 参数:[张三]
业务:新增用户 张三
【返回通知】方法正常执行,返回值:null
【最终通知】无论是否异常都会执行
【环绕通知】方法执行后
ProceedingJoinPoint 是 JoinPoint 子类,只有环绕通知可以使用,调用proceed()才会执行目标方法;
可以不调用,直接拦截,也可以多次调用。
七、完整 AOP 实战案例:接口操作日志切面
业务场景需求
后台管理系统,需要记录管理员操作日志:
要求:
业务代码完全不写日志相关代码,用 AOP 横切统一实现。
用到全部 AOP 组件:切面 Aspect、切点 Pointcut、通知 Advice、连接点 JoinPoint
步骤 1:自定义注解
用来标记需要记录操作日志的接口
/**
* 标记该方法需要记录操作日志
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OperateLog {
// 操作描述:新增用户、修改订单等
String value();
}
步骤 2:实体类
操作日志保存对象
@Data
public class OperateLogDTO {
private String username; // 当前操作人
private String operateDesc; // 操作描述
private String params; // 请求参数
private String result; // 返回结果
private Long costTime; // 耗时ms
private boolean success; // 是否成功
private String errorMsg; // 异常信息
}
步骤 3:切面类
核心!包含切点 + 5 种通知
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import com.alibaba.fastjson2.JSON;
@Aspect
@Component
public class OperateLogAspect {
// ========== 切点 Pointcut ==========
// 切点规则:拦截【加了@OperateLog注解】的方法
@Pointcut("@annotation(operateLog)")
public void logPointcut(OperateLog operateLog) {
}
// ========== 环绕通知 Around(包裹整个目标方法) ==========
@Around("logPointcut(operateLog)")
public Object around(ProceedingJoinPoint joinPoint, OperateLog operateLog) throws Throwable {
OperateLogDTO logDto = new OperateLogDTO();
long start = System.currentTimeMillis();
logDto.setOperateDesc(operateLog.value());
logDto.setUsername("admin"); // 模拟从上下文获取登录用户
logDto.setParams(JSON.toJSONString(joinPoint.getArgs()));
Object result;
try {
// proceed():执行目标方法,中间会触发@Before
result = joinPoint.proceed();
logDto.setSuccess(true);
logDto.setResult(JSON.toJSONString(result));
} catch (Throwable e) {
logDto.setSuccess(false);
logDto.setErrorMsg(e.getMessage());
throw e; // 继续向上抛异常,交给全局异常处理器
} finally {
long cost = System.currentTimeMillis() – start;
logDto.setCostTime(cost);
// 模拟保存日志到数据库
saveLog(logDto);
}
return result;
}
// ========== 前置通知 Before ==========
@Before("logPointcut(operateLog)")
public void before(JoinPoint joinPoint, OperateLog operateLog) {
System.out.println("==== @Before 执行:准备执行【" + operateLog.value() + "】操作 ====");
}
// ========== 正常返回通知 AfterReturning ==========
@AfterReturning(value = "logPointcut(operateLog)", returning = "ret")
public void afterReturning(JoinPoint joinPoint, OperateLog operateLog, Object ret) {
System.out.println("==== @AfterReturning 执行:操作【"+operateLog.value()+"】成功,返回值:" + ret);
}
// ========== 异常通知 AfterThrowing ==========
@AfterThrowing(value = "logPointcut(operateLog)", throwing = "ex")
public void afterThrowing(JoinPoint joinPoint, OperateLog operateLog, Throwable ex) {
System.out.println("==== @AfterThrowing 执行:操作【"+operateLog.value()+"】发生异常:" + ex.getMessage());
}
// ========== 最终通知 After(类似finally,无论成功失败都执行) ==========
@After("logPointcut(operateLog)")
public void after(JoinPoint joinPoint, OperateLog operateLog) {
System.out.println("==== @After 执行:操作【"+operateLog.value()+"】流程结束(finally) ====");
}
// 模拟入库
private void saveLog(OperateLogDTO dto) {
System.out.println("【保存操作日志到DB】" + JSON.toJSONString(dto));
}
}
步骤 4:目标业务 Controller(目标 Target)
@RestController
@RequestMapping("/user")
public class UserController {
// 打上注解,AOP切面会拦截这个方法
@OperateLog(value = "新增用户")
@PostMapping("/add")
public String addUser(String username, Integer age){
System.out.println("===== 目标方法:新增用户业务逻辑执行 =====");
// 模拟业务
return "用户新增成功, name:" + username;
}
// 模拟异常场景
@OperateLog(value = "删除用户")
@PostMapping("/delete")
public String deleteUser(){
System.out.println("===== 目标方法:删除用户业务,抛出异常 =====");
throw new RuntimeException("用户不存在,删除失败");
}
}
场景 1:调用 /user/add 【正常无异常】
输出顺序(对应 AOP 通知顺序)
==== @Before 执行:准备执行【新增用户】操作 ====
===== 目标方法:新增用户业务逻辑执行 =====
==== @AfterReturning 执行:操作【新增用户】成功,返回值:用户新增成功, name:zhangsan
==== @After 执行:操作【新增用户】流程结束(finally) ====
【保存操作日志到DB】{"username":"admin","operateDesc":"新增用户","params":
["zhangsan",20],"result":"用户新增成功, name:zhangsan","costTime":5,"success":true,"errorMsg":null}
完整执行链路:
@Around 前置代码 → @Before → 目标方法 → @AfterReturning → @After → Around 中 proceed 执行完毕,进入 finally 保存日志
场景 2:调用 /user/delete 【抛出异常】
==== @Before 执行:准备执行【删除用户】操作 ====
===== 目标方法:删除用户业务,抛出异常 =====
==== @AfterThrowing 执行:操作【删除用户】发生异常:用户不存在,删除失败
==== @After 执行:操作【删除用户】流程结束(finally) ====
【保存操作日志到DB】{"username":"admin","operateDesc":"删除用户","params":[],"result":null,"costTime":2,"success":false,"errorMsg":"用户不存在,删除失败"}
异常链路:
@Around 前置 → @Before → 目标抛异常 → @AfterThrowing → @After → Around 捕获异常,记录错误日志
❗️重点:异常时 AfterReturning 不会执行!
对应 AOP 核心名词对照
| Target 目标对象 | UserController |
| JoinPoint 连接点 | addUser ()、deleteUser () 这两个方法(可被增强的点位) |
| Pointcut 切点 | @annotation(OperateLog) 匹配规则,筛选哪些连接点要拦截 |
| Advice 通知 | @Before / @Around / @AfterReturning / @AfterThrowing / @After 这 5 段增强代码 |
| Aspect 切面 | OperateLogAspect 整个类(切点 + 所有通知组合在一起) |
| Weaving 织入 | Spring 启动后,运行期创建 UserController 的代理对象,把切面逻辑嵌入到目标方法的过程 |
概括
用后台操作日志这个场景理解 AOP。
需求是统一记录管理员操作,不想在每个接口写日志代码。
八、Spring AOP 常见坑
坑 1:同类方法自调用,AOP 失效
@Service
public class UserService{
public void test(){
// this是原始对象,不是代理对象!切面不会触发
this.addUser("test");
}
public void addUser(String name){}
}
解决:
坑 2:private 方法不能被 AOP 增强
Spring AOP 只能拦截public 方法,private 方法不会被代理。
坑 3:final 类 /final 方法无法被 CGLIB 代理
CGLIB 生成子类,final 不能重写,增强失效。
坑 4:事务注解 @Transactional 放在非 public 方法失效
底层就是 AOP,只能拦截 public 方法。
坑 5:多个切面顺序混淆
记住 @Order:数字越小,切面越先进入,最后退出。
Spring IOC 详细讲解
一、IOC 是什么
IOC:Inversion of Control,控制反转
是一种设计思想,不是 Spring 独有的,Spring 是 IOC 思想的实现框架。
传统开发:开发者 new 对象,主动创建、管理对象的依赖(控制权在程序员手上)
IOC:把对象创建、销毁、依赖注入的控制权交给容器,程序员不再手动 new 对象;由容器来实例化对象、装配依赖。
控制权从代码反转到容器,所以叫控制反转。
DI(Dependency Injection,依赖注入):是 IOC 思想最主流的实现方式。
IOC 是思想,DI 是手段。
容器在创建 Bean 的时候,自动把对象需要的依赖注入进去。(看这个具体的业务场景例子)
一句话理解原句:
程序员不再自己new A()、new B(),不再手动给 A 设置 B;
Spring 容器负责 new Bean,创建 A 这个 Bean 的同时,自动找到 A 需要的 B,把 B 塞给 A,这个动作就是 DI。
业务场景
电商下单场景: OrderService(下单)需要调用 PayService(支付)。 OrderService 依赖 PayService。
Step1:两个Service交给 Spring 管理,变成 Bean
@Service
public class PayService {
public boolean pay(Long orderId, BigDecimal money){
System.out.println("发起支付");
return true;
}
}
@Service
public class OrderService {
private final PayService payService;
// ✅ 构造器注入(推荐)
// 【重点】容器创建OrderService Bean的时候,会调用这个构造方法
// 容器从容器内部找到PayService Bean,自动传进来
public OrderService(PayService payService) {
this.payService = payService;
}
public void createOrder(Long orderId, BigDecimal money){
System.out.println("创建订单");
payService.pay(orderId, money);
}
}
Step2:Spring 容器内部执行流程(对应那句话)
@SpringBootApplication
public class IoCDemoApplication {
public static void main(String[] args) {
ApplicationContext ctx = SpringApplication.run(IoCDemoApplication.class, args);
// 直接从容器拿OrderService,依赖已经注入完成
OrderService orderService = ctx.getBean(OrderService.class);
orderService.createOrder(1L, new BigDecimal("99"));
}
}
输出:
创建订单
发起支付
原句翻译到本案例:
容器在创建 OrderService Bean 的时候,自动把它需要的依赖 PayService 注入进去。
总结:
IOC = 把对象生命周期和依赖交给 Spring 容器管理;
DI = 容器自动给 Bean 注入依赖属性。
关键词拆解:
二、为什么要用 IOC(解决什么问题)
反例(传统写法,紧耦合)
public class UserController {
// 硬编码new,耦合严重,替换实现要改代码
private UserService userService = new UserServiceImpl();
}
IOC 写法:
public class UserController {
// 容器自动注入,不需要new
@Autowired
private UserService userService;
}
三、IOC 容器核心组件
Spring IOC 容器顶层接口有两个:
- 方法:getBean()、containsBean()、isSingleton()等
- 在 BeanFactory 基础上扩展:
- 容器启动时预实例化单例 Bean(饿汉)
- 国际化、资源加载、事件发布、环境变量、AOP 支持
- 常用实现类:
- ClassPathXmlApplicationContext:从 classpath 读取 xml 配置
- FileSystemXmlApplicationContext:读取文件系统 xml
- AnnotationConfigApplicationContext:注解驱动(SpringBoot 默认,读取@Configuration)
区分:
BeanFactory 是简陋版 IOC;
ApplicationContext 是增强版 BeanFactory。
四、Bean:IOC 容器管理的对象
容器中管理的所有对象叫做 Bean。
Bean 的定义信息存在 BeanDefinition(Bean 定义):保存 Bean 的 class 类型、作用域、懒加载、初始化方法、销毁方法、依赖信息等。
BeanDefinition ≠ Bean
实例 BeanDefinition 是 Bean 的元数据描述;
容器根据 BeanDefinition 去实例化 Bean 对象。
Bean 的作用域 scope
⚠️ 单例 Bean 不是线程安全!多线程并发修改成员变量会有并发问题。
扩展:
Q:@Autowired private UserService userService; 这个 UserService 是全局唯一的吗?
A:
是,因为Spring Bean默认是全局唯一,因为 Spring Bean 默认是 singleton(单例)。
@Service 注解的 Bean 默认 @Scope("singleton")
- Spring 容器启动时,只实例化这一个 UserServiceImpl 对象
- 所有地方@Autowired UserService注入的,拿到同一个对象引用。
Q:这样的好处是?
A:
1. 减少对象创建、销毁开销,提升性能
容器启动阶段一次性 new 一次 UserServiceImpl; Controller、其他 Service 多处@Autowired注入时,不再重复 new 对象,直接返回同一个对象引用。
如果是 prototype,每次注入都 new,频繁创建对象、触发 GC,增加 CPU 与 GC 压力。
2. 节省堆内存
整个容器只保留一份 UserServiceImpl 实例。
项目里大量 Service、Controller、Dao 都复用这一个对象,减少堆中对象数量。
3. 一次性完成 Bean 所有后置处理(AOP 织入、依赖注入、初始化)
只做一次: 实例化 → 填充依赖(@Autowired) → 初始化回调 → AOP 代理织入(生成 Proxy)
重点!事务切面@Transactional的代理只生成一次。
如果是 prototype,每次获取 Bean 都要重新做一遍代理、初始化,开销很大。
4. 容器缓存,获取 Bean 速度快
实例创建完成后放入容器单例池(singletonObjects Map),后续 getBean / 注入直接从 Map 拿引用,不用反射创建对象。
5. 适合无状态业务对象(Service 天然满足)
UserServiceImpl 里面一般不存放可变成员变量,只有业务方法;方法内局部变量在栈上,多线程互不干扰,天然无状态,可以安全复用同一个实例。
概括:
@Service 默认 singleton,容器启动仅实例化一次,所有注入复用同一个对象。
减少对象创建 GC 开销、节省堆内存。
依赖注入、AOP 代理、初始化只执行一次,提升性能。
Service 本身无状态,适合多线程复用。
(关于GC开销和节省堆内存,我写过一篇博客详细讲解了这个过程:一文讲懂JVM与调优)
Q:既然复用同一个对象,那多线程并发访问 UserServiceImpl 会不会线程不安全?
A:不会,前提是不定义可变成员变量。
Spring MVC 每个请求是独立线程,调用 service 方法时,方法内的局部变量存放在线程栈,线程隔离;只要不在 Service 里写可修改的成员字段,就没有并发问题。
Q:什么时候不能用 singleton,要改成 prototype?
A:当 Bean 需要保存可变的成员状态,每个调用者必须拥有独立对象,比如有状态的业务模型对象。
五、依赖注入 DI 的 3 种方式
1. 构造器注入(Spring 官方推荐)
容器调用类的构造方法,把依赖传入。
@Service
public class UserController {
private final UserService userService;
// 构造注入,Spring4.3+单构造器可省略@Autowired
public UserController(UserService userService) {
this.userService = userService;
}
}
✅ 优点:
- 依赖不可变(final 修饰)
- 保证 Bean 创建完成时依赖已经就绪,不会出现空指针
- 强制依赖必须存在,容器启动失败快速暴露缺失依赖
- 避免循环依赖(部分场景)
2. Setter 方法注入
容器调用 set 方法注入属性。
@Service
public class UserController {
private UserService userService;
@Autowired
public void setUserService(UserService userService) {
this.userService = userService;
}
}
✅ 适合:可选依赖,可在 Bean 创建后修改依赖。
3. 字段注入(@Autowired 直接打在属性上)
@Service
public class UserController {
@Autowired
private UserService userService;
}
❌ 缺点:
- 无法 final 修饰,依赖可变
- 无法脱离容器做单元测试(new 对象时无法注入)
- 隐藏依赖,代码可读性差
Spring 官方不推荐字段注入,但是业务代码写的最多。
注入注解:@Autowired、@Resource区别
这个我专门写了一篇博客详解,非常详细:一文看懂@Autowired 和 @Resource区别(附代码)
六、Bean 完整生命周期
以单例、非懒加载 Bean为例:
- @PostConstruct 注解方法(JSR 规范)
- InitializingBean#afterPropertiesSet() 接口
- xml 指定 init-method 自定义初始化方法
✨ AOP 代理对象就是在这里生成。
如果需要 AOP 增强,返回代理对象,否则返回原对象。
- @PreDestroy
- DisposableBean#destroy()
- xml 指定 destroy-method
总结:实例化 → 填充属性 → Aware → 前置处理器 → 初始化 → 后置处理器 (AOP) → 使用 → 销毁
七、循环依赖(IOC 经典难点)
什么是循环依赖
A 里面依赖 B,B 里面依赖 A。
@Component
public class A {
@Autowired
private B b;
}
@Component
public class B {
@Autowired
private A a;
}
Spring 能解决哪种循环依赖?
✅ 单例 Bean + setter / 字段注入:可以解决
❌ 构造器注入的循环依赖:无法解决,直接抛异常
❌ prototype 原型 Bean:完全无法解决
三级缓存原理(面试必问)
Spring 单例池三级缓存:
解决流程简述:
- 若 A 需要 AOP 代理:在这里直接生成 A 的代理对象;不需要代理则返回原始实例。
- 将拿到的对象(代理 / 原始)存入二级缓存 earlySingletonObjects,同时删除三级缓存里 A 的 ObjectFactory。
重点:A 初始化阶段,AOP 后置处理器会判断该 Bean 已经提前生成过代理(earlyProxyReferences 标记),不再重复创建代理。
关键点:提前暴露半成品 Bean 的引用。
如果是构造注入:实例化和属性注入在构造方法阶段,对象还没 new 出来,无法提前暴露引用,所以无法解决。
很多人误区:三级缓存就是为了解循环依赖。
不完全对,三级缓存主要是为了循环依赖下同时支持 AOP 代理。
如果没有 AOP,二级缓存就足够。
八、Bean 的配置方式
@Configuration
public class MyConfig {
@Bean
public UserService userService(){
return new UserServiceImpl();
}
}
九、IOC 容器启动大致流程(ApplicationContext)
十、IOC 高频面试题汇总
IOC 是控制反转思想;
DI 依赖注入是 IOC 的实现方式。
BeanFactory 懒加载,基础 Bean 获取;
ApplicationContext 是子接口,启动预实例单例 Bean,扩展事件、国际化等能力。
默认容器刷新时提前实例化;加上@Lazy懒加载,第一次 getBean 才创建。
构造注入官方推荐,保证依赖必填;setter 适合可选依赖;字段注入可读性差,不推荐。
Bean 后置处理器,在 Bean 初始化前后拦截;AOP 代理生成就在postProcessAfterInitialization。
@Component 打在类上,组件扫描自动创建 Bean;
@Bean 打在方法上,手动实例化对象放到容器,适合引入第三方类。
IOC 和 AOP 的关系总结
IOC 是 Spring 的根基,AOP 建立在 IOC 之上
简单理解:IOC 负责造对象,AOP 负责给对象套代理增强。
网硕互联帮助中心





评论前必须登录!
注册