zoukankan      html  css  js  c++  java
  • 补习系列(21)-SpringBoot初始化之7招式

    背景

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

    • 获取一些当前环境的配置或变量
    • 向数据库写入一些初始数据
    • 连接某些第三方系统,确认对方可以工作..

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

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

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

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

    1、 @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框架执行的。

    下面的代码演示了使用@PostConstruct的例子:

    @Component
    public class PostConstructExampleBean {
     
        private static final Logger LOG 
          = Logger.getLogger(PostConstructExampleBean.class);
     
        @Autowired
        private Environment environment;
     
        @PostConstruct
        public void init() {
            //environment 已经注入
            LOG.info(Arrays.asList(environment.getDefaultProfiles()));
        }
    }
    

    2、 InitializingBean 接口

    InitializingBean 是由Spring框架提供的接口,其与@PostConstruct注解的工作原理非常类似。
    如果不使用注解的话,你需要让Bean实例继承 InitializingBean接口,并实现afterPropertiesSet()这个方法。

    下面的代码,展示了这种用法:

    @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 {
            //environment 已经注入
            LOG.info(Arrays.asList(environment.getDefaultProfiles()));
        }
    }
    

    3、 @Bean initMethod方法

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

    如下所示:

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

    然后,这里将initMethod指向init方法,相应的我们也需要在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()));
        }
    }
    

    上面的代码是基于Java注解的方式,使用Xml配置也可以达到同样的效果:

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

    该方式在早期的 Spring版本中大量被使用

    4、 构造器注入

    如果依赖的字段在Bean的构造方法中声明,那么Spring框架会先实例这些字段对应的Bean,再调用当前的构造方法。
    此时,构造方法中的一些操作也是安全的,如下:

    @Component
    public class LogicInConstructorExampleBean {
     
        private static final Logger LOG 
          = Logger.getLogger(LogicInConstructorExampleBean.class);
     
        private final Environment environment;
     
        @Autowired
        public LogicInConstructorExampleBean(Environment environment) {
            //environment实例已初始化
            this.environment = environment;
            LOG.info(Arrays.asList(environment.getDefaultProfiles()));
        }
    }
    

    5、 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这个事件来捕捉上下文初始化的时机。
    如下面的代码:

    @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++;
        }
    }
    

    在Spring上下文初始化完成后,这里定义的方法将会被执行。
    与前面的InitializingBean不同的是,通过ApplicationListener监听的方式是全局性的,也就是当所有的Bean都初始化完成后才会执行方法。
    Spring 4.2 之后引入了新的 @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++;
        }
    }
    

    6、 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;
    
    }
    

    这里的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的情况下可以使用@Order注解来控制它们的顺序。

    7、 ApplicationRunner

    与 CommandLineRunner接口类似, Spring boot 还提供另一个ApplicationRunner 接口来实现初始化逻辑。
    不同的地方在于 ApplicationRunner.run()方法接受的是封装好的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++;
        }
    }
    

    ApplicationArguments对象提供了一些非常方便的方法,可以用来直接获取解析后的参数,比如:

    java -jar application.jar --debug --ip=xxxx
    

    此时通过 ApplicationArguments的getOptionNames就会得到["debug","ip"]这样的值

    测试代码

    下面,通过一个小测试来演示几种初始化方法的执行次序,按如下代码实现一个复合式的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");
        }
     
        //在XML中定义为initMethod
        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
    

    所以,这几种初始化的顺序为:

    1. 构造器方法
    2. @PostConstruct 注解方法
    3. InitializingBean的afterPropertiesSet()
    4. Bean定义的initMethod属性方法

    参考文档

    https://www.baeldung.com/running-setup-logic-on-startup-in-spring

    美码师的 SpringBoot 补习系列

  • 相关阅读:
    Leetcode 191.位1的个数 By Python
    反向传播的推导
    Leetcode 268.缺失数字 By Python
    Leetcode 326.3的幂 By Python
    Leetcode 28.实现strStr() By Python
    Leetcode 7.反转整数 By Python
    Leetcode 125.验证回文串 By Python
    Leetcode 1.两数之和 By Python
    Hdoj 1008.Elevator 题解
    TZOJ 车辆拥挤相互往里走
  • 原文地址:https://www.cnblogs.com/littleatp/p/11123577.html
Copyright © 2011-2022 走看看