云计算百科
云计算领域专业知识百科平台

一文看懂Spring之AOP与IOC(代码+场景,通俗易懂)

Spring AOP 详细讲解

一、AOP 是什么

AOP:Aspect Oriented Programming,面向切面编程。

AOP 的核心原理就是动态生成代理对象,在不改动原业务代码的情况下,通过代理去拦截目标方法,在方法前后插入切面逻辑。

核心思想:业务代码只关心业务,公共逻辑交给切面。

典型通用场景:

日志记录、性能统计、权限校验、事务控制、异常处理、埋点。

例子:Spring 的 @Transactional 底层就是 AOP。

二、AOP 核心术语

  • JoinPoint 连接点 程序执行过程中可以被切面切入的点。

    Spring AOP 只支持方法级别的连接点(只能拦截方法执行,不能拦截字段、构造函数)。

    简单说:Spring 里,所有目标对象的方法都是连接点。

  • Pointcut 切点 匹配规则,用来筛选哪些连接点需要被切面增强。

    通过表达式匹配方法,只有匹配上的方法才会执行通知。

    JoinPoint 是候选点位,Pointcut 是挑选规则。

  • Advice 通知 切面里要执行的增强逻辑,也就是在目标方法的什么时候执行代码。Spring 有 5 种通知类型:
    • @Before:前置通知,目标方法执行之前执行
    • @AfterReturning:返回通知,目标方法正常执行成功返回后执行(异常不会执行)
    • @AfterThrowing:异常通知,目标方法抛出异常时执行
    • @After:后置通知(最终通知),目标方法执行完毕,不管正常还是异常都会执行(类似 finally)
    • @Around:环绕通知,包裹目标方法,可以在方法前后都写逻辑,还能控制目标方法是否执行,功能最强。
  • Aspect 切面 切面 = 切点 (Pointcut) + 通知 (Advice),一个类,里面定义切点表达式和通知方法。用@Aspect标注。
  • Target 目标对象 被 AOP 增强的原始对象,没有被代理之前的对象。
  • Proxy 代理对象 Spring AOP 底层通过动态代理生成的对象,交给容器使用的是代理对象,调用方法时先走代理逻辑。
  • 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 不只用代理模式

    还用到别的设计模式,例如:

  • 工厂模式:ProxyFactoryBean / DefaultAopProxyFactory,根据条件判断创建 JDK 代理还是 CGLIB 代理对象。
  • 装饰器模式(容易和代理混淆)
  • 简单区分

    • 代理模式:重点是控制访问,代理和目标实现同一个接口 / 继承同一个类,代理持有目标对象。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 前置:记录开始时间(startTime = System.currentTimeMillis())
  • @Before:校验请求参数、打印入参(业务执行前校验)
  • 目标方法:执行业务逻辑,创建订单
  • @AfterReturning:拿到返回订单号,记录业务成功日志、埋点上报成功事件
  • @After:资源清理(关闭一些临时资源,不管成功失败都清理)
  • @Around 后置:计算总耗时,打印接口耗时
  • 概括:

            统计接口耗时放在 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 核心名词对照

    AOP 名词在本案例对应的东西
    Target 目标对象 UserController
    JoinPoint 连接点 addUser ()、deleteUser () 这两个方法(可被增强的点位)
    Pointcut 切点 @annotation(OperateLog) 匹配规则,筛选哪些连接点要拦截
    Advice 通知 @Before / @Around / @AfterReturning / @AfterThrowing / @After 这 5 段增强代码
    Aspect 切面 OperateLogAspect 整个类(切点 + 所有通知组合在一起)
    Weaving 织入 Spring 启动后,运行期创建 UserController 的代理对象,把切面逻辑嵌入到目标方法的过程

    概括

    用后台操作日志这个场景理解 AOP。

    需求是统一记录管理员操作,不想在每个接口写日志代码。

  • 自定义@OperateLog注解标记需要记录日志的接口;
  • 写切面类,切点使用@annotation匹配该注解;
  • 通过多种通知实现:Before 打印准备信息,Around 统计耗时、捕获异常;AfterReturning 处理成功场景;AfterThrowing 捕获异常;After 做收尾。
  • Spring 运行期动态代理把切面织入目标 Controller 方法;业务代码只写业务,日志横切逻辑完全抽离,解耦。

  • 八、Spring AOP 常见坑

    坑 1:同类方法自调用,AOP 失效

    @Service
    public class UserService{
    public void test(){
    // this是原始对象,不是代理对象!切面不会触发
    this.addUser("test");
    }
    public void addUser(String name){}
    }

    解决:

  • 自己注入自己 @Autowired UserService self;,用self.addUser()
  • 通过 AopContext 获取代理对象 ((UserService) AopContext.currentProxy()).addUser(),需要开启 @EnableAspectJAutoProxy(exposeProxy = true)
  • 坑 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 容器内部执行流程(对应那句话)

  • Spring 启动,扫描@Service,识别PayService、OrderService两个类,准备创建 Bean。
  • 先实例化PayService,存入容器的 Bean 池。
  • 开始创建 OrderService 这个 Bean: 容器发现OrderService构造函数需要PayService类型参数; 容器去 Bean 池中查找,拿到已经建好的PayService实例; 自动传入构造器,完成依赖注入。
  • 完成 Bean 创建。业务代码里没有任何 new,没有手动赋值。
  • @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 注入依赖属性。

    关键词拆解:

  • Bean:交给 Spring 容器管理的对象(加@Service/@Component/@Controller等)
  • 依赖:A 类里面要用到 B 类,B 就是 A 的依赖
  • 依赖注入 DI:不是 A 自己 new B,而是容器把 B 送给 A
  • 时机:创建 Bean 的时候(实例化 Bean 阶段) 就完成注入
  • 二、为什么要用 IOC(解决什么问题)

  • 解耦:不用硬编码 new XXXService(),类之间不直接依赖具体实现,依赖抽象,方便替换实现类
  • 统一管理对象生命周期:创建、初始化、销毁由容器接管
  • 方便依赖注入,不用手动组装对象
  • 便于 AOP、事务等功能扩展(Spring AOP 底层依赖 IOC 容器,生成代理 Bean)
  • 方便单元测试,可以轻松 mock 依赖
  • 反例(传统写法,紧耦合)

    public class UserController {
    // 硬编码new,耦合严重,替换实现要改代码
    private UserService userService = new UserServiceImpl();
    }

    IOC 写法:

    public class UserController {
    // 容器自动注入,不需要new
    @Autowired
    private UserService userService;
    }

    三、IOC 容器核心组件

    Spring IOC 容器顶层接口有两个:

  • BeanFactory:最底层容器接口,IOC 容器的根接口。提供基础 Bean 获取能力,懒加载(getBean 的时候才实例化 Bean),功能简单。
    • 方法:getBean()、containsBean()、isSingleton()等
  • ApplicationContext:BeanFactory 的子接口,应用上下文,日常开发几乎都用它。
    • 在 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

  • singleton(默认):单例。整个容器中只创建 1 个实例,容器启动时(非懒加载)就创建,存入单例池(Map<String,Object> singletonObjects)。
  • ⚠️ 单例 Bean 不是线程安全!多线程并发修改成员变量会有并发问题。

  • prototype:原型。每次getBean()都会新建一个对象,容器只负责创建,不管理销毁。
  • request:web 环境,一次 http 请求一个实例
  • session:web 环境,一次 session 一个实例
  • application:web 环境,ServletContext 级别单例
  • 扩展:

    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:Spring 注解,默认按类型 byType,找不到再按名字 byName;可以配合@Qualifier指定 Bean 名称;required=true 默认,找不到 Bean 抛异常。
  • @Resource:Java 标准 JSR250,默认按名称 byName,名字找不到再 byType。
  • 这个我专门写了一篇博客详解,非常详细:一文看懂@Autowired 和 @Resource区别(附代码)

    六、Bean 完整生命周期

    以单例、非懒加载 Bean为例:

  • 实例化:容器根据 BeanDefinition,通过反射 new 对象(只是堆里创建空对象,属性还没赋值)
  • 属性填充(populateBean):进行依赖注入,给对象属性赋值(@Autowired 注入在这里)
  • Aware 接口回调:如果 Bean 实现了BeanNameAware、BeanFactoryAware、ApplicationContextAware,回调方法,让 Bean 拿到容器信息
  • BeanPostProcessor 前置处理:初始化前置后置处理器,postProcessBeforeInitialization
  • 初始化 Initialization
    • @PostConstruct 注解方法(JSR 规范)
    • InitializingBean#afterPropertiesSet() 接口
    • xml 指定 init-method 自定义初始化方法
  • BeanPostProcessor 后置处理:postProcessAfterInitialization

    ✨ AOP 代理对象就是在这里生成。

    如果需要 AOP 增强,返回代理对象,否则返回原对象。

  • Bean 完成:Bean 就绪,可以使用,放入单例池
  • 容器关闭,销毁
    • @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 单例池三级缓存:

  • singletonObjects:一级缓存,完整就绪 Bean(实例化 + 属性填充 + 初始化完成)
  • earlySingletonObjects:二级缓存,提前暴露的半成品 Bean(实例化完成,但还没填充属性、初始化;可能是代理对象)
  • singletonFactories:三级缓存,存放 Bean 工厂对象 ObjectFactory。只有发生循环依赖时,才会调用 ObjectFactory#getObject (),执行 getEarlyBeanReference,按需生成原始 Bean 或者 AOP 代理对象,用于提前暴露引用。
  • 解决流程简述:

  • 创建 A,实例化 A,往三级缓存 singletonFactories 存入 A 对应的 ObjectFactory;开始填充属性,发现需要依赖 B。
  • 创建 B,实例化 B,放入三级缓存;填充属性时发现需要依赖 A。
  • B 去缓存查找 A:一级没有,二级没有,取出三级缓存中 A 的 ObjectFactory,调用 getObject ()。
    • 若 A 需要 AOP 代理:在这里直接生成 A 的代理对象;不需要代理则返回原始实例。
    • 将拿到的对象(代理 / 原始)存入二级缓存 earlySingletonObjects,同时删除三级缓存里 A 的 ObjectFactory。
  • B 拿到二级缓存中 A 的引用,B 完成属性填充、初始化,B 放入一级缓存。
  • A 拿到 B 的引用,A 继续完成属性填充、初始化。

    重点:A 初始化阶段,AOP 后置处理器会判断该 Bean 已经提前生成过代理(earlyProxyReferences 标记),不再重复创建代理。

  • A 初始化完成,把二级缓存里的 A 对象(代理 / 原始)移入一级缓存,清除二级缓存中 A 的记录。
  • 关键点:提前暴露半成品 Bean 的引用。

    如果是构造注入:实例化和属性注入在构造方法阶段,对象还没 new 出来,无法提前暴露引用,所以无法解决。

    很多人误区:三级缓存就是为了解循环依赖。

    不完全对,三级缓存主要是为了循环依赖下同时支持 AOP 代理。

    如果没有 AOP,二级缓存就足够。

    八、Bean 的配置方式

  • 注解方式:@Component / @Service / @Controller / @Repository,组件扫描@ComponentScan
  • JavaConfig(SpringBoot 主流):@Configuration + @Bean
  • @Configuration
    public class MyConfig {
    @Bean
    public UserService userService(){
    return new UserServiceImpl();
    }
    }

    九、IOC 容器启动大致流程(ApplicationContext)

  • 加载配置(注解 / XML),扫描所有类
  • 解析得到所有 BeanDefinition,存入 BeanDefinitionMap
  • 遍历 BeanDefinition,实例化所有非懒加载单例 Bean(触发 Bean 生命周期)
  • 实例化过程中自动做依赖注入,处理循环依赖
  • 全部 Bean 创建完成,容器启动完毕,发布容器刷新完成事件
  • 十、IOC 高频面试题汇总

  • IOC 和 DI 的关系?
  • IOC 是控制反转思想;

    DI 依赖注入是 IOC 的实现方式。

  • BeanFactory 和 ApplicationContext 区别?
  • BeanFactory 懒加载,基础 Bean 获取;

    ApplicationContext 是子接口,启动预实例单例 Bean,扩展事件、国际化等能力。

  • singleton 单例 Bean 什么时候创建?
  • 默认容器刷新时提前实例化;加上@Lazy懒加载,第一次 getBean 才创建。

  • 三种注入方式优缺点?推荐哪个?
  • 构造注入官方推荐,保证依赖必填;setter 适合可选依赖;字段注入可读性差,不推荐。

  • 循环依赖 Spring 怎么解决?三级缓存分别作用?哪些场景无法解决循环依赖?
  • BeanPostProcessor 作用?
  • Bean 后置处理器,在 Bean 初始化前后拦截;AOP 代理生成就在postProcessAfterInitialization。

  • @Component 和 @Bean 区别?
  • @Component 打在类上,组件扫描自动创建 Bean;

    @Bean 打在方法上,手动实例化对象放到容器,适合引入第三方类。


    IOC 和 AOP 的关系总结

    IOC 是 Spring 的根基,AOP 建立在 IOC 之上

  • IOC 容器负责创建所有 Bean,管理 Bean 生命周期;
  • AOP 的代理对象,是在 Bean 生命周期的BeanPostProcessor后置处理阶段生成;
  • 如果没有 IOC,Spring 无法自动管理目标 Bean 和切面 Bean,AOP 无法自动生效。
  • 简单理解:IOC 负责造对象,AOP 负责给对象套代理增强。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 一文看懂Spring之AOP与IOC(代码+场景,通俗易懂)
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!