专栏首页BAT的乌托邦【小家Spring】注意BeanPostProcessor启动时对依赖Bean的“误伤”陷阱(is not eligible for getting processed by all...)

【小家Spring】注意BeanPostProcessor启动时对依赖Bean的“误伤”陷阱(is not eligible for getting processed by all...)

前言

本篇博文和Spring的上下文启动有较强的关联性,同时需要读者对Spring中的BeanPostProcessor有较为熟悉的了解。若之前没有接触过的同学,建议先点击一下相关阅读的文章列表,先对Spring容器有个大致的了解会效果更佳~

这是曾发生在我原公司工作中的一个Spring项目的真实场景案例:简单的描述就是在使用Spring整合@Async、security的时候,出现一个诡异的现象:我把security整合进后原来的@Async就木有生效了,但是如果不把security集成进来的话,就能正常work

当时还以为是spring-security的问题,甚至以为是它的bug,现在想起来确实是自己当初图样图森破,切忌不要轻易下结论啊~

其实当初我也没找到根本原因,而是通过另外一种集成方式绕过了就继续撸码了。但是我心里一直记着此事,因为我认为一个问题你不知道它根本原因的时候,它就像个定时炸弹,随时可能被引爆。介于此机会,所以此处拿出来跟大家分享分享,避免采坑哈~

记忆中唯一线索:BeanPostProcessorChecker这个后置处理器输出了一句 xxx is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)这样的日志~~~~今天突然想起,其实这就是个很大的突破口(因为这句日志一般情况下是不会输出的~)

本文就不还原当时的场景了,而是以一个模拟的场景进行讲解、定位问题最后解决问题

什么是BeanPostProcessor

BeanPostProcessor是Spring的Bean工厂中一个非常重要的钩子,允许Spring框架在新创建Bean实例时对其进行定制化修改。比如我们对Bean内容进行修改、创建代理对象等等~

BeanPostProcessor本身也是一个Bean,一般而言其实例化时机要早过普通的Bean,但是BeanPostProcessor有时也会依赖一些Bean,这就导致了一些普通Bean的实例化早于BeanPostProcessor的可能情况,由此如果使用不当,就会造成一些问题

场景模拟

现在通过我自己构造的一个场景,来模拟当时出现的问题~ 先看看只使用@Aysnc的现象:

@EnableAsync
@Configuration
public class AsyncConfig implements AsyncConfigurer {

    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // 这一步千万不能忘了,否则报错: java.lang.IllegalStateException: ThreadPoolTaskExecutor not initialized
        // 而且最好放在最上面  否则下面set方法对Executor都不会生效
        executor.initialize();

        executor.setCorePoolSize(10); //核心线程数
        executor.setMaxPoolSize(20);  //最大线程数
        executor.setQueueCapacity(1000); //队列大小
        executor.setKeepAliveSeconds(300); //线程最大空闲时间
        executor.setThreadNamePrefix("fsx-Executor-"); ////指定用于新创建的线程名称的前缀。
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); // 拒绝策略(一共四种,此处省略)
        return executor;
    }

    // 异常处理器:当然你也可以自定义的,这里我就这么简单写了~~~
    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return new SimpleAsyncUncaughtExceptionHandler();
    }
}

使用@Async

@Service
public class HelloServiceImpl implements HelloService {

	@Async
    @Override
    public Object hello() {
        log.info("当前线程:" + Thread.currentThread().getName());
        return "service hello";
    }
}

启动、测试:

16:02:58.520 [fsx-Executor-3] INFO  com.fsx.service.HelloServiceImpl - 当前线程:fsx-Executor-3

可以看到使用的是我们自定义的线程池里面的线程,并且HelloService是个Proxy代理对象了。@Async能够正常work,没毛病老铁


接下来加入我再加入一个组件:MyBeanPostProcessor

@Slf4j
@Component
public class MyBeanPostProcessor implements BeanPostProcessor, Ordered {

    @Autowired
    private ApplicationContext applicationContext;

	// 目的:在此BeanPostProcessor初始化的时候,提前把HelloServiceImpl给初始化掉~
	// 通过这种方式来模拟:我们的BeanProcessor需要依赖业务的service、dao等情况~
    @PostConstruct
    public void init() {
        HelloService helloService = applicationContext.getBean(HelloService.class);
        System.out.println(helloService.getClass());
    }

    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;
    }
}

