内容

Spring IOC

  • 谈谈你对 Spring IOC(控制反转)和 DI(依赖注入)的理解。

    • IOC(控制反转): 是一种设计思想,实际上就是将程序主动创建、管理对象的控制权交给 Spring 容器统一管理。这样一来就不用关注对象创建管理流程,实现了对象之间的解耦。
    • DI(依赖注入): 是实现 IOC 的一种方式。它是由容器在运行时动态地将依赖关系注入到组件之中。Spring 主要通过构建函数注入Setter 方法注入字段注入方式实现。

  • Spring 的 IOC 容器是如何工作的?

    • IOC 容器工作流程/Bean 对象生命周期:
      1. 资源定位(BeanDefinition 定位): 容器找到配置文件(application.xml)或注解(@Configration),准备进行解析;
      2. 加载解析(BeanDefinition 载入和解析): 将配置信息解析成 BeanDefinition 对象,这个对象定义了 Bean 的所有元数据 (类名、作用域、属性值、初始化方法等)。BeanDefinitionIOC 容器管理对象的基础。
      3. 注册(BeanDefinition 的注册): 解析得到的 BeanDefinition 会统一注册到 BeanDefinitionRegistry 中,此时 Bean 还没有被实例化。
      4. Bean 的实例化 : 容器通过反射调用 Bean 的构造函数来创建 Bean 的实例。
      5. 属性填充 : 容器将 Bean 定义的属性值(包括其他对 Bean 的引用)注入到实例中
      6. 初始化 :
        • 如果 Bean 实现了 BeanNameAwareAware 接口,则调用回调方法。
        • 执行 Bean 后置处理器(BeanPostProfessor)的 postProcessBeforeInitialization 方法
        • 调用 Bean 指定的初始化方法(如 init-method@PostConstruct 注解的方法)
        • 执行 Bean 后置处理器的 postProcessAfterInitialization 方法(这里可能发生 AOP 代理!)。
      7. 使用 :
      8. 销毁 : 当容器关闭时,如果 Bean 实现了 DisposableBean 接口或指定了销毁方法(destroy-method@PreDestroy),则会调用相应的方法。

  • BeanFactoryApplicationContext 有什么区别?

    • BeanFactory vs ApplicationContext :
      • BeanFactory(基础接口): 提供了 IOC 容器最基础的功能。它采用懒加载,只有在第一次通过 getBean() 请求时才会初始化 Bean
      • ApplicationContext(高级接口,继承自 BeanFactory): 是 BeanFactory 的超集,提供更多功能
        1. 消息资源处理(i8n)
        2. 事件发布(ApplicationContext)
        3. 统一的资源文件访问方式(ResourceLoader)
        4. 载入多个(有继承联系)的上下文
        5. 默认立即初始化单例 Bean,这样就可以在启动时立即发现配置错误,而非运行时
          一般都用后者

Spring AOP

  • Spring AOP 的实现原理是什么?
    • 实现原理 : 基于动态代理实现
      • 如果一个类实现了接口,Spring 默认使用 JDK 动态代理来创建代理对象
      • 如果没有实现任何接口,Spring 会使用 CGLIB 动态代理来创建代理对象
  • 它和 AspectJ 有什么区别?

    | 特性 | Spring AOP | AspectJ |
    | —————— | ——————————————————————— | ———————————————————————— |
    | 实现方式 | 运行时动态代理 | 编译时或类加载时织入 |
    | 性能 | 运行时稍有性能开销 | 运行时无额外性能开销 |
    | 能力 | 仅支持方法级别的切面 | 功能更强大,支持字段、构造器、方法等更多连接点 |
    | 依赖 | 轻量,仅依赖于 Spring 容器 | 更重,需要额外的编译器和织入器 |
    | 适用场景 | 满足大多数企业应用需求,解决声明式事务、日志等 | 需要更复杂的切面功能(修改字段值)的性能敏感场景 |

  • JDK 动态代理和 CGLIB 动态代理有什么区别?

    | 特性 | JDK | CGLIB |
    | ———— | ———————————————————————— | ————————————————————————— |
    | 机制 | 基于接口,通过反射实现了一个相同接口的代理类 | 基于继承,通过字节码技术生成目标类的子类作为代理类 |
    | 限制 | 目标类必须实现至少一个接口 | 目标类不能是 final,方法不能是 final/static |
    | 性能 | 在 JDK1.8+之后,生成代理对象和调用速度有显著提升 | 生成代理对象较慢,但调用速度通常更快 |

  • Spring 如何选择?

    1. 默认策略:Spring 如果目标对象实现了接口,则使用 JDK 代理;否则使用 CGLIB。SpringBoot 默认使用 CGLIB。
    2. 可以通过配置 spring.aop.proxy-target-class=true 来强制使用 CGLIB 代理,即使有接口也会生成一个基于子类的代理。这在需要代理类而不是接口,或希望代理没有在接口中声明的方法时很有用。

