java面试-spring篇

一、bean的生命周期

1.创建

ApplicationContext ctx=new AnnotionConfigApplicationContext(App.class);

2.实例化

Object rawBean=clazz.getConstructor().newInstance();//反射创建

3.依赖注入(属性赋值)

populateBean(beanName,mbd,instanceWrapper);//属性赋值

4.初始化

initializeBean(beanName,exposedObject,mbd);//初始化

5.生成动态代理

return applyBeanPostProcessorAfterInitialization(wrappedBean,beanName);//aop封装

6.放入单例池后续使用

singletonObjects.put(beanName,proxyBean);//放入单例池

7.销毁

destoryBean(beanName,bean);//容器关闭,资源销毁

二、如何解决Spring多线程事务失效的问题?

当使用事务注解,底层就会给你创建一个事务的动态代理开启事务,就会把你开启事务的信息放到Theadlocal 当你调用后面一个方法,后面方法动态台历通过Theadlocal.get()方法获取外层事务信息。你new一个新线程调用那样子就拿不到外层线程的事务信息。通过下面的代码的样例信息就可以把外层事务传递下去(可以学习下Spring源码里面的事务模块)

代码样例:

复制代码
@Service
public  class   TranscationalDemoServiceImpl(

  @Autowired
  private JdbcTemplete jdbcTemplete;
  
  @Autowired
  private DataSource datasource;
  
  @Transcational
  public void testA() throws Exception{
  
       ConnectionHolder cnt=(ConnectionHolder)
       TranscationalSynchronizationManager.getResource(datasource);
       
       jdbcTemplete.execute(" insert .......");
       
       TranscationalDemoService transcationalDemoService=(TranscationalDemoService)
                  AopContext.currentProxy();
                  
       Thread thread=new Thread(()->{
       
         TranscationalSynchronizationManager.bindResource(datasource,cnt);
         transcationalDemoService.testA();
         
       });
       thread.start;
       thread.join();
  }
  
  @Transcational
  public void testA(){
      //执行更新
  }

)

三、Spring Bean的循环依赖是怎么解决的?二级缓存不行吗,为什么用三级缓存?

Spring Bean 的循环依赖解决原理

Spring 使用三级缓存 来解决单例 Bean 之间的setter 注入(或字段注入) 造成的循环依赖。对于构造器注入的循环依赖,Spring 无法解决,会直接抛出异常。

三级缓存结构

Spring 在 DefaultSingletonBeanRegistry 中维护了三个 Map:

|------|-------------------------|------------------------------------------------------|
| 缓存名称 | 变量名 | 作用 |
| 一级缓存 | singletonObjects | 存放完全初始化完成的单例 Bean(成品) |
| 二级缓存 | earlySingletonObjects | 存放提前暴露的、尚未填充属性&初始化的 Bean 实例(半成品),用于解决循环依赖 |
| 三级缓存 | singletonFactories | 存放 ObjectFactory (对象工厂),用于生成 Bean 的早期引用(可生成代理对象) |

解决循环依赖的核心流程(以 A→B→A 为例)

假设有两个 Bean:A 依赖 B,B 依赖 A(均通过 setter 注入)。

1. 开始创建 A
    • getBean(A) → 实例化 A(调用构造器,得到原始对象)
    • 将 A 的 ObjectFactory 放入三级缓存 singletonFactories
    • 开始为 A 填充属性,发现需要 B
2. 尝试获取 B
    • getBean(B) → 实例化 B
    • 将 B 的 ObjectFactory 放入三级缓存
    • 为 B 填充属性,发现需要 A
3. 获取 A 的早期引用
    • getBean(A) 此时从缓存中查找:一级没有,二级没有,但三级有
    • 从三级缓存拿到 A 的 ObjectFactory**,调用** getObject()
    • 将得到的对象(可能是原始对象,也可能是 AOP 代理对象)放入二级缓存 earlySingletonObjects
    • 同时移除三级缓存中的 A 的工厂
    • 将这个早期引用注入给 B
4. B 完成创建
    • B 的属性填充完毕,继续执行初始化方法等
    • B 创建完成后,放入一级缓存 singletonObjects
5. 回到 A 的创建
    • 现在 A 能拿到 B 的完整实例(因为 B 已在一级缓存)
    • A 继续完成属性填充、初始化
    • A 创建完成后,也放入一级缓存,并移除二级/三级缓存中的 A

为什么二级缓存不行?------ 代理对象的诞生时机问题

如果只有二级缓存singletonObjectsearlySingletonObjects),对于普通 Java 对象,上述流程完全可行:

  • 实例化 A → 放入二级缓存(半成品)→ B 从二级缓存拿到 A 引用 → B 完成 → A 完成。

但是 ,当 Bean 需要被 AOP 代理(例如 @Transactional@Async 等)时,代理对象不是在实例化阶段生成的,而是在初始化之后 (通过 BeanPostProcessor 的后置处理,如 AbstractAutoProxyCreator)。

