1.問題說明
在日常的業務開發中,有時會利用@PostConstruct
在容器啟動時執行一些任務。例如:
@PostConstruct
public void init(){System.out.println("service 初始化...............");
}
一般情況這沒什么問題,但最近一個同事在做一個數據結轉的任務中使用這個注解進行測試的時候卻出現了問題,大概的偽代碼如下:
@Component
public class TreePrune{@PostConstructpublic void init() {System.out.println("初始化開始...............");CompletableFuture<Void> voidCompletableFuture = CompletableFuture.runAsync(this::process);try {voidCompletableFuture.get();} catch (Exception e) {throw new RuntimeException(e);}System.out.println("初始化成功...............");
}private void process() {SpringContextHolder.getBean(Tree.class).test(null);}
}@Component
public class Tree {public TreeNode test(TreeNode root) {System.out.println("測試Tree");return root;}
}
啟動項目,控制臺輸出:
"初始化成功...............
控制臺并沒有繼續輸出測試Tree
和初始化成功...............
這兩句,看起來程序似乎處于中止的狀態,沒有繼續向下執行。
為了查看線程的執行狀態,使用jstack -l pid
命令打印堆棧,查看輸出的日志,發現線程確實處于BLOCKED
狀態,而且仔細看堆棧信息的話可以發現是在執行DefaultSingletonBeanRegistry.getSingleton
方法時等待獲取monitor
鎖。
我們先找到相關源碼,Spring的版本是5.2.11.RELEASE
,在org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:179)
protected Object getSingleton(String beanName, boolean allowEarlyReference) {// Quick check for existing instance without full singleton lockObject singletonObject = this.singletonObjects.get(beanName);if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {singletonObject = this.earlySingletonObjects.get(beanName);if (singletonObject == null && allowEarlyReference) {// singletonObjects就是一個ConcurrentHashMapsynchronized (this.singletonObjects) {// Consistent creation of early reference within full singleton locksingletonObject = this.singletonObjects.get(beanName);if (singletonObject == null) {singletonObject = this.earlySingletonObjects.get(beanName);if (singletonObject == null) {ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);if (singletonFactory != null) {singletonObject = singletonFactory.getObject();this.earlySingletonObjects.put(beanName, singletonObject);this.singletonFactories.remove(beanName);}}}}}}return singletonObject;}
對Spring創建bean
相關源碼有一定了解的同學應該對這個方法比較熟悉,Spring在創建bean
的時候會先嘗試從一級緩存里獲取,如果獲取到直接返回,如果沒有獲取到會先獲取鎖然后繼續嘗試從二級緩存、三級緩存中獲取。CompletableFuture
里執行任務的線程在獲取singletonObjects
對象的monitor
鎖時被阻塞了也就是說有其它線程已經提前獲取了這個鎖并且沒有釋放。根據鎖對象的地址0x00000005c4e76198
在日志中搜索,果然有發現。
可以看到持有對象0x00000005c4e76198
的monitor
鎖的線程就是main
線程,也就是Springboot項目啟動的主線程,也就是執行被@PostConstruct
修飾的init
方法的線程,同時main
線程在執行get
方法等待獲取任務執行結果時切換為WAITING
狀態。看堆棧的話,main
線程是在啟動時創建TreePrune
對象時獲取的鎖,相關源碼如下,在org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:222)
:
public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {Assert.notNull(beanName, "Bean name must not be null");// 獲取singletonObjects的monitor鎖synchronized (this.singletonObjects) {Object singletonObject = this.singletonObjects.get(beanName);if (singletonObject == null) {......beforeSingletonCreation(beanName);boolean newSingleton = false;boolean recordSuppressedExceptions = (this.suppressedExceptions == null);if (recordSuppressedExceptions) {this.suppressedExceptions = new LinkedHashSet<>();}try {// 創建對象,后續會執行到TreePrune類中的init方法singletonObject = singletonFactory.getObject();newSingleton = true;}.......if (newSingleton) {addSingleton(beanName, singletonObject);}}return singletonObject;}}
因此,整個流程就是main
線程在創建TreePrune
對象時,先獲取singletonObjects
的monitor
鎖然后執行到init
方法,在init
方法里異步開啟CompletableFuture
任務,使用get
方法獲取任務結果,在結果返回之前main
線程處于WAITING
狀態,并且不釋放鎖。與此同時CompletableFuture
內的異步線程從容器中獲取bean
也需要獲取singletonObjects
的monitor
鎖,由于main
線程不釋放鎖,CompletableFuture
內的異步線程一直處于BLOCKED
狀態無法返回結果,get
方法也就一直處于WAITING
狀態,形成了一個類似死鎖的局面。
tips:分析stack文件的時候,有一個比較好用的在線工具Online Java Thread Dump Analyzer,它能比較直觀的展示鎖被哪個線程獲取,哪個線程又在等待獲取鎖。
2.問題解決
根據上面的分析解決辦法也很簡單,既然問題是由于main
線程在獲取鎖后一直不釋放導致的,而沒有釋放鎖主要是因為一直在get
方法處等待,那么只需要從get
方法入手即可。
-
方法一,如果業務允許,干脆不調用
get
方法獲取結果; -
方法二,
get
方法添加等待超時時間,這樣其實也無法獲取到異步任務執行結果:voidCompletableFuture.get(1000L)
-
方法三,
get
方法放在異步線程執行:new Thread(){@Overridepublic void run(){try {voidCompletableFuture.get();} catch (Exception e) {throw new RuntimeException(e);} }}.start();
-
方法四,
CompletableFuture
里的異步任務改為同步執行@PostConstruct public void init() {System.out.println("初始化開始...............");process();System.out.println("初始化成功..............."); }
單純就上面這個偽代碼例子來說,除了上面幾種方法,其實還有一種方法也可以解決,那就是修改process
方法,將手動從容器中獲取tree
改為自動注入,至于原因將在后文進行分析,可以提示一下與@PostConstruct
執行的時機有關。前面的例子之所以要寫成手動從容器獲取是因為原始代碼process
方法里是調用Mapper
對象操作數據庫,為了復現問題做了類似的處理。
@Component
public class TreePrune{@AutowiredTree tree;@PostConstructpublic void init() {System.out.println("初始化開始...............");CompletableFuture<Void> voidCompletableFuture = CompletableFuture.runAsync(this::process);try {voidCompletableFuture.get();} catch (Exception e) {throw new RuntimeException(e);}System.out.println("初始化成功...............");
}private void process() {tree.test(null);}
}@Component
public class Tree {public TreeNode test(TreeNode root) {System.out.println("測試Tree");return root;}
}
3.問題拓展
問題看起來是解決了,但對于問題形成的根本原因以及@PostConstruct
的原理還沒有過多的講解,下面就簡單介紹下。
@PostConstruct
注解是在javax.annotation
包下的,也就是java拓展包定義的注解,并不是Spring定義的,但Spring對它的功能做了實現。與之類似的還有@PreDestroy
、@Resource
等注解。
package javax.annotation;
....
@Documented
@Retention (RUNTIME)
@Target(METHOD)
public @interface PostConstruct {
}
Spring提供了一個CommonAnnotationBeanPostProcessor
來處理這幾個注解,看名字就知道這是一個bean
的后置處理器,它能介入bean
創建過程。
public CommonAnnotationBeanPostProcessor() {setOrder(Ordered.LOWEST_PRECEDENCE - 3);setInitAnnotationType(PostConstruct.class);setDestroyAnnotationType(PreDestroy.class);ignoreResourceType("javax.xml.ws.WebServiceContext");}
這個后置處理器會在容器啟動時進行注冊
// Check for JSR-250 support, and if present add the CommonAnnotationBeanPostProcessor.if (jsr250Present && !registry.containsBeanDefinition(COMMON_ANNOTATION_PROCESSOR_BEAN_NAME)) {RootBeanDefinition def = new RootBeanDefinition(CommonAnnotationBeanPostProcessor.class);def.setSource(source);beanDefs.add(registerPostProcessor(registry, def, COMMON_ANNOTATION_PROCESSOR_BEAN_NAME));}
首先我們看Spring創建bean
的一個核心方法,只保留一些核心的代碼,源碼在org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBeann(AbstractAutowireCapableBeanFactory.java:547)。
protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)throws BeanCreationException {// Instantiate the bean.BeanWrapper instanceWrapper = null;.....if (instanceWrapper == null) {// 創建對象instanceWrapper = createBeanInstance(beanName, mbd, args);}Object bean = instanceWrapper.getWrappedInstance();Class<?> beanType = instanceWrapper.getWrappedClass();if (beanType != NullBean.class) {mbd.resolvedTargetType = beanType;}....// Initialize the bean instance.Object exposedObject = bean;try {// 注入屬性populateBean(beanName, mbd, instanceWrapper);// 初始化exposedObject = initializeBean(beanName, exposedObject, mbd);}......return exposedObject;}
我們主要看初始化的initializeBean
方法
protected Object initializeBean(String beanName, Object bean, @Nullable RootBeanDefinition mbd) {if (System.getSecurityManager() != null) {AccessController.doPrivileged((PrivilegedAction<Object>) () -> {invokeAwareMethods(beanName, bean);return null;}, getAccessControlContext());}else {// 處理Aware接口invokeAwareMethods(beanName, bean);}Object wrappedBean = bean;if (mbd == null || !mbd.isSynthetic()) {//后置處理器的before方法wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);}try {//處理InitializingBean和init-methodinvokeInitMethods(beanName, wrappedBean, mbd);}catch (Throwable ex) {throw new BeanCreationException((mbd != null ? mbd.getResourceDescription() : null),beanName, "Invocation of init method failed", ex);}if (mbd == null || !mbd.isSynthetic()) {//后置處理器的after方法wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);}return wrappedBean;}@Overridepublic Object applyBeanPostProcessorsBeforeInitialization(Object existingBean, String beanName)throws BeansException {Object result = existingBean;//遍歷所有的后置處理器然后執行它的postProcessBeforeInitializationfor (BeanPostProcessor processor : getBeanPostProcessors()) {Object current = processor.postProcessBeforeInitialization(result, beanName);if (current == null) {return result;}result = current;}return result;}protected void invokeInitMethods(String beanName, Object bean, @Nullable RootBeanDefinition mbd)throws Throwable {boolean isInitializingBean = (bean instanceof InitializingBean);if (isInitializingBean && (mbd == null || !mbd.isExternallyManagedInitMethod("afterPropertiesSet"))) {if (logger.isTraceEnabled()) {logger.trace("Invoking afterPropertiesSet() on bean with name '" + beanName + "'");}if (System.getSecurityManager() != null) {try {AccessController.doPrivileged((PrivilegedExceptionAction<Object>) () -> {((InitializingBean) bean).afterPropertiesSet();return null;}, getAccessControlContext());}catch (PrivilegedActionException pae) {throw pae.getException();}}else {// 處理處理InitializingBean((InitializingBean) bean).afterPropertiesSet();}}if (mbd != null && bean.getClass() != NullBean.class) {String initMethodName = mbd.getInitMethodName();if (StringUtils.hasLength(initMethodName) &&!(isInitializingBean && "afterPropertiesSet".equals(initMethodName)) &&!mbd.isExternallyManagedInitMethod(initMethodName)) {// 處理init-method方法invokeCustomInitMethod(beanName, bean, mbd);}}}
在applyBeanPostProcessorsBeforeInitialization
方法里會遍歷所有的后置處理器然后執行它的postProcessBeforeInitialization
,前面說的CommonAnnotationBeanPostProcessor
類繼承了InitDestroyAnnotationBeanPostProcessor
,所以執行的是下面這個方法。
@Overridepublic Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {// 查找@PostConstruct、@PreDestroy注解修飾的方法LifecycleMetadata metadata = findLifecycleMetadata(bean.getClass());try {// 通過反射調用metadata.invokeInitMethods(bean, beanName);}catch (InvocationTargetException ex) {throw new BeanCreationException(beanName, "Invocation of init method failed", ex.getTargetException());}catch (Throwable ex) {throw new BeanCreationException(beanName, "Failed to invoke init method", ex);}return bean;}private LifecycleMetadata findLifecycleMetadata(Class<?> clazz) {if (this.lifecycleMetadataCache == null) {return buildLifecycleMetadata(clazz);}// 從緩存里獲取LifecycleMetadata metadata = this.lifecycleMetadataCache.get(clazz);if (metadata == null) {synchronized (this.lifecycleMetadataCache) {metadata = this.lifecycleMetadataCache.get(clazz);if (metadata == null) {// 沒有去創建metadata = buildLifecycleMetadata(clazz);this.lifecycleMetadataCache.put(clazz, metadata);}return metadata;}}return metadata;}
在buildLifecycleMetadata
方法里,會通過反射去獲取方法上有initAnnotationType
和destroyAnnotationType
類型方法,而initAnnotationType
和destroyAnnotationType
的值就是前面創建CommonAnnotationBeanPostProcessor
的構造方法里賦值的,也就是PostConstruct.class
和PreDestroy.class
。
private LifecycleMetadata buildLifecycleMetadata(final Class<?> clazz) {if (!AnnotationUtils.isCandidateClass(clazz, Arrays.asList(this.initAnnotationType, this.destroyAnnotationType))) {return this.emptyLifecycleMetadata;}List<LifecycleElement> initMethods = new ArrayList<>();List<LifecycleElement> destroyMethods = new ArrayList<>();Class<?> targetClass = clazz;do {final List<LifecycleElement> currInitMethods = new ArrayList<>();final List<LifecycleElement> currDestroyMethods = new ArrayList<>();ReflectionUtils.doWithLocalMethods(targetClass, method -> {//initAnnotationType就是PostConstruct.classif (this.initAnnotationType != null && method.isAnnotationPresent(this.initAnnotationType)) {LifecycleElement element = new LifecycleElement(method);currInitMethods.add(element);}//destroyAnnotationType就是PreDestroy.classif (this.destroyAnnotationType != null && method.isAnnotationPresent(this.destroyAnnotationType)) {currDestroyMethods.add(new LifecycleElement(method));}});initMethods.addAll(0, currInitMethods);destroyMethods.addAll(currDestroyMethods);targetClass = targetClass.getSuperclass();}while (targetClass != null && targetClass != Object.class);return (initMethods.isEmpty() && destroyMethods.isEmpty() ? this.emptyLifecycleMetadata :new LifecycleMetadata(clazz, initMethods, destroyMethods));}
獲取到方法上有initAnnotationType
和destroyAnnotationType
類型方法后,后續就是通過反射進行調用,就不贅述了。完整的流程其實還是相對比較簡單的,下面有個大致的流程圖,感興趣的同學可以自己打個斷點跟著走一走。
根據源碼的執行流程我們可以知道,在一個bean
創建的過程中@PostConstruct
的執行在屬性注入populateBean
方法之后的initializeBean
方法即初始化bean
的方法中。現在你知道為什么我們前面說將process
方法中手動從容器中獲取tree
改為自動注入也可以解決問題了嗎?
改為自動注入后獲取tree
對象就是在populateBean
方法中執行,也就是說是main
線程在執行,當它嘗試去獲取singletonObjects
的monitor
鎖時,由于Sychronized
是可重入鎖,它不會被阻塞,等執行到CompletableFuture
的異步任務時,由于并不需要去容器中獲取bean
,也就不會嘗試去獲取singletonObjects
的monitor
鎖,即不會被阻塞,那么get
方法自然就能獲取到結果,程序也就能正常的執行下去。
此外,通過源碼我們也可以知道在Bean初始化的執行三種常見方法的執行順序,即
1.注解@PostConstruct
2.InitializingBean
接口的afterPropertiesSet
方法
3.<bean>
或者@Bean
注入bean,它的init-method
的屬性
4.結論
通過上述的分析,可以做幾個簡單的結論:
1.@PostConstruct
修飾的方法是在bean
初始化的時候執行,并且相比其它初始化方法,它們的順序是@PostConstruct
> InitializingBean
> init-method
2.不要在@PostConstruct
中執行耗時任務,它會影響程序的啟動速度,如果實在有這樣的需求可以考慮異步執行或者使用定時任務。
3.程序中如果有類似future.get
獲取線程執行結果的代碼,盡量使用有超時時間的get
方法。
參考:Spring 框架中 @PostConstruct 注解詳解