Skip to content

Spring依赖注入时机问题:构造函数vs @PostConstruct

在Spring框架开发中,依赖注入的时机是一个容易被忽视但非常重要的问题。本文将通过一个实际案例,深入分析Spring Bean生命周期中依赖注入的执行顺序,以及如何正确处理需要在Bean初始化完成后执行的逻辑。

问题背景

在开发AI模块的指标调度器时,遇到了一个典型的依赖注入时机问题:

java
 @Component
  @Slf4j
  public class SyncAiTaskScheduler {

      @Resource
      private BigDataProxy bigDataProxy;

      @Resource
      private ApplicationEventPublisher eventPublisher;

      public SyncAiTaskScheduler() {
          // 在构造函数中启动定时任务
          ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();
          service.scheduleAtFixedRate(() -> executeAiTask(task), 0, 5 * 60, TimeUnit.SECONDS);
      }

      private void executeAiTask(AiMetric task) {
          // 使用bigDataProxy获取数据
          String result = bigDataProxy.getMonitorResultV2(...); // NullPointerException!
      }
  }

报错信息:

  java.lang.NullPointerException: null
      at com.tywl.data.gather.ai.scheduler.SyncAiTaskScheduler.executeAiTask(SyncAiTaskScheduler.java:90)

问题分析

Spring Bean生命周期

Spring Bean的创建过程遵循以下顺序:

  1. 实例化阶段: 调用构造函数创建Bean实例
  2. 属性注入阶段: 通过@Resource、@Autowired等注解注入依赖
  3. 初始化阶段: 调用@PostConstruct标注的方法
  4. Bean就绪: Bean完全初始化完成,可以被使用

问题根因

在构造函数执行时,Spring还没有完成依赖注入,此时:

  • bigDataProxy 字段为 null
  • eventPublisher 字段为 null
  • 立即启动的定时任务尝试使用这些依赖时触发空指针异常
java
public SyncAiTaskScheduler() {
    // 此时 bigDataProxy 还是 null!
    // 但是定时任务已经开始执行,会导致 NPE
    service.scheduleAtFixedRate(() -> executeAiTask(task), 0, 5 * 60, TimeUnit.SECONDS);
}

解决方案

使用@PostConstruct注解

将需要使用依赖的初始化逻辑移到@PostConstruct方法中:

java
@Component
@Slf4j
public class SyncAiTaskScheduler {

    @Resource
    private BigDataProxy bigDataProxy;

    @Resource
    private ApplicationEventPublisher eventPublisher;

    private final List<ScheduledExecutorService> schedulers = new ArrayList<>();

    public SyncAiTaskScheduler() {
        // 构造函数中不启动任务,等待依赖注入完成
        log.info("AI调度器构造完成,等待依赖注入完成后启动任务");
    }

    @PostConstruct
    public void initSchedulers() {
        log.info("AI调度器开始初始化任务...");

        // 此时所有依赖已经注入完成,可以安全使用
        int length = AiMetric.values().length;
        double totalStartupTimeSec = 300.0;
        double intervalSec = totalStartupTimeSec / length;

        for (int i = 0; i < length; i++) {
            final AiMetric task = AiMetric.values()[i];
            long initialDelay = Math.round(i * intervalSec);

            ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();
            service.scheduleAtFixedRate(() -> executeAiTask(task), initialDelay, 5 * 60, TimeUnit.SECONDS);
            schedulers.add(service);

            log.info("AI调度器启动任务: {}, 初始延迟: {}秒", task.getCode(), initialDelay);
        }

        log.info("AI调度器任务初始化完成,共启动 {} 个任务", schedulers.size());
    }

    private void executeAiTask(AiMetric task) {
        try {
            // 现在可以安全使用 bigDataProxy
            String result = bigDataProxy.getMonitorResultV2(...);

            // 发布事件
            eventPublisher.publishEvent(new MetricCollectedEvent(task.getCode(), value));

        } catch (Exception e) {
            log.error("AI任务执行异常: {}", task.getCode(), e);
        }
    }

    @PreDestroy
    public void shutdown() {
        log.info("AI调度器正在关闭...");
        for (ScheduledExecutorService scheduler : schedulers) {
            if (scheduler != null) {
                scheduler.shutdown();
            }
        }
        log.info("AI调度器已关闭");
    }
}

关键改进点

  1. 构造函数简化: 只进行不依赖外部Bean的初始化
  2. @PostConstruct: 在依赖注入完成后执行需要依赖的初始化逻辑
  3. @PreDestroy: 确保资源正确释放
  4. 详细日志: 跟踪Bean生命周期各个阶段

最后更新于:

本站所有文章遵循CC BY 4.0许可协议