或者这么写,直接使用@Autowired注入属性~~~

@Slf4j
@Component
public class MyBeanPostProcessor implements BeanPostProcessor, Ordered {

    @Autowired
    private ApplicationContext applicationContext;
    @Autowired
    private HelloService helloService;

    @PostConstruct
    public void init() {
        System.out.println(helloService.getClass());
    }

    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;
    }
}

最终启动测试,输出为:

启动输出:
...
o.s.c.s.PostProcessorRegistrationDelegate$BeanPostProcessorChecker - Bean 'helloServiceImpl' of type [com.fsx.service.HelloServiceImpl] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
class com.fsx.service.HelloServiceImpl
...

发现启动的时候,输出了BeanPostProcessorChecker这个Bean检查的处理器的日志 而且,而且我们的helloService这个Bean不再是Proxy代理对象了~

再次请求执行目标方法看看:

14:34:50.164 [http-nio-8080-exec-3] INFO  com.fsx.service.HelloServiceImpl - 当前线程:http-nio-8080-exec-3

应该能猜到了,它已经不是在我们的异步线程池里面执行了,很显然@Aysnc此时就没有再生效了

导致这个现象的原因:就是我们在开发过程中,因为不清楚Spring容器对BeanPostProcessor、Bean的装载顺序,从而导致有时候我们需要提前用到Bean的功能,从而导致启动时的"误伤"

关于BeanPostProcessor的加载顺序

可能有的人会有疑问,为什么你这里(MyBeanPostProcessor)能够直接@Autowired,但是我这里为什么得到的是Null呢? 其实这里面是有文章可寻的,那就是BeanPostProcessor的加载顺序:

【小家Spring】Spring IOC容器启动流程 AbstractApplicationContext#refresh()方法源码分析(一) 【小家Spring】Spring IOC容器启动流程 AbstractApplicationContext#refresh()方法源码分析(二),Spring容器启动/刷新的完整总结

Spring容器启动过程,从向容器注册BeanPostProcessor这一步开始说明:

registerBeanPostProcessors(beanFactory);
	public static void registerBeanPostProcessors(ConfigurableListableBeanFactory beanFactory, AbstractApplicationContext applicationContext) {
	
		// 注意:此处只会拿到Bean的定义信息~~~~
		// 已经被实例化的Bean最终都会调用`beanFactory.addBeanPostProcessor`而缓存在AbstractBeanFactory的字段:beanPostProcessors里,它是个CopyOnWriteArrayList
		// 更重要的是:最终最终所有的BeanPostProcessor的执行都会从这个List里面拿出来执行
		// 所以这一步很关键:那就是按照顺序,把`BeanPostProcessor`们都实例化好,然后添加进List里
		// 因此顺序是关键~~~~~如果某些Bean提前被实例化,它就很有可能不能被所有的`BeanPostProcessor`处理到了
		// 这也是我们BeanPostProcessorChecker的作用,它就是检查这个然后输出日志的~
		String[] postProcessorNames = beanFactory.getBeanNamesForType(BeanPostProcessor.class, true, false);


		// 这个beanProcessorTargetCount此处赋值了,后续就都不会变了,BeanPostProcessorChecker就是和这个进行比较的~
		// beanFactory里面的Bean实例总个数+1(自己)+bean定义信息~
		int beanProcessorTargetCount = beanFactory.getBeanPostProcessorCount() + 1 + postProcessorNames.length;
		// 把BeanPostProcessorChecker加进去,它其实就是做了一个检查而已~~~~~~~输出一个info日志~
		beanFactory.addBeanPostProcessor(new BeanPostProcessorChecker(beanFactory, beanProcessorTargetCount));
		
		// 1、找到所有实现PriorityOrdered的`BeanPostProcessor`,然后getBean,然后统一排序,然后beanFactory.addBeanPostProcessor()
		// 2、处理实现Ordered的,步骤同上
		// 3、处理没实现排序接口的普通的处理器,不需要sort了,直接add进去~
	
		// 最后注册一个特殊的处理器
		beanFactory.addBeanPostProcessor(new ApplicationListenerDetector(applicationContext));
	}

掌握这个顺序,是我们后续解释上诉现象的根本基础。

采用@Autowired注入ApplicationContext和实现接口ApplicationContextAware的区别

