Autowired 对象,但是对象为null【Spring初始化顺序问题】

背景和价值

@Service
public class LlmService {
	private static final Logger log = LoggerFactory.getLogger(LlmService.class);

	private final ChatClient agentExecutionClient;

	private final ChatClient planningChatClient;

	private final ChatClient finalizeChatClient;

	@Autowired
	JdbcChatMemoryRepository chatMemoryRepository;

	private final ChatMemory conversationMemory = MessageWindowChatMemory.builder().chatMemoryRepository(chatMemoryRepository).maxMessages(1000).build();

	private final ChatMemory agentMemory = MessageWindowChatMemory.builder().chatMemoryRepository(chatMemoryRepository).maxMessages(1000).build();

    }

原因分析:

conversationMemory 和 agentMemory 的初始化发生在对象构造阶段,此时 @Autowired 的字段注入尚未完成。
这是因为 Spring 的字段注入是在对象实例化之后进行的,而 private final ChatMemory 的初始化代码在构造方法之前执行

代码修改

@Service
public class LlmService {
    private final JdbcChatMemoryRepository chatMemoryRepository;
    private final ChatMemory conversationMemory;
    private final ChatMemory agentMemory;

  
    public LlmService(JdbcChatMemoryRepository chatMemoryRepository) {
        this.chatMemoryRepository = chatMemoryRepository;
        this.conversationMemory = MessageWindowChatMemory.builder()
            .chatMemoryRepository(chatMemoryRepository)
            .maxMessages(1000)
            .build();
        this.agentMemory = MessageWindowChatMemory.builder()
            .chatMemoryRepository(chatMemoryRepository)
            .maxMessages(1000)
            .build();
    }
}

构造器注入 vs Autowired

在 Spring 框架中,构造器注入@Autowired 字段注入的依赖注入顺序和机制存在显著差异,以下是两者的核心区别:


一、注入顺序的差异

  1. 构造器注入

    • 执行时机:在对象实例化时通过构造器参数完成依赖注入。
    • 顺序优先级:构造器执行 → 字段初始化 → @PostConstruct 方法调用。
    • 示例
      public class ServiceA {
          private final ServiceB serviceB;
          
          public ServiceA(ServiceB serviceB) { // 构造器注入
              this.serviceB = serviceB;
          }
      }
      
  2. @Autowired 字段注入

    • 执行时机:在对象实例化后通过反射完成依赖注入。
    • 顺序优先级:构造器执行 → 字段注入 → @PostConstruct 方法调用。
    • 示例
      public class ServiceB {
          @Autowired
          private ServiceC serviceC; // 字段注入
      }
      

二、核心区别

特性 构造器注入 @Autowired 字段注入
不可变性 支持 final 字段 不支持 final 字段
依赖完整性 强制要求依赖在构造时存在(避免空指针) 依赖可能在运行时未初始化(存在空指针风险)
可测试性 易于通过构造器传入模拟对象 需要依赖 Spring 容器或反射
循环依赖处理 天然支持(通过构造器参数链式解决) 需要额外配置(如 @Lazy
代码可读性 明确列出所有依赖 依赖关系可能隐藏在字段中

三、适用场景

  1. 构造器注入

    • 推荐场景
      • 依赖不可变(如 final 字段)。
      • 需要强制依赖完整性(如核心服务)。
      • 单元测试需要手动注入模拟对象。
    • 示例
      @Component
      public class OrderService {
          private final PaymentService paymentService;
          
          public OrderService(PaymentService paymentService) { // 强制依赖
              this.paymentService = paymentService;
          }
      }
      
  2. @Autowired 字段注入

    • 适用场景
      • 依赖非核心(如可选工具类)。
      • 代码简洁性优先(如快速原型开发)。
    • 示例
      @Component
      public class LogService {
          @Autowired
          private AuditLogger auditLogger; // 非核心依赖
      }
      

四、最佳实践

  1. 优先使用构造器注入
    • 遵循 Spring 官方推荐,确保依赖不可变性和完整性。
  2. 避免混合使用两种方式
    • 混合注入可能导致依赖顺序混乱(如构造器中依赖未初始化)。
  3. 处理循环依赖
    • 若必须使用字段注入,可通过 @Lazy 延迟加载。

总结

构造器注入通过强制依赖在对象创建时初始化,提供了更高的代码安全性和可维护性;而 @Autowired 字段注入虽然简化了代码,但牺牲了依赖的不可变性和测试便利性。在实际开发中,应根据具体需求选择合适的注入方式。

参考资料

posted @ 2025-05-28 23:34  向着朝阳  阅读(33)  评论(0)    收藏  举报