Spring 循环依赖

  • 什么是循环依赖?

    • 循环依赖是指两个或多个 Bean 相互支持对方的引用,形成一个闭环
  • Spring 是如何解决循环依赖的?

    • 通过三级缓存来提前暴露未完全初始化的 Bean 实例,从而解决循环依赖

      1. 一级缓存:存放已经完全初始化好的单例 Bean
      2. 二级缓存:存放提前暴露的早期 Bean,还没有进行属性填充和初始化
      3. 三级缓存:存放 Bean 的工厂对象
    • 解决流程

      1. 创建 A,实例化 A,然后将 A 的工厂对象放入三级缓存
      2. 准备为 A 进行属性填充,发现需要注入 B
      3. 开始创建 B,实例化 B,然后将 B 的工厂对象放入三级缓存
      4. 准备为 B 进行属性填充,发现需要注入 A
      5. 从缓存中寻找 A依次检查三级缓存 1 -> 2 -> 3
      6. 在三级缓存中找到了 A 的工厂方法,调用 getObject() 方法。该方法执行可能存在的后置处理器,可能会返回一个早期对象。将这个早期对象放入二级缓存,并从三级缓存移除
      7. B 成功获取到 A 的早期应用,完成属性注入和初始化,成为完整 Bean,放入一级缓存。清理二三级缓存中 B 的信息
      8. A 紧接着注入初始化好的 B,继续 A 的初始化流程。。。
  • 能解决所有情况的循环依赖吗?

    • 不能解决所有循环依赖
    • 只能解决 Setter 和字段注入,即属性注入发生在实例化之后
    • 不能解决构造器注入,因为 JVM 要求在构造方法中必须完成所有依赖的初始化,而 Spring 在实例化 Bean(调用构造器)时就需要所有依赖项,此时 Bean 还完全不可用,无法提前暴露引用,导致死锁。

Spring 事务

  • Spring 事务的传播机制有哪些?

    • 传播机制(Propagation): 定义了当一个事务方法被另一个事务方法调用时,事务应该如何传播。
      • REQUIRED(默认): 如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
      • REQUIRES_NEW: 创建一个新的事务,如果当前存在事务,则把当前事务挂起。两个事务互不干扰。
      • SUPPORTS: 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
      • NOT_SUPPORTED: 以非事务方式运行,如果当前存在事务,则把当前事务挂起。
      • MANDATORY: 必须在一个已有的事务中运行,否则抛出异常。
      • NEVER: 必须不在事务中运行,否则抛出异常。
      • NESTED: 如果当前存在事务,则在嵌套事务内执行;如果当前没有事务,则行为同 REQUIRED。嵌套事务可以独立于外部事务进行提交或回滚(Savepoint 机制)。
  • 隔离级别呢?

    • 隔离级别(Isolation): 与数据库隔离级别概念一致,Spring 只是做了一个抽象和传递。
      • DEFAULT:使用底层数据库的默认隔离级别。
      • READ_UNCOMMITTED:读未提交。
      • READ_COMMITTED:读已提交(最常用)。
      • REPEATABLE_READ:可重复读。
      • SERIALIZABLE:串行化。
  • @Transactional 注解在什么情况下会失效?

    • @Transactional 失效的常见场景:
      1. 方法非 public @Transactional 只能用于 public 方法上,否则事务不生效。
      2. 自调用(同一个类中): 在一个类中,一个非事务方法 A 调用同一个类里的事务方法 B,事务会失效。因为事务是基于 AOP 代理的,自调用不会经过代理对象。
      3. 异常被捕获(catch): 默认只在抛出运行时异常(RuntimeException) 和 Error 时回滚。如果抛出了受检异常(Exception)或者异常被方法内部 catch 处理了,事务不会回滚。可以通过 @Transactional(rollbackFor = Exception.class) 来指定回滚的异常类型。
      4. 数据库引擎不支持: 例如使用 MyISAM 引擎,它本身就不支持事务。
      5. 未开启事务管理@EnableTransactionManagement): 在配置类上忘记添加此注解。