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 注解详解