在绝大多数情况下,这两种使用方式是等价的,都能够方便的获取到Spring容器上下文:ApplicationContext,但是在某些情况下,是有区别的,比如在如下情况下:

// 和上面唯一区别是 此处实现的是`PriorityOrdered`接口,上面就是普通的Ordered接口
@Slf4j
@Component
public class MyBeanPostProcessor implements BeanPostProcessor, PriorityOrdered{
	@Autowired
    private ApplicationContext applicationContext;
    ...
}

这样子注入,最终applicationContext的值为null。为何呢?其实这就和BeanPostProcessor的加载时机以及@Autowired的执行时机有关,下面通过两张截图可以清晰的看到顺序: 此图表示执行到此处,容器内已经存在的BeanPostProcessor实例(注意已经是实例):

此图表示BeanPostProcessor的定义信息们,他们等待被实例化:

下面对此几个BeanPostProcessor的Bean定义信息做分析如下:

  • AutowiredAnnotationBeanPostProcessor它实现了PriorityOrdered接口
  • CommonAnnotationBeanPostProcessor它也实现了PriorityOrdered接口
  • AsyncAnnotationBeanPostProcessor没有实现任何Orderd排序接口
  • MyBeanPostProcessor此处我们让它实现了PriorityOrdered接口===========

从上面可以看到,因为实现了PriorityOrdered接口的BeanPostProcessor属于于同一级别,都是先统一调用getBean()实例化后被统一addBeanPostProcessor

因此AutowiredAnnotationBeanPostProcessor这个后置处理器并不能作用在我们的MyBeanPostProcessor上面给我们的属性赋值(因为他俩是同一级别的),因为根本就木有生效嘛~~~~但是像我们第一个例子,MyBeanPostProcessor只是实现了普通的Ordered接口,优先级比PriorityOrdered低,所以就实现了正常的注入(因为高优先级的处理器先辈实例化了,所以可以作用于低优先级的bean了)~

由上可知,我们在注册BeanPostProcessor的时候,他们的优先级的层级原则是需要注意的:高优先级的Bean能够作用于低有衔接的,反之不成立。但是同优先级的Bean不能相互作用~

若是实现ApplicationContextAware接口的话,ApplicationContext不管咋样都可以被正常获取到。道理也是一样的,是因为这个接口是被ApplicationContextAwareProcessor来解析的,而它已经早早被放进了Spring容器里面,所以通过实现接口的方式任何时候都是阔仪的

那平时到底使用@Autowired注入还是实现接口ApplicationContextAware呢?

建议平时能够使用@Autowired注入时(我更建议使用构造器注入,这样完全不与Spring容器耦合),就尽量使用注入的方式。而不是去实现实现ApplicationContextAware接口的方式,因为这种方式属于与Spring容器强耦合的方式。(当然非常特殊情况下,只能使用ApplicationContextAware,比如上面那个情况)

@Async异步注解失效得原因

