@PostConstruct虽好,请勿乱用

1.问题说明

在日常的业务开发中,有时会利用@PostConstruct在容器启动时执行一些任务。例如:

@PostConstruct
public void init(){
    System.out.println("service 初始化...............");
}

一般情况这没什么问题,但最近一个同事在做一个数据结转的任务中使用这个注解进行测试的时候却出现了问题,大概的伪代码如下:

@Component
public class TreePrune{
	@PostConstruct
	public 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 lock
		Object singletonObject = this.singletonObjects.get(beanName);
		if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
			singletonObject = this.earlySingletonObjects.get(beanName);
			if (singletonObject == null && allowEarlyReference) {
				// singletonObjects就是一个ConcurrentHashMap
				synchronized (this.singletonObjects) {
					// Consistent creation of early reference within full singleton lock
					singletonObject = 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在日志中搜索,果然有发现。

在这里插入图片描述

可以看到持有对象0x00000005c4e76198monitor锁的线程就是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对象时,先获取singletonObjectsmonitor锁然后执行到init方法,在init方法里异步开启CompletableFuture任务,使用get方法获取任务结果,在结果返回之前main线程处于WAITING状态,并且不释放锁。与此同时CompletableFuture内的异步线程从容器中获取bean也需要获取singletonObjectsmonitor锁,由于main线程不释放锁,CompletableFuture内的异步线程一直处于BLOCKED状态无法返回结果,get方法也就一直处于WAITING状态,形成了一个类似死锁的局面。

tips:分析stack文件的时候,有一个比较好用的在线工具Online Java Thread Dump Analyzer,它能比较直观的展示锁被哪个线程获取,哪个线程又在等待获取锁。

在这里插入图片描述

2.问题解决

根据上面的分析解决办法也很简单,既然问题是由于main线程在获取锁后一直不释放导致的,而没有释放锁主要是因为一直在get方法处等待,那么只需要从get方法入手即可。

  • 方法一,如果业务允许,干脆不调用get方法获取结果;

  • 方法二,get方法添加等待超时时间,这样其实也无法获取到异步任务执行结果:

    voidCompletableFuture.get(1000L)
    
  • 方法三,get方法放在异步线程执行:

        new Thread(){
            @Override
            public 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{
    
    @Autowired
    Tree tree;
    
	@PostConstruct
	public 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-method
			invokeInitMethods(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;
	}

	@Override
	public Object applyBeanPostProcessorsBeforeInitialization(Object existingBean, String beanName)
			throws BeansException {

		Object result = existingBean;
        //遍历所有的后置处理器然后执行它的postProcessBeforeInitialization
		for (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,所以执行的是下面这个方法。

	@Override
	public 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方法里,会通过反射去获取方法上有initAnnotationTypedestroyAnnotationType类型方法,而initAnnotationTypedestroyAnnotationType的值就是前面创建CommonAnnotationBeanPostProcessor的构造方法里赋值的,也就是PostConstruct.classPreDestroy.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.class
				if (this.initAnnotationType != null && method.isAnnotationPresent(this.initAnnotationType)) {
					LifecycleElement element = new LifecycleElement(method);
					currInitMethods.add(element);
				}
                //destroyAnnotationType就是PreDestroy.class
				if (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));
	}

获取到方法上有initAnnotationTypedestroyAnnotationType类型方法后,后续就是通过反射进行调用,就不赘述了。完整的流程其实还是相对比较简单的,下面有个大致的流程图,感兴趣的同学可以自己打个断点跟着走一走。

在这里插入图片描述

根据源码的执行流程我们可以知道,在一个bean 创建的过程中@PostConstruct的执行在属性注入populateBean方法之后的initializeBean方法即初始化bean的方法中。现在你知道为什么我们前面说将process方法中手动从容器中获取tree改为自动注入也可以解决问题了吗?

改为自动注入后获取tree对象就是在populateBean方法中执行,也就是说是main线程在执行,当它尝试去获取singletonObjectsmonitor锁时,由于Sychronized是可重入锁,它不会被阻塞,等执行到CompletableFuture的异步任务时,由于并不需要去容器中获取bean,也就不会尝试去获取singletonObjectsmonitor锁,即不会被阻塞,那么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 注解详解

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值