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

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 【小家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));
  }


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

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
1月前
|
XML 安全 Java
|
30天前
|
存储 Java Spring
【Spring】获取Bean对象需要哪些注解
@Conntroller,@Service,@Repository,@Component,@Configuration,关于Bean对象的五个常用注解
|
30天前
|
存储 Java 应用服务中间件
【Spring】IoC和DI,控制反转,Bean对象的获取方式
IoC,DI,控制反转容器,Bean的基本常识,类注解@Controller,获取Bean对象的常用三种方式
|
1月前
|
XML Java 数据格式
Spring容器Bean之XML配置方式
通过对以上内容的掌握,开发人员可以灵活地使用Spring的XML配置方式来管理应用程序的Bean,提高代码的模块化和可维护性。
67 6
|
1月前
|
XML Java 数据格式
🌱 深入Spring的心脏:Bean配置的艺术与实践 🌟
本文深入探讨了Spring框架中Bean配置的奥秘,从基本概念到XML配置文件的使用,再到静态工厂方式实例化Bean的详细步骤,通过实际代码示例帮助读者更好地理解和应用Spring的Bean配置。希望对你的Spring开发之旅有所助益。
118 3
|
2月前
|
缓存 Java Spring
实战指南:四种调整 Spring Bean 初始化顺序的方案
本文探讨了如何调整 Spring Boot 中 Bean 的初始化顺序,以满足业务需求。文章通过四种方案进行了详细分析: 1. **方案一 (@Order)**:通过 `@Order` 注解设置 Bean 的初始化顺序,但发现 `@PostConstruct` 会影响顺序。 2. **方案二 (SmartInitializingSingleton)**:在所有单例 Bean 初始化后执行额外的初始化工作,但无法精确控制特定 Bean 的顺序。 3. **方案三 (@DependsOn)**:通过 `@DependsOn` 注解指定 Bean 之间的依赖关系,成功实现顺序控制,但耦合性较高。
104 4
实战指南:四种调整 Spring Bean 初始化顺序的方案
|
1月前
|
存储 缓存 Java
Spring面试必问:手写Spring IoC 循环依赖底层源码剖析
在Spring框架中,IoC(Inversion of Control,控制反转)是一个核心概念,它允许容器管理对象的生命周期和依赖关系。然而,在实际应用中,我们可能会遇到对象间的循环依赖问题。本文将深入探讨Spring如何解决IoC中的循环依赖问题,并通过手写源码的方式,让你对其底层原理有一个全新的认识。
66 2
|
1月前
|
安全 Java 开发者
Spring容器中的bean是线程安全的吗?
Spring容器中的bean默认为单例模式,多线程环境下若操作共享成员变量,易引发线程安全问题。Spring未对单例bean做线程安全处理,需开发者自行解决。通常,Spring bean(如Controller、Service、Dao)无状态变化,故多为线程安全。若涉及线程安全问题,可通过编码或设置bean作用域为prototype解决。
43 1
|
3月前
|
XML Java 数据格式
Spring从入门到入土(bean的一些子标签及注解的使用)
本文详细介绍了Spring框架中Bean的创建和使用,包括使用XML配置文件中的标签和注解来创建和管理Bean,以及如何通过构造器、Setter方法和属性注入来配置Bean。
96 9
Spring从入门到入土(bean的一些子标签及注解的使用)
|
3月前
|
Java 测试技术 Windows
咦!Spring容器里为什么没有我需要的Bean?
【10月更文挑战第11天】项目经理给小菜分配了一个紧急需求,小菜迅速搭建了一个SpringBoot项目并完成了开发。然而,启动测试时发现接口404,原因是控制器包不在默认扫描路径下。通过配置`@ComponentScan`的`basePackages`字段,解决了问题。总结:`@SpringBootApplication`默认只扫描当前包下的组件,需要扫描其他包时需配置`@ComponentScan`。