java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > SpringBoot初始化加载配置

SpringBoot初始化加载配置的八种方式总结

作者:独行客-编码爱好者

在日常开发时,我们常常需要 在SpringBoot应用启动时执行某一段逻辑,如获取一些当前环境的配置或变量、向数据库写入一些初始数据或者连接某些第三方系统,确认对方可以工作,那么在实现初始化逻辑代码时就需要小心了,所以本文介绍了SpringBoot初始化加载配置的方式

序号

初始化加载方式

执行时机

1

@PostConstruct 注解(在方法加注解)

Bean对象初始化完成后执行( 该方法会在所有依赖字段注入后才执行 

2

构造器注入(构造方法加  @Autowired注解 

Bean对象初始化完成后执行( 该方法会在所有依赖字段注入后才执行 

3

InitializingBean 接口(继承 InitializingBean接口,并实现 afterPropertiesSet()这个方法)

Bean对象初始化完成后执行( 该方法会在所有依赖字段注入后才执行 ) 3和4是一样的,实现方式不同

4

@Bean initMethod方法

Bean对象初始化完成后执行( 该方法会在所有依赖字段注入后才执行  3和4是一样的,实现方式不同

5

SmartLifecycle 接口(继承SmartLifecycle 接口),并实现start() 方法

SmartLifecycle 的执行时机是在 Spring 应用上下文刷新完成之后,即所有的 Bean 都已经被实例化和初始化之后。

6

ApplicationListener(继承  ApplicationListener接口,并实现onApplicationEvent()方法  与方法上加 @EventListener的效果一样 

所有Bean初始化完成后才会执行方法

7

CommandLineRunner 继承 CommandLineRunner,继承 run() 方法 

应用启动后执行

8

ApplicationRunner( 继承 CommandLineRunner,继承 run() 方法 

应用启动后执行

背景

在日常开发时,我们常常需要 在SpringBoot 应用启动时执行某一段逻辑,如下面的场景:

  • 获取一些当前环境的配置或变量

  • 向缓存数据库写入一些初始数据

  • 连接某些第三方系统,确认对方可以工作

在实现这些功能时,我们可能会遇到一些"坑"。 为了利用SpringBoot框架的便利性,我们不得不将整个应用的执行控制权交给容器,于是造成了大家对于细节是一无所知的。那么在实现初始化逻辑代码时就需要小心了,比如,我们并不能简单的将初始化逻辑在Bean类的构造方法中实现,类似下面的代码:

@Component 
public class InvalidInitExampleBean { 
    @Autowired 
     private Environment env; 
    
    public InvalidInitExampleBean() { 
    
        env.getActiveProfiles(); 
    } 
}

注意:这里,我们在InvalidInitExampleBean的构造方法中试图访问一个自动注入的env字段,当真正执行时,你一定会得到一个空指针异常(NullPointerException)。原因在于,当构造方法被调用时,Spring上下文中的Environment这个Bean很可能还没有被实例化,同时也仍未注入到当前对象,所以并不能这样进行调用。

下面,我们来看看在SpringBoot中实现"安全初始化"的一些方法:

一、@PostConstruct 注解

@PostConstruct 注解其实是来自于 javax的扩展包中(大多数人的印象中是来自于Spring框架),它的作用在于声明一个Bean对象初始化完成后执行的方法。

来看看它的原始定义:

The PostConstruct annotation is used on a method that needs to be executed after dependency injection is done to perform any initialization.

也就是说,该方法会在所有依赖字段注入后才执行,当然这一动作也是由Spring框架执行的。 

示例:

package com.cfcc.teis.load;
 
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.Arrays;
 
/**
 * @Description TODO
 * @date 2024/12/5 11:19
 * @Version 1.0
 * @Author gezongyang
 */
@Component
@Slf4j
public class InvalidInitExampleBean {
 
    @Autowired
    private Environment environment;
 
    /**
     * 该方法会在所有依赖字段(environment)注入后才执行,当然这一动作也是由Spring框架执行的。
     */
    @PostConstruct
    public void init() {
        //environment 已经注入
        log.info("@PostConstruct execute:{}",Arrays.asList(environment.getDefaultProfiles()));
    }
}

二、实现 InitializingBean 接口

InitializingBean 是由Spring框架提供的接口,其与@PostConstruct注解的工作原理非常类似。

如果不使用注解的话,你需要让Bean实例继承 InitializingBean接口,并实现afterPropertiesSet()这个方法。

示例:

package com.cfcc.teis.load;
 
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
import java.util.Arrays;
 
/**
 * @Description 继承 InitializingBean接口,并实现afterPropertiesSet()
 *  afterPropertiesSet() 会在所有依赖的字段(environment)注入后才执行
 * @date 2024/12/5 11:37
 * @Version 1.0
 * @Author gezongyang
 */
@Component
@Slf4j
public class InitializingBeanExampleBean implements InitializingBean {
 
    @Autowired
    private Environment environment;
 
    /**
      * 这个方法会在environment注入后执行
     */
    @Override
    public void afterPropertiesSet() {
        //environment 已经注入
        log.info("InitializingBean execute:{}", Arrays.asList(environment.getDefaultProfiles()));
    }
}

三、@Bean initMethod方法

我们在声明一个Bean的时候,可以同时指定一个initMethod属性,该属性会指向Bean的一个方法,表示在初始化后执行。

示例:

package com.cfcc.teis.load;
 
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
import java.util.Arrays;
 
/**
 * @Description 指定一个initMethod属性,该属性会指向Bean的一个方法,表示在初始化后执行。
 * @date 2024/12/5 11:37
 * @Version 1.0
 * @Author gezongyang
 */
@Component
@Slf4j
public class InitializingBean {
 
    @Autowired
    private Environment environment;
 
    /**
     * 这个方法会在environment注入后执行
     */
    public void init() {
        //environment 已经注入
        log.info("@Bean initMethod execute:{}", Arrays.asList(environment.getDefaultProfiles()));
    }
 
    /**
     * 这里将initMethod指向init方法,相应的我们也需要在Bean中实现这个方法:
     * @return
     */
    @Bean(initMethod="init")
    public InitializingBean exBean() {
        return new InitializingBean();
    }
}

四、构造器注入

如果依赖的字段在Bean的构造方法中声明,那么Spring框架会先实例这些字段对应的Bean,再调用当前的构造方法。

package com.cfcc.teis.load;
 
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
import java.util.Arrays;
 
/**
 * @Description 如果依赖的字段在Bean的构造方法中声明,那么Spring框架会先实例这些字段对应的Bean,
 * 再调用当前的构造方法。
 * @date 2024/12/5 14:11
 * @Version 1.0
 * @Author gezongyang
 */
@Slf4j
@Component
public class LogicInConstructorExampleBean {
    private final Environment environment;
 
    /**
     * LogicInConstructorExampleBean(Environment environment) 调用在
     * Environment 对象实例化之后
     * @param environment
     */
    @Autowired
    public LogicInConstructorExampleBean(Environment environment) {
        //environment实例已初始化
        this.environment = environment;
        log.info("LogicInConstructor:{}", Arrays.asList(environment.getDefaultProfiles()));
    }
}

五、实现ApplicationListener 接口

ApplicationListener 是由 spring-context组件提供的一个接口,主要是用来监听 “容器上下文的生命周期事件”。它的定义如下:

public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
    /**
     * Handle an application event.
     * @param event the event to respond to
     */
    void onApplicationEvent(E event);
}

这里的event可以是任何一个继承于ApplicationEvent的事件对象。 对于初始化工作来说,我们可以通过监听ContextRefreshedEvent这个事件来捕捉上下文初始化的时机。

示例:

package com.cfcc.teis.load;
 
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;
/**
 * @Description 当所有的Bean都初始化完成后,执行onApplicationEvent 方法
 * @date 2024/12/5 14:19
 * @Version 1.0
 * @Author gezongyang
 */
@Slf4j
@Component
public class StartupApplicationListenerExample  implements ApplicationListener<ContextRefreshedEvent> {
    public static int counter;
 
    /**
     * 这里的event可以是任何一个继承于ApplicationEvent的事件对象。
     * 对于初始化工作来说,我们可以通过监听ContextRefreshedEvent这个事件来捕捉上下文初始化的时机。
     * @param event
     */
    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        log.info("ApplicationListener run!");
        counter++;
    }
}

在Spring上下文初始化完成后,这里定义的方法将会被执行。 与前面的 InitializingBean 不同的是,通过 ApplicationListener 监听的方式是全局性的,也就是当 所有 的 Bean 都 初始化完成 后才会执行方法。
Spring 4.2 之后引入了新的 @EventListener注解,可以实现同样的效果:

package com.cfcc.teis.load;
 
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
 
/**
 * @Description TODO
 * @date 2024/12/5 14:24
 * @Version 1.0
 * @Author gezongyang
 */
@Slf4j
@Component
public class EventListenerExampleBean {
    public static int counter;
 
    /**
     * 所有的Bean都初始化完成后,执行onApplicationEvent方法。
     * @param event
     */
    @EventListener
    public void onApplicationEvent(ContextRefreshedEvent event) {
        log.info("@EventListener execute", event.toString());
        counter++;
    }
}

六、实现 CommandLineRunner 接口

SpringBoot 提供了一个CommanLineRunner接口,用来实现在应用启动后的逻辑控制,其定义如下:

public interface CommandLineRunner {
    /**
     * Callback used to run the bean.
     * @param args incoming main method arguments
     * @throws Exception on error
     */
    void run(String... args) throws Exception;
}

此外,对于多个CommandLineRunner的情况下可以使用@Order注解来控制它们的顺序。

案例:

1 定义一个CommandDemo实现CommandLineRunner,并纳入到spring容器

package com.cfcc.teis.load;
 
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
/**
 * @Description 应用启动后的回调函数,在应用启动后执行此方法
 * @date 2024/12/5 14:32
 * @Version 1.0
 * @Author gezongyang
 */
@Component
@Slf4j
public class CommandDemo implements CommandLineRunner {
    /**
     * 此方法在应用启动后执行
     * @param args
     */
    @Override
    public void run(String... args) {
        log.info("CommandLineRunner run!");
    }
}

2 配置参数,然后执行启动类

3 打印结果:

=====应用已经启动成功======[aaa,bbb]

七、实现 ApplicationRunner 接口

package org.springframework.boot;
 
 
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
 
 
/**
* Interface used to indicate that a bean should <em>run</em> when it is contained within
* a {@link SpringApplication}. Multiple {@link ApplicationRunner} beans can be defined
* within the same application context and can be ordered using the {@link Ordered}
* interface or {@link Order @Order} annotation.
*
* @author Phillip Webb
* @since 1.3.0
* @see CommandLineRunner
*/
@FunctionalInterface
public interface ApplicationRunner {
 
   /**
    * Callback used to run the bean.
    * @param args incoming application arguments
    * @throws Exception on error
    */
   void run(ApplicationArguments args) throws Exception;
}

与 CommandLineRunner接口类似, Spring boot 还提供另一个ApplicationRunner 接口来实现初始化逻辑。不同的地方在于 ApplicationRunner.run()方法接受的是封装好的ApplicationArguments参数对象,而不是简单的字符串参数。ApplicationArguments是对参数做了进一步的处理,可以解析key=value形式,我们可以通过name来获取value(而CommandLineRunner只是获取key=value整体)

package org.springframework.boot;
 
import java.util.List;
import java.util.Set;
/**
* Provides access to the arguments that were used to run a {@link SpringApplication}.
*
* @author Phillip Webb
* @since 1.3.0
*/
public interface ApplicationArguments {
 
   /**
    * Return the raw unprocessed arguments that were passed to the application.
    * @return the arguments
    */
   String[] getSourceArgs();
 
   /**
    * Return the names of all option arguments. For example, if the arguments were
    * "--foo=bar --debug" would return the values {@code ["foo", "debug"]}.
    * @return the option names or an empty set
    */
   Set<String> getOptionNames();
 
   /**
    * Return whether the set of option arguments parsed from the arguments contains an
    * option with the given name.
    * @param name the name to check
    * @return {@code true} if the arguments contain an option with the given name
    */
   boolean containsOption(String name);
 
   /**
    * Return the collection of values associated with the arguments option having the
    * given name.
    * <ul>
    * <li>if the option is present and has no argument (e.g.: "--foo"), return an empty
    * collection ({@code []})</li>
    * <li>if the option is present and has a single value (e.g. "--foo=bar"), return a
    * collection having one element ({@code ["bar"]})</li>
    * <li>if the option is present and has multiple values (e.g. "--foo=bar --foo=baz"),
    * return a collection having elements for each value ({@code ["bar", "baz"]})</li>
    * <li>if the option is not present, return {@code null}</li>
    * </ul>
    * @param name the name of the option
    * @return a list of option values for the given name
    */
   List<String> getOptionValues(String name);
 
   /**
    * Return the collection of non-option arguments parsed.
    * @return the non-option arguments or an empty list
    */
   List<String> getNonOptionArgs();
}

ApplicationArguments可以接收key=value这样的参数,getOptionNames()方法可以得到key的集合,getOptionValues(String name)方法可以得到value这样的集合

示例:

import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
 
/**
* @Description TODO
* @date 2022/6/23 9:55
* @Version 1.0
* @Author gezongyang
*/
@Component
public class ApplicationRunnerDemo implements ApplicationRunner {
 
    @Override
    public void run(ApplicationArguments args) throws Exception {
        System.out.println("====getOptionNames======"+args.getOptionNames());
        System.out.println("====getOptionValues====="+args.getOptionValues("key"));
    }
}

配置参数启动

打印结果:

====getOptionNames======[key]
====getOptionValues=====[value]

八、实现SmartLifecycle 接口

package org.springframework.context;
 
public interface SmartLifecycle extends Lifecycle, Phased {
    boolean isAutoStartup();
    void stop(Runnable var1);
}

SmartLifecycle 是 Spring Framework 中的一个接口,它扩展了 Lifecycle 接口,并提供了更灵活的生命周期管理功能。使用 SmartLifecycle 可以更精细地控制应用组件的启动和关闭过程。以下是 SmartLifecycle 的一些主要特性:

特性:

isAutoStartup(): 返回一个布尔值,指示该组件是否应该在容器启动时自动启动。

getPhase(): 返回一个整数值,表示组件的启动顺序(阶段)。较低的数字表示较早的启动顺序。

start(): 启动组件,当返回时,组件应该已经启动完毕。

stop(Runnable callback): 停止组件,并且可以在停止完成后执行提供的回调函数。

isRunning(): 返回一个布尔值,指示组件当前是否处于运行状态。

案例:

package com.cfcc.teis.load;
 
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.SmartLifecycle;
import org.springframework.stereotype.Component;
 
/**
 * @Description SmartLifecycle 可以更精细地控制应用组件的启动和关闭过程
 * @date 2024/12/5 15:10
 * @Version 1.0
 * @Author gezongyang
 */
@Slf4j
@Component
public class SmartLifecycleDemo implements SmartLifecycle {
 
    private volatile boolean running = false;
 
    /**
     * SmartLifecycle run
     */
    @Override
    public void start() {
        log.info("SmartLifecycle run!");
        this.running = true;
    }
 
    @Override
    public void stop() {
 
    }
 
    /**
     * 在这里放置停止逻辑
     * @param callback
     */
    @Override
    public void stop(Runnable callback) {
        this.running = false;
        callback.run();  // 确保回调被执行
    }
 
    @Override
    public boolean isRunning() {
        return this.running;
    }
 
    /**
     * 组件将在Spring上下文加载后自动启动
     * @return
     */
    @Override
    public boolean isAutoStartup() {
        return true;
    }
 
    /**
     * 定义启动顺序,数字越小越先启动
     * @return
     */
    @Override
    public int getPhase() {
        return 0;
    }
}

注意事项

当你实现 SmartLifecycle 时,确保 start() 方法不会阻塞,因为它会延迟整个应用程序的启动。如果需要长时间运行的任务,考虑将其放入单独的线程中。

stop(Runnable callback) 方法中的回调是重要的,必须调用它来通知框架组件已经停止。如果你不打算使用某些方法,可以不重写它们;默认实现将提供合理的行为。

通过使用 SmartLifecycle,你可以更好地控制应用程序中不同组件的生命周期行为,这对于那些对启动和关闭有特殊需求的应用特别有用。

SmartLifecycle 的执行时机:

是在 Spring 应用上下文刷新完成之后,即所有的 Bean 都已经被实例化和初始化之后。具体来说,Spring 容器在调用 finishRefresh() 方法时会触发所有实现了 SmartLifecycle 接口的 Bean 的启动过程。这是通过容器中的 LifecycleProcessor 来管理的,默认使用的是 DefaultLifecycleProcessor。

SmartLifecycle 执行的具体流程

上下文刷新完成后:当 Spring 应用上下文完成刷新(finishRefresh),意味着所有的 Bean 已经被加载并初始化完毕。

初始化生命周期处理器:Spring 会初始化 LifecycleProcessor,如果没有显式定义,则创建一个默认的 DefaultLifecycleProcessor。

调用 onRefresh():接着,Spring 会调用 LifecycleProcessor 的 onRefresh() 方法,这将导致调用 startBeans() 方法。

根据 phase 分组并排序:startBeans() 方法会获取所有实现了 Lifecycle 接口的 Bean,并根据它们的 phase 属性进行分组和排序。phase 值越小的 Bean 将越早启动。

检查 isAutoStartup():对于每个 SmartLifecycle Bean,如果它的 isAutoStartup() 方法返回 true,那么它的 start() 方法就会被自动调用。

执行 start() 方法:按照 phase 排序后的顺序,依次调用每个 SmartLifecycle Bean 的 start() 方法来启动组件。

设置运行状态:start() 方法应该确保组件处于运行状态,并且 isRunning() 方法应返回 true。

同样的逻辑也适用于停止组件的过程,但是是以相反的顺序执行的,即 phase 值较大的 Bean 会先停止。

因此,如果你希望在 Spring 容器完全准备好之后执行某些任务,比如开启消息监听、启动定时任务等,你可以实现 SmartLifecycle 接口并在 start() 方法中放置这些逻辑。同时,通过调整 getPhase() 返回的值,可以控制多个 SmartLifecycle Bean 之间的启动顺序。

以上就是SpringBoot初始化加载配置的八种方式总结的详细内容,更多关于SpringBoot初始化加载配置的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文