相信到了此处,@Async失效的原因已经不用再详细阐述一遍了。简单的说就是因为HelloService该Bean被提前初始化了,而这个时候AsyncAnnotationBeanPostProcessor根本就还没起作用(因为它仅仅是一个普通的BeanPostProcessor,加载是靠后的),所以肯定也就不能扫描到@Async这种`注解方法,从而就不能生成代理对象,那就自然而然就失效了~

想说明的是,本文说明的是一类问题,而不是@Async这一个问题,请大家能够举一反三

关于BeanPostProcessorChecker

首先它是一个BeanPostProcessor,是PostProcessorRegistrationDelegate的一个private static内部类,它的作用就是在postProcessAfterInitialization里检查作用在此Bean上的BeanPostProcessor够不够数,如果不够数(一般是提前加载了,或者被auto-proxying了这种情况就输出一条info日志~~~)

	private static final class BeanPostProcessorChecker implements BeanPostProcessor {

		private static final Log logger = LogFactory.getLog(BeanPostProcessorChecker.class);

		// 从if条件可以看出哪些是不需要检测的Bean
		// 1、BeanPostProcessor类型不检测
		// 2、ROLE_INFRASTRUCTURE这种类型的Bean不检测 (Spring自己的Bean)
		@Override
		public Object postProcessAfterInitialization(Object bean, String beanName) {
			if (!(bean instanceof BeanPostProcessor) && !isInfrastructureBean(beanName) &&
					this.beanFactory.getBeanPostProcessorCount() < this.beanPostProcessorTargetCount) {
				if (logger.isInfoEnabled()) {
					logger.info("Bean '" + beanName + "' of type [" + bean.getClass().getName() +
							"] is not eligible for getting processed by all BeanPostProcessors " +
							"(for example: not eligible for auto-proxying)");
				}
			}
			return bean;
		}

		private boolean isInfrastructureBean(@Nullable String beanName) {
			if (beanName != null && this.beanFactory.containsBeanDefinition(beanName)) {
				BeanDefinition bd = this.beanFactory.getBeanDefinition(beanName);
				return (bd.getRole() == RootBeanDefinition.ROLE_INFRASTRUCTURE);
			}
			return false;
		}
	}

如果你的bean被提前加载了,那就会看到这么一条日志:

Bean 'XXX' of type [XXXX] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)

比如我们项目中使用到了Feign面向接口进行远程调用,每次项目启动的时候,就会输出大量的类似日志,如下图:

Tips: 一般的如果你的Config类是一个XXXConfigurer的扩展配置类,也会打印类似的消息:

o.s.c.s.PostProcessorRegistrationDelegate$BeanPostProcessorChecker - Bean 'asyncConfig' of type [com.config.AsyncConfig$$EnhancerBySpringCGLIB$$d4dbccee] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)

根本原因也是上面说的。具体到代码这里简单分析一下(此处以AsyncConfigurer为例):

// 在对它(AsyncAnnotationBeanPostProcessor)进行实例化的时候(它是个BeanPostProcessor,所以会根据bean定义进行getBean())
// 所以就必须先实例化它@Bean所在的类:
@Configuration
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class ProxyAsyncConfiguration extends AbstractAsyncConfiguration {
	@Bean(name = TaskManagementConfigUtils.ASYNC_ANNOTATION_PROCESSOR_BEAN_NAME)
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	public AsyncAnnotationBeanPostProcessor asyncAdvisor() { ... }
}

// 然后它的父类`AbstractAsyncConfiguration`里有个@Autowired方法:
@Configuration
public abstract class AbstractAsyncConfiguration implements ImportAware {
	@Autowired(required = false)
	void setConfigurers(Collection<AsyncConfigurer> configurers) { ... }
}
// 这个依赖注入就会去容器内找所有的`AsyncConfigurer`的实现,所以就把`AsyncConfig`给提前实例化了
// 所以打印了那么一句日志,不要感觉到惊奇。这也是为何spring这个checker里使用的日志级别是Info,而不是debug,更不是worn。
// 因为它Spring认为这个debug太轻了,但是warn又太重了,因为绝大部分情况下它都不影响程序的正常work~

注意避免BeanPostProcessor启动时对依赖的Bean造成误伤

BeanPostProcessor实例化时,自动依赖注入根据类型获得需要注入的Bean时,会将某些符合条件的Bean先实例化,如果此FacotryBean又依赖其他普通Bean,会导致该Bean提前启动,造成"误伤"(无法享受部分BeanPostProcessor的后置处理,例如典型的auto-proxy)。

本文参与 腾讯云自媒体分享计划 ,欢迎热爱写作的你一起参与!
本文分享自作者个人站点/博客:https://blog.csdn.net/f641385712复制
如有侵权,请联系 cloudcommunity@tencent.com 删除。
登录 后参与评论
0 条评论

相关文章

  • Spring Framework 源码学习笔记(四)

    alt+command+b进入AnnotationConfigApplication类中,调用过程为:

    RiemannHypothesis
  • 排查not eligible for getting processed by all BeanPostProcessors

    上一篇文章我们聊了一下自定义实现的SPI如何与spring进行整合,但其实在实现的过程中有个小细节,就是原先我们的SPI是带了拦截器功能,(ps:对如何实现一个...

    lyb-geek
  • 排查not eligible for getting processed by all BeanPostProcessors

    上一篇文章我们聊了一下[自定义实现的SPI如何与spring进行整合](https://mp.weixin.qq.com/s?__biz=MzI1MTY1Njk...

    lyb-geek
  • spring 注册Bean后置处理器

    参考spring4.2.9 java项目环境下ioc源码分析 (十一)——refresh之registerBeanPostProcessors方法

    平凡的学生族
  • Spring依赖注入@Autowired深层原理、源码级分析,感受DI带来的编程之美【享学Spring】

    关于Spring IOC的依赖注入(DI机制),之前虽有过分析,但总感觉一直落了一块:对@Autowired注解元数据的解析部分。

    YourBatman
  • 使用@Async异步注解导致该Bean在循环依赖时启动报BeanCurrentlyInCreationException异常的根本原因分析,以及提供解决方案【享学Spring】

    今天在自己工程中使用@Async的时候,碰到了一个问题:Spring循环依赖(circular reference)问题。 或许刚说到这,有的小伙伴就会大惊失...

    YourBatman
  • static关键字真能提高Bean的优先级吗?答:真能

    各位小伙伴大家好,我是A哥。关于Spring初始化Bean的顺序问题,是个老生常谈的话题了,结论可总结为一句话:全局无序,局部有序。Spring Bean整体上...

    YourBatman
  • static关键字真能提高Bean的优先级吗?答:真能

    各位小伙伴大家好,我是A哥。关于Spring初始化Bean的顺序问题,是个老生常谈的话题了,结论可总结为一句话:全局无序,局部有序。Spring Bean整体上...

    YourBatman
  • 【小家Spring】Spring IOC容器启动流程 AbstractApplicationContext#refresh()方法源码分析(二),Spring容器启动/刷新的完整总结

    在上一篇文章:【小家Spring】Spring IOC容器启动流程 AbstractApplicationContext#refresh()方法源码分析(一)中...

    YourBatman
  • Spring框架参考手册_5.0.0_中英文对照版_Part II_3.8

    文章作者:Tyan 博客:noahsnail.com  |  CSDN  |  简书

    Tyan
  • Spring官网阅读(八)容器的扩展点(三)(BeanPostProcessor)

    *从上面的执行结果我们可以得出一个结论,BeanPostProcessor接口中的两个方法的执行时机在属性注入之后。*因为从打印的结果我们可以发现,IndexS...

    程序员DMZ
  • Spring官网阅读系列(八):容器的扩展点(BeanPostProcessor)

    从上面的执行结果我们可以得出一个结论,BeanPostProcessor接口中的两个方法的执行时机在属性注入之后。因为从打印的结果我们可以发现,IndexSer...

    秃顶的Java程序员
  • 记录下多个BeanPostProcessor代理同个Bean的问题

    在去年研发XX项目时,需要一种字节码增强机制,用于增强HSF、Tair、TDDL等相关类,用于信息采集。当时考虑了好几种方案,也踩到了一些坑,特别是关于Spri...

    LNAmp
  • SpringAop源码分析(基于注解)一

    关于Aop的原理,简单来讲无非就是用代理模式为目标对象生产代理对象,对原有的方法进行增强。看上去挺简单,但在Spring中,有许多细节是要注意到的。比如:

    大王叫下
  • Spring拓展接口之BeanPostProcessor,我们来看看它的底层实现

        小明:“妈,我被公司开除了”,妈:“啊,为什么呀?”, 小明:“我骂董事长是笨蛋,公司召开高层会议还要起诉我”,妈:“告你诽谤是吧?”,小明:“不是,他...

    青石路
  • Spring5 源码学习 (8) refresh方法总结

    之前已经分析到了invokeBeanFactoryPostProcessors方法,现在来看一下registerBeanPostProcessors。

    Coder小黑
  • Spring读源码系列07----容器扩展功能--中

    在AbstractAutowireCapableBeanFactory#doCreateBean方法中会调用initializeBean方法来初始化bean:

    大忽悠爱学习
  • 万能的BeanPostProcessor是如何让spring无限扩展的?

    很多朋友一提到spring,首先想到的肯定是IOC(控制反转)和 AOP (面向切面编程),没错,这两个是spring的核心功能。但是什么技术让spring拥...

    苏三说技术
  • 【小家Spring】Spring异步处理@Async的使用以及原理、源码分析(@EnableAsync)

    在开发过程中,我们会遇到很多使用线程池的业务场景,例如异步短信通知、异步记录操作日志。大多数使用线程池的场景,就是会将一些可以进行异步操作的业务放在线程池中去完...

    YourBatman

扫码关注腾讯云开发者

领取腾讯云代金券