概述
本教程将重点介绍如何在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. @Bean 的 initMethod 属性
我们可以使用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,我们可以结合上述机制。执行顺序如下:
- 构造函数
- 带有
@PostConstruct
注解的方法 InitializingBean
的afterPropertiesSet()
方法- 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找到。