Spring Bean 生命周期你除了会背八股文面试,真的会用了吗?
Spring Bean 生命周期你除了会背八股文面试,真的会用了吗?Spring Bean 的初始化过程及销毁过程中的一些问题。
有些 bug 可在 Spring 异常提示下快速解决,但却不理解背后原理一些错误,不易在开发环境下被发现,从而在产线上造成较为严重后果 1 使用构造器参数实现隐式注入类初始化时的常见 bug。构建宿舍管理系统时,有 LightMgrService 来管理 LightService,控制宿舍灯的开启和关闭。
现在期望在 LightMgrService 初始化时自动调用 LightService#check 检查所有宿舍灯的电路是否正常:
Spring Bean 生命周期你除了会背八股文面试,真的会用了吗?我们在 LightMgrService 的默认构造器中调用了通过 @Autoware 注入的成员变量 LightService#check:
LightService 对象的原始类 Spring Bean 生命周期你除了会背八股文面试,真的会用了吗?预期现象:
在 LightMgrService 初始化过程中,LightService 因被**@Autowired**标记,所以能被自动装配在 LightMgrService 构造器执行中,LightService#check() 能被自动调用打印 check all lights 然而事与愿违,我们得到的只会是 NPE:
Spring Bean 生命周期你除了会背八股文面试,真的会用了吗?1.1 源码解析根因在于对 Spring 类初始化过程没有足够的了解。下面这张时序图描述了 Spring 启动时的一些关键结点:
将一些必要系统类,比如 Bean 后置处理器,注册到 Spring 容器,包括 CommonAnnotationBeanPostProcessor 将这些后置处理器实例化,并注册到 Spring 容器实例化所有用户定制类,调用后置处理器进行辅助装配、类初始化等等。
CommonAnnotationBeanPostProcessor 后置处理类是何时被 Spring 加载和实例化的呢?
很多必要系统类,比如 Bean 后置处理器(CommonAnnotationBeanPostProcessor、AutowiredAnnotationBeanPostProcessor 等),都是被 Spring 统一加载和管理通过 Bean 后置处理器,Spring 能灵活地在不同场景调用不同后置处理器,比如 @PostConstruct,它的处理逻辑就要用到 CommonAnnotationBeanPostProcessor(继承自 InitDestroyAnnotationBeanPostProcessor)Spring 初始化单例类的一般过程:
getBean()doGetBean()getSingleton()若发现 Bean 不存在,则调用
createBean()=》doCreateBean()进行实例化。
doCreateBean()protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)throws BeanCreationException {// ...if (instanceWrapper == null) {// 1.instanceWrapper = createBeanInstance(beanName, mbd, args);}final Object bean = instanceWrapper.getWrappedInstance();
}Bean 初始化关键步骤:
实例化 Bean 注入 Bean 依赖初始化 Bean (例如执行 @PostConstruct 标记的方法 )实例化 Bean 的 createBeanInstance 通过依次调用:
DefaultListableBeanFactory.instantiateBean()SimpleInstantiationStrategy.instantiate()最终执行到 BeanUtils.instantiateClass():
public static <T> T instantiateClass(Constructor<T> ctor, Object... args) throws BeanInstantiationException {Assert.notNull(ctor, "Constructor must not be null");try {ReflectionUtils.makeAccessible(ctor);return (KotlinDetector.isKotlinReflectPresent() && KotlinDetector.isKotlinType(ctor.getDeclaringClass()) ?KotlinDelegate.instantiateClass(ctor, args) : ctor.newInstance(args));}catch (InstantiationException ex) {throw new BeanInstantiationException(ctor, "Is it an abstract class?", ex);}// ...}最终调用 ctor.newInstance() 实例化用户定制类 LightMgrService,而默认构造器在类实例化时被自动调用,Spring 也无法控制。
而此时负责自动装配的 populateBean 方法还没有执行,LightMgrService 的属性 LightService 还是 null,导致 NPE。
修正问题在于使用 @Autowired 直接标记在成员属性引发的装配行为发生在构造器执行后。
所以可通过如下方案解决:
构造器注入 Spring Bean 生命周期你除了会背八股文面试,真的会用了吗?当使用上述代码,构造器参数 LightService 会被自动注入 LightService 的 Bean,从而在构造器执行时,避免 NPE。
Spring 在类属性完成注入之后,会回调我们定义的初始化方法。即在 populateBean 方法之后,会调用
AbstractAutowireCapableBeanFactory#initializeBeanSpring Bean 生命周期你除了会背八股文面试,真的会用了吗?applyBeanPostProcessorsBeforeInitialization 处理 @PostConstructinvokeInitMethods 处理 InitializingBean 接口两种不同的初始化方案的逻辑
applyBeanPostProcessorsBeforeInitialization 与 @PostConstruct
applyBeanPostProcessorsBeforeInitialization 方法最终执行到
InitDestroyAnnotationBeanPostProcessor#buildLifecycleMetadata:
applyBeanPostProcessorsBeforeInitialization 处理 @PostConstructinvokeInitMethods 处理 InitializingBean 接口两种不同的初始化方案的逻辑
applyBeanPostProcessorsBeforeInitialization 与 @PostConstructapplyBeanPostProcessorsBeforeInitialization 方法最终执行到 InitDestroyAnnotationBeanPostProcessor#buildLifecycleMetadata:在这个方法里,Spring 将遍历查找被 PostConstruct.class 注解过的方法,返回到上层,并最终调用此方法。
invokeInitMethods 与 InitializingBean 接口给 bean 一个机会去响应现在它的所有属性都已设置,并有机会了解它拥有的 bean 工厂(这个对象)。 这意味着检查 bean 是否实现了 InitializingBean 或自定义了 init 方法。
若是,则调用必要的回调。
invokeInitMethods 会判断当前 Bean 是否实现了 InitializingBean 接口,只有实现该接口时,Spring 才会调用该 Bean 的接口实现方法 afterPropertiesSet()。
Spring Bean 生命周期你除了会背八股文面试,真的会用了吗?还有两种方式:
init 方法 && @PostConstructSpring Bean 生命周期你除了会背八股文面试,真的会用了吗?Spring Bean 生命周期你除了会背八股文面试,真的会用了吗?实现 InitializingBean 接口,回调 afterPropertiesSet()Spring Bean 生命周期你除了会背八股文面试,真的会用了吗?对于本案例,后两种方案并非最优。但在一些场景下,这两种方案各有所长。
2 意外触发 shutdown 方法类销毁时,也容易写出一堆 bug。
LightService#shutdown,负责关灯:
Spring Bean 生命周期你除了会背八股文面试,真的会用了吗?之前的案例中,若宿管系统重启,灯是不会被关闭的。但随着业务变化,可能会去掉 @Service ,而使用另外一种产生 Bean 的方式:创建一个配置类 BeanConfiguration(标记 @Configuration)来创建一堆 Bean,其中就包含了创建 LightService 类型的 Bean,并将其注册到 Spring 容器:
Spring Bean 生命周期你除了会背八股文面试,真的会用了吗?让 Spring 启动完成后立马关闭当前 Spring 上下文,这就能模拟模拟宿管系统的启停:
Spring Bean 生命周期你除了会背八股文面试,真的会用了吗?
以上代码没有其他任何方法的调用,仅是将所有符合约定的类初始化并加载到 Spring 容器,完成后再关闭当前 Spring 容器。预期:运行后不会有任何 log,只改变 Bean 的产生方式。
运行后,控制台打印:
Spring Bean 生命周期你除了会背八股文面试,真的会用了吗?
显然 shutdown 方法未按照预期,被执行了,这就导致一个有意思的 bug:
在使用新的 Bean 生成方式之前,每一次宿舍管理服务被重启时,宿舍里所有的灯都不会被关闭但修改后,只要服务重启,灯都被意外关闭你能理解这个 bug 吗?
源码解析发现:
只有通过使用 Bean 注解注册到 Spring 容器的对象,才会在 Spring 容器被关闭时自动调用 shutdown 使用 @Component 将当前类自动注入到 Spring 容器时,shutdown 方法则不会被自动执行可尝试到 Bean 注解类的代码中去寻找一些线索,可看到属性 destroyMethod。
使用 Bean 注解的方法所注册的 Bean 对象,如果用户不设置 destroyMethod 属性,则其属性值为 AbstractBeanDefinition.INFER_METHOD。
此时 Spring 会检查当前 Bean 对象的原始类中是否有名为 shutdown 或 close 的方法:
有,此方法会被 Spring 记录下来,并在容器被销毁时自动执行没有,安然无事查找 INFER_METHOD 枚举值的引用,很容易就找到了使用该枚举值的方法
DisposableBeanAdapter#inferDestroyMethodIfNecessaryprivate String inferDestroyMethodIfNecessary(Object bean, RootBeanDefinition beanDefinition) {String destroyMethodName = beanDefinition.getDestroyMethodName();if (AbstractBeanDefinition.INFER_METHOD.equals(destroyMethodName) ||(destroyMethodName == null && bean instanceof AutoCloseable)) {if (!(bean instanceof DisposableBean)) {try// 尝试查找 close 方法 return bean.getClass().getMethod(CLOSE_METHOD_NAME).getName();}catch (NoSuchMethodException ex) {try {// 尝试查找 shutdown 方法 return bean.getClass().getMethod(SHUTDOWN_METHOD_NAME).getName();}catch (NoSuchMethodException ex2) {// no candidate destroy method found}}}return null;}return (StringUtils.hasLength(destroyMethodName) ? destroyMethodName : null);}代码逻辑和 Bean 注解类中对于 destroyMethod 属性的注释:完全一致。
destroyMethodName==INFER_METHOD&&当前类没有实现 DisposableBean 接口则先查找类的 close 方法:
找不到就在抛出异常后继续查找 shutdown 方法找到则返回其方法名(close 或者 shutdown)接着,继续逐级查找引用,最终得到的调用链从上到下为:
doCreateBeanregisterDisposableBeanIfNecessaryregisterDisposableBean(new DisposableBeanAdapter)inferDestroyMethodIfNecessary 然后,我们追溯到了顶层的 doCreateBean:
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)throws BeanCreationException {// 实例化 beanif (instanceWrapper == null) {instanceWrapper = createBeanInstance(beanName, mbd, args);}// ...// 初始化 bean 实例.Object exposedObject = bean;try {populateBean(beanName, mbd, instanceWrapper)exposedObject = initializeBean(beanName, exposedObject, mbd);}// ...// Register bean as disposable.try {registerDisposableBeanIfNecessary(beanName, bean, mbd);}catch (BeanDefinitionValidationException ex) {throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Invalid destruction signature", ex);}
return exposedObject;}doCreateBean 管理了 Bean 的整个生命周期中几乎所有的关键节点,直接负责了 Bean 对象的生老病死,其主要功能包括:
Bean 实例的创建 Bean 对象依赖的注入定制类初始化方法的回调 Disposable 方法的注册接着,继续查看 registerDisposableBean:
public void registerDisposableBean(String beanName, DisposableBean bean) {synchronized (this.disposableBeans) {this.disposableBeans.put(beanName, bean);}}DisposableBeanAdapter 类(其属性 destroyMethodName 记录了使用哪种 destory 方法)被实例化
并添加到 DefaultSingletonBeanRegistry#disposableBeans 属性内,disposableBeans 将暂存这些 DisposableBeanAdapter 实例,直到 AnnotationConfigApplicationContext#close 被调用。
而当 AnnotationConfigApplicationContext#close 被调用时,即当 Spring 容器被销毁时,最终会调用到 DefaultSingletonBeanRegistry#destroySingleton:
遍历 disposableBeans 属性逐一获取 DisposableBean 依次调用其 close 或 shutdownpublic void destroySingleton(String beanName) {// Remove a registered singleton of the given name, if any.removeSingleton(beanName);// Destroy the corresponding DisposableBean instance.DisposableBean disposableBean;synchronized (this.disposableBeans) {disposableBean = (DisposableBean) this.disposableBeans.remove(beanName);}destroyBean(beanName, disposableBean);}案例调用了 LightService#shutdown 方法,将所有的灯关闭了。
修正避免在 Java 类中定义一些带有特殊意义动词的方法来解决。
如果一定要定义名为 close 或者 shutdown 方法,可以将 Bean 注解内 destroyMethod 属性设置为空。如下:
import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;@Configurationpublic class BeanConfiguration {
}为什么 @Service 注入的 LightService,其 shutdown 方不能被执行?想要执行,则必须要添加 DisposableBeanAdapter,而它的添加是有条件的:
protected void registerDisposableBeanIfNecessary(String beanName, Object bean, RootBeanDefinition mbd) {AccessControlContext acc = (System.getSecurityManager() != null ? getAccessControlContext() : null);if (!mbd.isPrototype() && requiresDestruction(bean, mbd)) {if (mbd.isSingleton()) {// Register a DisposableBean implementation that performs all destruction// work for the given bean: DestructionAwareBeanPostProcessors,// DisposableBean interface, custom destroy method.registerDisposableBean(beanName,new DisposableBeanAdapter(bean, beanName, mbd, getBeanPostProcessors(), acc));}else {//省略非关键代码}}}关键的语句在于:
!mbd.isPrototype() && requiresDestruction(bean, mbd 案例代码修改前后,我们都是单例,所以区别仅在于是否满足 requiresDestruction 条件。
DisposableBeanAdapter#hasDestroyMethod:
public static boolean hasDestroyMethod(Object bean, RootBeanDefinition beanDefinition) {if (bean instanceof DisposableBean || bean instanceof AutoCloseable) {return true;}String destroyMethodName = beanDefinition.getDestroyMethodName();if (AbstractBeanDefinition.INFER_METHOD.equals(destroyMethodName)) {return (ClassUtils.hasMethod(bean.getClass(), CLOSE_METHOD_NAME) ||ClassUtils.hasMethod(bean.getClass(), SHUTDOWN_METHOD_NAME));}return StringUtils.hasLength(destroyMethodName);}如果使用 @Service 产生 Bean,则上述代码获取的 destroyMethodName 是 null 使用 @Bean,默认值为 AbstractBeanDefinition.INFER_METHOD,参考 Bean 定义:public @interface Bean {//省略其他非关键代码 String destroyMethod() default AbstractBeanDefinition.INFER_METHOD;}总结
DefaultListableBeanFactory 类是 Spring Bean 的灵魂,核心就是其 doCreateBean,掌控了 Bean 实例的创建、Bean 对象依赖的注入、定制类初始化方法的回调以及 Disposable 方法的注册等关键节点。
评论