如果只有二级缓存,B 拿到的 A 是实例化后的原始对象 ,而 A 最终会变成一个代理对象。这就导致 B 中持有的 A 引用是原始对象,而容器中实际管理的是代理对象,两者不一致,AOP 功能(如事务增强)将失效。

三级缓存如何解决代理问题?
  • 三级缓存中存放的不是原始对象,而是ObjectFactory
  • 当调用 getObject() 时,可以提前执行 BeanPostProcessorgetEarlyBeanReference() 方法。
  • 该方法的典型实现(如 AbstractAutoProxyCreator)可以在这个早期阶段就生成代理对象
  • 因此 B 拿到的 A 引用,已经是最终的代理对象(如果 A 确实需要被代理的话)。
  • 这样既保证了循环依赖得以解决,又保证了 AOP 语义正确。
伪代码演示
复制代码
// 三级缓存中的 ObjectFactory
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));

// 当需要提前暴露时
Object earlySingleton = getSingleton(beanName, true);
// 内部会调用 singletonFactories.get(beanName).getObject()
// getEarlyBeanReference 可能生成代理

如果只有二级缓存,Spring 无法在"提前暴露"这个时间点知道 Bean 是否需要代理,也无法执行 getEarlyBeanReference 逻辑,因此必须借助三级缓存中的工厂来延迟决定最终暴露的对象。


|---------|-------------------|----------------------|
| 缓存级别 | 存储内容 | 作用 |
| 一级(成品) | 完全初始化好的 Bean | 常规获取 |
| 二级(半成品) | 早期暴露的 Bean(可能是代理) | 解决循环依赖的直接引用 |
| 三级(工厂) | ObjectFactory | 在需要时生成早期引用,支持 AOP 代理 |

  • 二级缓存能解决普通对象的循环依赖,但无法处理 AOP 等需要动态代理的场景。
  • 三级缓存通过工厂模式,将"何时生成代理"的决定权推迟到真正需要早期引用的那一刻,从而保证循环依赖注入的始终是最终正确的对象。
  • 这是 Spring 设计中的经典细节:用一层间接层(工厂)换来了更高的灵活性

四、spring 为什么推荐使用构造函数进行依赖注入?

1、保证依赖的不可变性(Immutability)

通过构造函数注入的依赖可以声明为 final 字段,确保对象创建后依赖关系不可变。这带来了线程安全性(对象一旦发布就不会被修改)和清晰的不可变设计。

复制代码
@Service
public class OrderService {
    private final UserRepository userRepository;
    private final EmailSender emailSender;

    // 构造函数注入
    public OrderService(UserRepository userRepository, EmailSender emailSender) {
        this.userRepository = userRepository;
        this.emailSender = emailSender;
    }
}

而字段注入(@Autowired 直接写在字段上)无法使用 final,因为字段需要在对象实例化之后通过反射赋值,依赖的可变性容易引发并发问题。


2、强制依赖不为空,避免 NPE

构造函数注入要求容器在创建 Bean 时提供所有依赖的实例。如果某个依赖在 Spring 容器中不存在,则在启动阶段就会抛出异常(NoSuchBeanDefinitionException),将问题提前暴露

相反,字段注入或 Setter 注入可能因为某个依赖未注入而导致运行时 NullPointerException,问题更难定位。

复制代码
// 字段注入可能导致 NPE 延迟到运行时
@Autowired
private UserRepository userRepository;

3、主动发现循环依赖,避免隐藏问题

Spring 的构造函数注入能够提前发现循环依赖 (如 A 和 B 互相通过构造函数注入)。当 Spring 尝试创建 A 时需要 B,创建 B 时需要 A,容器会立即抛出 BeanCurrentlyInCreationException,帮助开发者快速修正设计。

而字段注入或 Setter 注入允许循环依赖存在(Spring 通过三级缓存解决了单例 setter 循环依赖),但这样的设计往往意味着职责耦合过重或架构不合理。使用构造函数注入可以强制开发者优化设计。


4、便于单元测试,无需启动 Spring 容器

构造函数注入使得被测试类成为一个普通的 POJO,不依赖 Spring 的反射机制 。在单元测试中,直接 new 对象并传入 Mock 依赖即可:

复制代码
// 测试时不需要 Spring 容器
UserRepository mockRepo = mock(UserRepository.class);
EmailSender mockSender = mock(EmailSender.class);
OrderService service = new OrderService(mockRepo, mockSender);

如果是字段注入的类,你需要借助 SpringExtensionMockitoExtension(通过 @InjectMocks)或反射手动设置字段,测试代码更复杂且与框架耦合。


5、明确表示必需依赖,符合"清晰编码"原则

构造函数参数清晰地列出了类正常运行所必需的所有依赖。任何阅读代码的人一眼就能知道这个类需要哪些服务才能工作,而字段注入则将依赖隐藏在类的内部,降低了代码可读性。

同时,如果一个类的构造函数参数过多(例如超过 7 个),这往往是一个代码异味(Code Smell),提示该类可能承担了过多的职责,需要拆分。构造函数注入天然地促进单一职责原则。


6、与 Spring 框架解耦,更通用的代码风格

虽然字段注入的 @Autowired 注解是 Spring 特有的,但构造函数注入可以不使用任何 Spring 特定注解(Spring 4.3+ 自动推断单构造函数的参数进行注入),使得类可以轻松地被其他 IoC 容器(如 Guice)或纯 Java 代码使用。

复制代码
// 无需 @Autowired,Spring 会自动注入
@Service
public class MyService {
    private final MyDao myDao;
    public MyService(MyDao myDao) { this.myDao = myDao; }
}

7、性能优势(微小但存在)

构造函数注入在 Bean 实例化时一次性完成所有依赖设置,无需后续的字段访问或反射调用(如字段注入在第一次使用时可能需要调用 postProcessProperties)。在高频创建 Bean 的场景下,构造函数注入略微更高效。


8、最佳实践对比示例
不推荐的字段注入(Field Injection)
复制代码
@Service
public class LegacyService {
    @Autowired
    private DependencyA a;   // 缺点:final 不可用,测试困难,隐藏依赖
    @Autowired
    private DependencyB b;
}
推荐的构造函数注入(Constructor Injection)
复制代码
@Service
public class ModernService {
    private final DependencyA a;
    private final DependencyB b;

    public ModernService(DependencyA a, DependencyB b) {
        this.a = a;
        this.b = b;
    }
}

总结表格

|-------------|------------------|------------------------|
| 维度 | 构造函数注入 | 字段/Setter 注入 |
| 不可变性 | ✅ 支持 final | ❌ 不支持 |
| NPE 预防 | ✅ 启动时检查 | ❌ 运行时发现 |
| 循环依赖检测 | ✅ 立即报错 | ⚠️ 可能被解决,但隐藏设计问题 |
| 单元测试 | ✅ 直接 new 对象 | ❌ 需要反射或 Spring 容器 |
| 依赖显式性 | ✅ 清晰列出所需依赖 | ❌ 依赖隐藏在类内部 |
| 框架耦合度 | ✅ 低(普通 Java 构造器) | ❌ 较高(依赖 @Autowired ) |
| 职责单一性提示 | ✅ 参数过多时可发现设计问题 | ❌ 容易不知不觉添加过多依赖 |


例外情况

虽然构造函数注入是首选,但在以下场景中可以(或需要)使用 Setter 注入:

  • 可选依赖:提供合理的默认行为,依赖可为空。
  • 循环依赖重构困难:遗留系统中不得不使用 Setter 注入绕过构造器循环依赖问题。
  • 需要重新注入:某些动态场景需要在 Bean 创建完成后更换依赖(极少见)。

但在绝大多数新代码中,请坚持使用构造函数注入

五、spring AOP底层原理

JDK 动态代理 vs CGLIB

|-----------|-------------------------------|----------------------------------------------------------------|
| 对比项 | JDK 动态代理 | CGLIB |
| 实现基础 | Proxy + InvocationHandler | 字节码生成库(ASM),创建子类 |
| 前提条件 | 目标类必须实现接口 | 目标类无需接口 |
| 限制 | 只能代理接口方法 | 无法代理 final 类/方法 |
| Spring 默认 | 有接口时默认使用 | 无接口时使用;Spring Boot 2.x+ 默认 proxy-target-class=true ,优先 CGLIB |

同类方法调用 AOP 失效问题

原因:Spring AOP 基于代理,同类中 this.methodB() 调用的是目标对象自身方法,而非代理对象方法,无法触发增强。

解决方案(按推荐度排序):

自注入:@Autowired @Lazy private UserService self; 然后 self.methodB()

通过 ApplicationContext 获取代理对象:context.getBean(UserService.class).methodB()

拆分到不同类中

相关推荐
大大杰哥2 小时前
Spring AI 开发笔记:ChatClient 的创建、配置与工具函数注册
人工智能·笔记·spring
0xDevNull2 小时前
Spring中统一异常处理详细教程
java·开发语言·后端
shjita2 小时前
maven涉及的配置
java·前端·maven
Gauss松鼠会2 小时前
GaussDB(DWS)数据融合:云端GaussDB(DWS)迁移
java·服务器·网络·数据库·性能优化·gaussdb
金融小白数据分析之路2 小时前
java 打包exe maven 版本
java·开发语言·maven
兩尛2 小时前
C++面向对象和类相关
java·c++·面试
帅次2 小时前
Android 高级工程师面试参考答案:架构设计、Jetpack 与 Compose
android·面试·职场和发展·架构·composer·jetpack
ch.ju2 小时前
Java程序设计(第3版)第二章——循环结构(3)
java
再玩一会儿看代码2 小时前
idea中快捷键详细总结整理
java·ide·经验分享·笔记·学习·intellij-idea