概述

本教程将重点介绍如何在Spring应用程序启动时运行逻辑

2. 启动时运行逻辑

在Spring应用程序启动期间或之后运行逻辑是常见的场景,但也可能导致多个问题。为了利用控制反转(Inversion of Control)的优势,我们需要放弃对应用程序流程的部分控制,让容器接管。因此,初始化、启动时的逻辑等需要特别注意。

我们不能简单地在bean的构造函数中包含我们的逻辑,或者在对象实例化后调用方法,因为在这些过程中我们并没有控制权。

让我们看一个实际的例子:

@Component
public class InvalidInitExampleBean {

    @Autowired
    private Environment env;

    public InvalidInitExampleBean() {
        env.getActiveProfiles();
    }
}

这里我们试图在构造函数中访问一个自动注入的字段。当构造函数被调用时,Spring bean还没有完全初始化。这是一个问题,因为**尝试访问尚未初始化的字段会导致NullPointerException**。

接下来,我们将探讨Spring为我们处理这种情况的一些方法。

2.1. @PostConstruct 注解

我们可以使用Java标准库的**@PostConstruct注解来标记一个方法,该方法应在bean初始化后的第一时间**执行一次。请记住,即使没有注入任何内容,Spring也会执行带有该注解的方法。

这是@PostConstruct的示例应用:

@Component
public class PostConstructExampleBean {

    private static final Logger LOG 
      = Logger.getLogger(PostConstructExampleBean.class);

    @Autowired
    private Environment environment;

    @PostConstruct
    public void init() {
        LOG.info(Arrays.asList(environment.getDefaultProfiles()));
    }
}

我们可以看到,Environment实例安全地注入并随后在带有@PostConstruct注解的方法中调用,不会抛出NullPointerException

2.2. InitializingBean 接口

通过实现InitializingBean接口的方法也可以达到类似的效果。不需要在方法上添加注解,而是需要实现afterPropertiesSet()方法。

下面是使用InitializingBean接口实现之前示例的方式:

@Component
public class InitializingBeanExampleBean implements InitializingBean {

    private static final Logger LOG 
      = Logger.getLogger(InitializingBeanExampleBean.class);

    @Autowired
    private Environment environment;

    @Override
    public void afterPropertiesSet() throws Exception {
        LOG.info(Arrays.asList(environment.getDefaultProfiles()));
    }
}

2.3. ApplicationListener

我们可以使用此方法在Spring上下文初始化完成后执行逻辑。这意味着我们不关注特定的bean,而是等待所有bean初始化完成。

要做到这一点,我们需要创建一个实现ApplicationListener<ContextRefreshedEvent>接口的bean:

@Component
public class StartupApplicationListenerExample implements 
  ApplicationListener<ContextRefreshedEvent> {

    private static final Logger LOG 
      = Logger.getLogger(StartupApplicationListenerExample.class);

    public static int counter;

    @Override public void onApplicationEvent(ContextRefreshedEvent event) {
        LOG.info("Increment counter");
        counter++;
    }
}

我们也可以使用新引入的@EventListener注解来实现相同效果:

@Component
public class EventListenerExampleBean {

    private static final Logger LOG 
      = Logger.getLogger(EventListenerExampleBean.class);

    public static int counter;

    @EventListener
    public void onApplicationEvent(ContextRefreshedEvent event) {
        LOG.info("Increment counter");
        counter++;
    }
}

确保根据需求选择合适的事件。在这个例子中,我们选择了ContextRefreshedEvent

2.4. @BeaninitMethod 属性

我们可以使用initMethod属性在bean初始化后运行一个方法。

下面是一个bean的定义:

public class InitMethodExampleBean {

    private static final Logger LOG = Logger.getLogger(InitMethodExampleBean.class);

    @Autowired
    private Environment environment;

    public void init() {
        LOG.info(Arrays.asList(environment.getDefaultProfiles()));
    }
}

请注意,我们没有实现任何特殊接口,也没有使用特殊的注解。

然后,我们可以通过@Bean注解定义bean:

@Bean(initMethod="init")
public InitMethodExampleBean initMethodExampleBean() {
    return new InitMethodExampleBean();
}

而在XML配置中,bean定义看起来像这样:

<bean id="initMethodExampleBean"
  class="com.baeldung.startup.InitMethodExampleBean"
  init-method="init">
</bean>

2.5. 构造函数注入

如果使用构造函数注入字段,我们可以直接在构造函数中包含我们的逻辑:

@Component 
public class LogicInConstructorExampleBean {

    private static final Logger LOG 
      = Logger.getLogger(LogicInConstructorExampleBean.class);

    private final Environment environment;

    @Autowired
    public LogicInConstructorExampleBean(Environment environment) {
        this.environment = environment;
        LOG.info(Arrays.asList(environment.getDefaultProfiles()));
    }
}

2.6. Spring Boot CommandLineRunner

Spring Boot提供了一个CommandLineRunner接口,其run()方法会在应用程序启动后,Spring应用上下文实例化后调用。

让我们看一个例子:

@Component
public class CommandLineAppStartupRunner implements CommandLineRunner {
    private static final Logger LOG =
      LoggerFactory.getLogger(CommandLineAppStartupRunner.class);

    public static int counter;

    @Override
    public void run(String...args) throws Exception {
        LOG.info("Increment counter");
        counter++;
    }
}

注意:文档所述,同一个应用上下文中可以定义多个CommandLineRunner bean,并且可以使用@Ordered接口或@Order注解进行排序。

2.7. Spring Boot ApplicationRunner

CommandLineRunner类似,Spring Boot还提供了ApplicationRunner接口,其run()方法在应用程序启动时调用。然而,回调方法接收的是ApplicationArguments类的一个实例,而不是原始的字符串参数。

ApplicationArguments接口有获取选项参数值和普通参数值的方法。以双破折号前缀的参数被视为选项参数。

看一个例子:

@Component
public class AppStartupRunner implements ApplicationRunner {
    private static final Logger LOG =
      LoggerFactory.getLogger(AppStartupRunner.class);

    public static int counter;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        LOG.info("Application started with option names : {}", 
          args.getOptionNames());
        LOG.info("Increment counter");
        counter++;
    }
}

3. 组合机制

为了完全控制我们的bean,我们可以结合上述机制。执行顺序如下:

  1. 构造函数
  2. 带有@PostConstruct注解的方法
  3. InitializingBeanafterPropertiesSet()方法
  4. XML中指定的init-method初始化方法

让我们创建一个结合所有机制的Spring bean:

@Component
@Scope(value = "prototype")
public class AllStrategiesExampleBean implements InitializingBean {

    private static final Logger LOG 
      = Logger.getLogger(AllStrategiesExampleBean.class);

    public AllStrategiesExampleBean() {
        LOG.info("Constructor");
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        LOG.info("InitializingBean");
    }

    @PostConstruct
    public void postConstruct() {
        LOG.info("PostConstruct");
    }

    public void init() {
        LOG.info("init-method");
    }
}

如果我们尝试实例化这个bean,我们可以看到符合上述顺序的日志:

[main] INFO o.b.startup.AllStrategiesExampleBean - Constructor
[main] INFO o.b.startup.AllStrategiesExampleBean - PostConstruct
[main] INFO o.b.startup.AllStrategiesExampleBean - InitializingBean
[main] INFO o.b.startup.AllStrategiesExampleBean - init-method

4. 总结

在这篇文章中,我们展示了在Spring应用程序启动时运行逻辑的多种方式。

代码示例可以在GitHub找到。


« 上一篇: Spring集成测试