SpringBoot/Spring扩展点系列之初始化之@PostConstruct、init-method、InitializingBean - 第434篇
导读
在一些业务场景下,我们想要在Spring Bean初始化后自动做一些事情,比如预加载一部分数据,举例说明:要开发一个短信发送服务,在服务中,有些基本的配置信息是存放在数据库的,那么在Spring Bean初始化就需要从数据库加载到这些配置信息。
一、缘起:为什么需要init()方法?
为什么需要使用额外的方式来加载一个类的init()方法,好奇的宝宝可能会有这么一个疑问:类的初始化不是会执行构造方法吗?宝宝不要急,听我慢慢到来。
有这么一种场景:在Spring Bean中要调用另外一个Spring Bean的方法,那么这个时候会出现什么情况呢?看个小栗子:
package com.kfit.demo.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Service;
@Service
public class DemoService3 {
@Autowired
private Environment environment;
public DemoService3(){
System.out.println("DemoService3的构造方法打印environment="+environment);
}
}
这个小栗子很简单,就是在DemoService3中注入Environment,然后在构造方法打印Environment的值。小伙伴们,猜猜看Environment的值是多少?
如果对于Spring的执行顺序不是很了解的,这个问题,不见得能理解的很到位。
先看下执行的结果:
执行结果是null,惊不惊喜,意不意外。这个说明一个点,Spring中对于类的初始化以及注入属性值,这个是两个步骤,不是一次性完成的。此时当类的构造方法执行时,类的属性是并未被注入的,那么也就无法获取到Environment的值了。
二、破解:使用Spring的init()
对于这种,需要在Spring初始化之后做一些事情的话,那么怎么破呢?
对于初始化数据常用的有3种实现方式:
(1)使用JSR-250规范定义的@Postconstruct注解。
(2)使用Spring提供的@Bean init-method标签。
(3)实现InitializingBean接口,实现afterPropertiesset()方法。
对于这3种方式的使用,我们直接来看个小栗子:
package com.kfit.demo.service;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import javax.annotation.PostConstruct;
public class DemoService4 implements InitializingBean {
@Autowired
private Environment environment;
public DemoService4(){
System.out.println("DemoService4的构造方法打印environment="+environment);
}
/**
* 使用JSR-250规范定义的@Postconstruct注解
*/
@PostConstruct
public void postConstruct() {
System.out.println("DemoService4使用@postConstruct打印environment="+environment);
}
/**
* 使用@Bean的init-method属性
*/
public void initMethod() {
System.out.println("DemoService4使用initMethod打印environment="+environment);
}
/**
* InitializingBean接口中的方法
* @throws Exception
*/
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("DemoService4使用InitializingBean打印environment=" + environment);
}
}
然后在添加一个配置类:
package com.kfit.config;
import com.kfit.demo.service.DemoService4;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class BeanConfig {
@Bean(initMethod = "initMethod")
public DemoService4 demoService4(){
return new DemoService4();
}
}
说明:
(1)在这里定义了一个普通的java类DemoService4,然后通过配置类BeanConfig的@Bean进行注入。
(2)在@Bean注入的时候,指定了初始化方法initMethod。
(3)对于@PostConstruct的使用只需要在类的方法上添加注解即可。
(4)对于InitializingBean的使用需要实现接口InitializingBean中的afterPropertiesSet方法。
来看下控制台的信息打印:
调用顺序:构造函数 --> @PostConstruct --> InitializingBean接口 --> @Bean init-method
基本的使用就这么简单,那么你还会问,纳尼,为啥存在这么种方式呢?它们有什么区别?
三、缘灭:前因后果
3.1 @PostConstruct
注解@PostConstruct是javax里定义的,属于JSR-250规范的一部分。
注: @PostConstruct已经从jdk 11版本开始移除了,所以在使用时需要额外引入javax.annotation-api:
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
</dependency>
使用方法就是类的方法上添加注解@PostConstruct,那么是否允许注解多个方法呢?答案是可以的,看如下执行效果:
对应的代码块:
其实不执行注解多个,也不影响,我们可以在方法中调用内部的其它方法就可以了。
3.2 @Bean init-method
在早起的Spring的版本是使用的xml实现,如<bean id="demoService" class="com.kfit.DemoService" init-method="initMethod"/>,而在Spring高版本中,可以使用注解的方式配置。
@Bean(initMethod = "initMethod")
public DemoService4 demoService4(){
return new DemoService4();
}
对于这种实现方式,我们会发现需要额外的@Configuration来声明一个@Bean。
如果对于使用@Service、@Controller等这样的方式注入Bean的,那么这种实现方案,就有点爱莫能助了。
initMethod就是原来spring配置文件里bean标签上的init-method,
3.3实现InitializingBean接口
方式相当于与Spring框架深度绑定了,所以官方文档上也说了并不是十分推荐用这个方式,而是推荐使用第1或是第2种方式。
官方文档:
https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-factory-lifecycle-initializingbean
但是对于我们的项目深度捆绑又如何呢?本身框架就是基于Spring的体系搭建的,难道你能够把框架变成summer,想太多。所以就是哪种合适使用哪一种就好了,当然如果前面两种可以满足,优先使用前面两种方式。
如果你有看过MyBatis的代码,就可以看到SqlSessionFactoryBean就是实现了接口InitializingBean: