@Bean 注解的方法调用多次会创建多个bean 实例吗


highlight: a11y-dark

1.缘起

在看一段基于 spring security 的鉴权代码的时候,我发现一个有趣的 Bean 声明和方法调用。在一个 @Configuration 注解的配置类中用 @Bean 注解了一个方法 tokenStore,声明了 Spring bean: tokenStore。在 Spring 中,把 @Bean 注解的方法称为工厂方法,即用于创建 Spring bean 的方法。在同一个配置类另一个方法 configure 中调用了这个工厂方法 tokenStore 来获取 TokenStore 实例。

那么这会导致系统中存在多个 TokenStore 实例吗?如果是两个实例,则一个应该是 Spring bean 实例,一个是 configure 方法中通过 tokenStroe() 方法创建的实例。代码如下:

  1. @Configuration
  2. public class AuthorizationConfig {
  3. @Bean
  4. public TokenStore tokenStore() {
  5. RedisTokenStore redis = new RedisTokenStore(connectionFactory);
  6. return redis;
  7. }
  8. @Override
  9. public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
  10. /*使用oauth2的密码模式时需要配置authenticationManager*/
  11. endpoints.authenticationManager(authenticationManager);
  12. //直接调用工厂方法获取 TokenStore 实例
  13. endpoints.tokenStore(tokenStore())
  14. ...
  15. }
  16. ...
  17. }

答案是否定的,在系统中只有一个实例,这个实例就是 spring bean。这是怎么做到的呢?继续阅读之前,可以闭上眼睛思考一下。

2.原理

通过调式代码,我们可以发现这个配置类已经被 CGLIB 代理了,这个配置类的实例类名变成了 AuthorizationConfig$$EnhancerBySpringCGLIB,如图:
image.png

断点堆栈如图:
image.png
这个堆栈图从最底下向上看,配置类的 configure 方法调用本类工厂方法 tokenStore,变成了调用AuthorizationConfigEnhancerBySpringCGLIB 的tokenStore了,而这个调用被 ConfigurationClassEnhancerBeanMethodInterceptor 拦截器拦截,接着堆栈出现了我们熟悉的 Spring getBean 的调用堆栈(当 bean 不存在的时候,就会触发创建 bean)。

由此可见spring 容器通过 CGLIB 代理了配置类,调用配置类 @Bean 注解的工厂方法时,这个方法会被拦截。拦截器会通过 Spring 容器的机制去获取这个工厂方法上声明的 bean,如果这个bean 实例还不存在,Spring 容器会创建 bean 实例,而这个 bean 是通过配置类的 tokenStore 方法创建的,所以最终找到通过代理类调用到了配置类的 tokenStore 方法创建了 bean 实例。

3.机制探究

Spring 为 @Configuration 专门设计了一个 BeanFactoryPostProcessor 实现类ConfigurationClassPostProcessor,我们知道 Spring 在初始化加载 bean 的过程中,预留了 BeanFactoryPostProcessor 扩展点这个扩展点的执行时机是在 BeanFactory 初始化之后,所有的Bean定义已经被加载,但Bean的实例还没被创建(不包括 BeanFactoryPostProcessor 实例)的时候。Spring 会在这个时候调用 BeanFactoryPostProcessor 的 postProcessBeanFactory 方法。这个扩展点通常用于修改 bean 的定义,bean 的属性值等

我们来看看 ConfigurationClassPostProcessor 的 postProcessBeanFactory 方法实现(省略了很多代码,只列出关键部分,可以参考 spring 5.2 版本的源代码:

  1. /**
  2. * Prepare the Configuration classes for servicing bean requests at runtime
  3. * by replacing them with CGLIB-enhanced subclasses.
  4. * 通过 CGLIB 增强的子类来代替配置类来为 bean 请求提供支持
  5. */
  6. @Override
  7. public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
  8. int factoryId = System.identityHashCode(beanFactory);
  9. if (this.factoriesPostProcessed.contains(factoryId)) {
  10. throw new IllegalStateException(
  11. "postProcessBeanFactory already called on this post-processor against " + beanFactory);
  12. }
  13. this.factoriesPostProcessed.add(factoryId);
  14. if (!this.registriesPostProcessed.contains(factoryId)) {
  15. // BeanDefinitionRegistryPostProcessor hook apparently not supported...
  16. // Simply call processConfigurationClasses lazily at this point then.
  17. processConfigBeanDefinitions((BeanDefinitionRegistry) beanFactory);
  18. }
  19. //配置类主要的增强逻辑
  20. enhanceConfigurationClasses(beanFactory);
  21. beanFactory.addBeanPostProcessor(new ImportAwareBeanPostProcessor(beanFactory));
  22. }
  23. public void enhanceConfigurationClasses(ConfigurableListableBeanFactory beanFactory) {
  24. Map<String, AbstractBeanDefinition> configBeanDefs = new LinkedHashMap<>();
  25. for (String beanName : beanFactory.getBeanDefinitionNames()) {
  26. ...
  27. //如果配置类是 full 模式,则将配置类加入到需要增强的配置类列表中
  28. if (ConfigurationClassUtils.CONFIGURATION_CLASS_FULL.equals(configClassAttr)) {
  29. ...
  30. configBeanDefs.put(beanName, (AbstractBeanDefinition) beanDef);
  31. }
  32. }
  33. if (configBeanDefs.isEmpty()) {
  34. // nothing to enhance -> return immediately
  35. return;
  36. }
  37. //遍历需要增强的配置类列表,为每个配置类实现增强逻辑
  38. ConfigurationClassEnhancer enhancer = new ConfigurationClassEnhancer();
  39. for (Map.Entry<String, AbstractBeanDefinition> entry : configBeanDefs.entrySet()) {
  40. ...
  41. // 获取配置类的 Class 对象
  42. Class<?> configClass = beanDef.getBeanClass();
  43. // 增强实现
  44. Class<?> enhancedClass = enhancer.enhance(configClass, this.beanClassLoader);
  45. if (configClass != enhancedClass) {
  46. ...
  47. //将增强后的配置类设置到 bean 定义对象中
  48. beanDef.setBeanClass(enhancedClass);
  49. }
  50. }
  51. }

从上面关键代码,我们可以看出 Spring 会为符合条件的 full 模式的配置类实施增强。

注: Full 模式和 lite 模式

Spring 把 bean 分成两类:full 模式和 lite 模式。在 @Configuration 注解的配置类中声明的 bean 就是 full 模式的,其他的 spring bean,比如在 @Component 注解的类中声明的 bean 都是 lite 模式。也就是说通常只有 @Configuration 注解的配置类需要增强,这也是 @Configuration 注解和其他类型的组件注解的一个重要的区别。

下面我们接着探索 ConfigurationClassEnhancer 是如何实现增强的,关键代码如下:

  1. public Class<?> enhance(Class<?> configClass, @Nullable ClassLoader classLoader) {
  2. if (EnhancedConfiguration.class.isAssignableFrom(configClass)) {
  3. ...
  4. //说明已经增强过了,直接返回
  5. return configClass;
  6. }
  7. Class<?> enhancedClass = createClass(newEnhancer(configClass, classLoader));
  8. if (logger.isTraceEnabled()) {
  9. logger.trace(String.format("Successfully enhanced %s; enhanced class name is: %s",
  10. configClass.getName(), enhancedClass.getName()));
  11. }
  12. return enhancedClass;
  13. }
  14. /**
  15. * Creates a new CGLIB {@link Enhancer} instance
  16. */
  17. private Enhancer newEnhancer(Class<?> configSuperClass, @Nullable ClassLoader classLoader) {
  18. Enhancer enhancer = new Enhancer();
  19. //将配置类设置为增强结果类的父类
  20. enhancer.setSuperclass(configSuperClass);
  21. enhancer.setInterfaces(new Class<?>[] {EnhancedConfiguration.class});
  22. enhancer.setUseFactory(false);
  23. //设置增加类的命名策略,即增加 BySpringCGLIB,可以看前面调试贴图中的类名
  24. enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE);
  25. enhancer.setStrategy(new BeanFactoryAwareGeneratorStrategy(classLoader));
  26. //设置回调(拦截器)过滤器,就是说当配置类方法被调用的时候,会先执行符合过滤器条件的拦截器逻辑
  27. enhancer.setCallbackFilter(CALLBACK_FILTER);
  28. enhancer.setCallbackTypes(CALLBACK_FILTER.getCallbackTypes());
  29. return enhancer;
  30. }
  31. /**
  32. * Uses enhancer to generate a subclass of superclass,
  33. * ensuring that callbacks are registered for the new subclass.
  34. */
  35. private Class<?> createClass(Enhancer enhancer) {
  36. Class<?> subclass = enhancer.createClass();
  37. // 注册拦截器
  38. Enhancer.registerStaticCallbacks(subclass, CALLBACKS);
  39. return subclass;
  40. }

拦截器分析

增加逻辑主要是通过CGLIB enhancer 以配置类为父类创建一个代理子类,并设置了调用配置方法的时候,需要执行的拦截器。下面我们看看拦截器。

  1. class ConfigurationClassEnhancer {
  2. // The callbacks to use. Note that these callbacks must be stateless.
  3. private static final Callback[] CALLBACKS = new Callback[] {
  4. new BeanMethodInterceptor(),
  5. new BeanFactoryAwareMethodInterceptor(),
  6. NoOp.INSTANCE
  7. };

我们再来看看 BeanMethodInterceptor 的 intercept 实现逻辑:

  1. private static class BeanMethodInterceptor implements MethodInterceptor, ConditionalCallback {
  2. ...
  3. @Override
  4. @Nullable
  5. public Object intercept(Object enhancedConfigInstance, Method beanMethod, Object[] beanMethodArgs,
  6. MethodProxy cglibMethodProxy) throws Throwable {
  7. ...
  8. if (isCurrentlyInvokedFactoryMethod(beanMethod)) {
  9. return cglibMethodProxy.invokeSuper(enhancedConfigInstance, beanMethodArgs);
  10. }
  11. return resolveBeanReference(beanMethod, beanMethodArgs, beanFactory, beanName);
  12. }
  13. private Object resolveBeanReference(Method beanMethod, Object[] beanMethodArgs,
  14. ConfigurableBeanFactory beanFactory, String beanName) {
  15. // The user (i.e. not the factory) is requesting this bean through a call to
  16. // the bean method, direct or indirect. The bean may have already been marked
  17. // as in creation in certain autowiring scenarios; if so, temporarily set
  18. // the in-creation status to false in order to avoid an exception.
  19. boolean alreadyInCreation = beanFactory.isCurrentlyInCreation(beanName);
  20. try {
  21. if (alreadyInCreation) {
  22. beanFactory.setCurrentlyInCreation(beanName, false);
  23. }
  24. boolean useArgs = !ObjectUtils.isEmpty(beanMethodArgs);
  25. if (useArgs && beanFactory.isSingleton(beanName)) {
  26. // Stubbed null arguments just for reference purposes,
  27. // expecting them to be autowired for regular singleton references?
  28. // A safe assumption since @Bean singleton arguments cannot be optional...
  29. for (Object arg : beanMethodArgs) {
  30. if (arg == null) {
  31. useArgs = false;
  32. break;
  33. }
  34. }
  35. }
  36. Object beanInstance = (useArgs ? beanFactory.getBean(beanName, beanMethodArgs) :
  37. beanFactory.getBean(beanName));
  38. if (!ClassUtils.isAssignableValue(beanMethod.getReturnType(), beanInstance)) {
  39. // Detect package-protected NullBean instance through equals(null) check
  40. if (beanInstance.equals(null)) {
  41. if (logger.isDebugEnabled()) {
  42. logger.debug(String.format("@Bean method %s.%s called as bean reference " +
  43. "for type [%s] returned null bean; resolving to null value.",
  44. beanMethod.getDeclaringClass().getSimpleName(), beanMethod.getName(),
  45. beanMethod.getReturnType().getName()));
  46. }
  47. beanInstance = null;
  48. }
  49. else {
  50. String msg = String.format("@Bean method %s.%s called as bean reference " +
  51. "for type [%s] but overridden by non-compatible bean instance of type [%s].",
  52. beanMethod.getDeclaringClass().getSimpleName(), beanMethod.getName(),
  53. beanMethod.getReturnType().getName(), beanInstance.getClass().getName());
  54. try {
  55. BeanDefinition beanDefinition = beanFactory.getMergedBeanDefinition(beanName);
  56. msg += " Overriding bean of same name declared in: " + beanDefinition.getResourceDescription();
  57. }
  58. catch (NoSuchBeanDefinitionException ex) {
  59. // Ignore - simply no detailed message then.
  60. }
  61. throw new IllegalStateException(msg);
  62. }
  63. }
  64. Method currentlyInvoked = SimpleInstantiationStrategy.getCurrentlyInvokedFactoryMethod();
  65. if (currentlyInvoked != null) {
  66. String outerBeanName = BeanAnnotationHelper.determineBeanNameFor(currentlyInvoked);
  67. beanFactory.registerDependentBean(beanName, outerBeanName);
  68. }
  69. return beanInstance;
  70. }
  71. finally {
  72. if (alreadyInCreation) {
  73. beanFactory.setCurrentlyInCreation(beanName, true);
  74. }
  75. }
  76. }

这里面和我们主题相关的就是 BeanMethodInterceptor。这个拦截器的主要逻辑就是拦截对于 @Bean 注解方法的调用,并看声明的 Spring bean 是否已经存在,如果存在则直接返回容器中的 Spring bean。否则真正的配置类的方法创建 Spring bean 实例。

总结

Spring 利用 BeanFactoryPostProcessor 扩展点, 通过 CGLIB enhancer 增强了 @Configuration 注解的配置类。重载的方式是创建了一个新的以配置类为父类增强子类。对于配置类中 @Bean 注解的方法的调用将会被拦截器拦截。拦截器的逻辑是判断声明的 Spring bean 在容器中是否已经存在,如果存在则直接返回容器中的 Spring bean。否则真正的配置类的方法创建 Spring bean 实例。


文章标签:

原文连接:https://juejin.cn/post/7116091617542406157

相关推荐

Flask框架——消息闪现

34个图片压缩工具集合,包含在线压缩和CLI工具

入门即享受!coolbpf 硬核提升 BPF 开发效率 | 龙蜥技术

基于 OPLG 从 0 到 1 构建统一可观测平台实践

全链路灰度在数据库上我们是怎么做的?

冴羽答读者问:过程比结果重要吗?如果是,怎么理解?如果不是,又怎么解?

接口文档管理工具,选yapi 还是 Apifox? 这里列出了两款软件的深度分析,看完再下载不迟。

基于 Docker 来部署 Vue 或 React 前端项目及 Node 后端服务

三十岁的我,自由了!

如何实现带timeout的input?

统计千行代码Bug率,有没有意义?

814. 二叉树剪枝 : 简单递归运用题

【综合笔试题】难度 3.5\u002F5,多解法热门二叉树笔试题

为什么设计的软件不好用?那是因为不熟悉软件开发模型!一文熟悉软件开发模型

作为前端,我是这样从零实现CI\u002FCD二(node服务部署及前后端联调)

极智开发 | 讲解 Nginx 特性之一:反向代理

Netty 案例之 IM 方案设计

从 Google 离职,前Go 语言负责人跳槽小公司

最终一致性性分布式事务 TCC

不谈源码,聊聊位运算的实际应用