@Configuration如何保证@Bean单例语义?

news2024/11/15 11:26:55

1. 前言

Spring允许通过@Bean注解方法来向容器中注册Bean,如下所示:

@Bean
public Object bean() {
    return new Object();
}

默认情况下,bean应该是单例的,但是如果我们手动去调用@Bean方法,bean会被实例化多次,这破坏了bean的单例语义。
于是,Spring提供了@Configuration注解,当一个配置类被加上@Configuration注解后,Spring会基于该配置类生成CGLIB代理类,子类会重写@Bean方法,来保证bean是单例的。如下所示:

@Configuration
public class BeanMethodConfig {

    @Bean
    public Object bean() {
        System.err.println("bean...");
        return new Object();
    }

    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(BeanMethodConfig.class);
        BeanMethodConfig config = context.getBean(BeanMethodConfig.class);
        System.err.println("-----------");
        config.bean();
        config.bean();
        config.bean();
    }
}

即使手动触发多次bean()方法,也只会生成一个Object对象,保证了bean的单例语义。Spring是如何做到的呢?

2. ConfigurationClassPostProcessor

ConfigurationClassPostProcessor是BeanFactoryPostProcessor的子类,属于Spring的扩展点之一,它会在BeanFactory准备完毕后,处理BeanFactory里面所有ConfigurationClass类。

@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
    int factoryId = System.identityHashCode(beanFactory);
    if (this.factoriesPostProcessed.contains(factoryId)) {
        throw new IllegalStateException(
                "postProcessBeanFactory already called on this post-processor against " + beanFactory);
    }
    this.factoriesPostProcessed.add(factoryId);
    if (!this.registriesPostProcessed.contains(factoryId)) {
        processConfigBeanDefinitions((BeanDefinitionRegistry) beanFactory);
    }

    /**
     * FullConfigurationClass 才会生成代理类
     * 避免@Bean方法被反复调用,生成多个实例,破坏了singleton语义
     * @see ConfigurationClassEnhancer#enhance(Class, ClassLoader)
     */
    enhanceConfigurationClasses(beanFactory);
    beanFactory.addBeanPostProcessor(new ImportAwareBeanPostProcessor(beanFactory));
}

processConfigBeanDefinitions()方法会处理ConfigurationClass的@ComponentScan注解完成类的扫描和注册,解析@Bean方法等,不是本文分析的重点,略过。
我们重点关注enhanceConfigurationClasses()方法,它会过滤出容器内所有Full模式的ConfigurationClass,只有Full模式的ConfigurationClass才会生成CGLIB代理类

  • 何为Full模式的的ConfigurationClass?

ConfigurationClass分为两种模式,加了@Configuration注解的类才是Full模式,否则是Lite模式。

/**
 * 过滤出所有的FullConfigurationClass 加了@Configuration注解的类
 */
Map<String, AbstractBeanDefinition> configBeanDefs = new LinkedHashMap<>();
for (String beanName : beanFactory.getBeanDefinitionNames()) {
    BeanDefinition beanDef = beanFactory.getBeanDefinition(beanName);
    if (ConfigurationClassUtils.isFullConfigurationClass(beanDef)) {
        if (!(beanDef instanceof AbstractBeanDefinition)) {
            throw new BeanDefinitionStoreException("Cannot enhance @Configuration bean definition '" +
                    beanName + "' since it is not stored in an AbstractBeanDefinition subclass");
        } else if (logger.isInfoEnabled() && beanFactory.containsSingleton(beanName)) {
            logger.info("Cannot enhance @Configuration bean definition '" + beanName +
                    "' since its singleton instance has been created too early. The typical cause " +
                    "is a non-static @Bean method with a BeanDefinitionRegistryPostProcessor " +
                    "return type: Consider declaring such methods as 'static'.");
        }
        configBeanDefs.put(beanName, (AbstractBeanDefinition) beanDef);
    }
}
if (configBeanDefs.isEmpty()) {
    return;
}

如果容器内存在Full模式的ConfigurationClass,则需要挨个处理,生成CGLIB代理类,然后将BeanDefinition的beanClass指向CGLIB代理类,这样Spring在实例化ConfigurationClass对象时,生成的就是CGLIB代理对象了。

ConfigurationClassEnhancer enhancer = new ConfigurationClassEnhancer();
for (Map.Entry<String, AbstractBeanDefinition> entry : configBeanDefs.entrySet()) {
    AbstractBeanDefinition beanDef = entry.getValue();
    // If a @Configuration class gets proxied, always proxy the target class
    beanDef.setAttribute(AutoProxyUtils.PRESERVE_TARGET_CLASS_ATTRIBUTE, Boolean.TRUE);
    try {
        // Set enhanced subclass of the user-specified bean class
        Class<?> configClass = beanDef.resolveBeanClass(this.beanClassLoader);
        if (configClass != null) {
            Class<?> enhancedClass = enhancer.enhance(configClass, this.beanClassLoader);
            if (configClass != enhancedClass) {
                if (logger.isTraceEnabled()) {
                    logger.trace(String.format("Replacing bean definition '%s' existing class '%s' with " +
                            "enhanced class '%s'", entry.getKey(), configClass.getName(), enhancedClass.getName()));
                }
                beanDef.setBeanClass(enhancedClass);
            }
        }
    } catch (Throwable ex) {
        throw new IllegalStateException("Cannot load configuration class: " + beanDef.getBeanClassName(), ex);
    }
}

3. ConfigurationClassEnhancer

代理类的生成逻辑在ConfigurationClassEnhancer#enhance(),我们重点关注。

public Class<?> enhance(Class<?> configClass, @Nullable ClassLoader classLoader) {
    /**
     * 生成的CGLIB代理类会实现EnhancedConfiguration接口,
     * 如果已经实现了EnhancedConfiguration接口,则直接返回
     */
    if (EnhancedConfiguration.class.isAssignableFrom(configClass)) {
        if (logger.isDebugEnabled()) {
            logger.debug(String.format("Ignoring request to enhance %s as it has " +
                            "already been enhanced. This usually indicates that more than one " +
                            "ConfigurationClassPostProcessor has been registered (e.g. via " +
                            "<context:annotation-config>). This is harmless, but you may " +
                            "want check your configuration and remove one CCPP if possible",
                    configClass.getName()));
        }
        return configClass;
    }
    // 生成代理类
    Class<?> enhancedClass = createClass(newEnhancer(configClass, classLoader));
    if (logger.isTraceEnabled()) {
        logger.trace(String.format("Successfully enhanced %s; enhanced class name is: %s",
                configClass.getName(), enhancedClass.getName()));
    }
    return enhancedClass;
}

重点是生成Enhancer对象,然后调用Enhancer#createClass()来生成增强后的子类。
newEnhancer()方法我们重点关注,重点是Enhancer#setCallbackFilter()方法,当我们调用ConfigurationClass的方法时,会被这里设置的Callback子类给拦截。

private Enhancer newEnhancer(Class<?> configSuperClass, @Nullable ClassLoader classLoader) {
    Enhancer enhancer = new Enhancer();
    enhancer.setSuperclass(configSuperClass);
    enhancer.setInterfaces(new Class<?>[]{EnhancedConfiguration.class});
    enhancer.setUseFactory(false);
    enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE);
    enhancer.setStrategy(new BeanFactoryAwareGeneratorStrategy(classLoader));
    enhancer.setCallbackFilter(CALLBACK_FILTER);
    enhancer.setCallbackTypes(CALLBACK_FILTER.getCallbackTypes());
    return enhancer;
}

我们重点看CALLBACK_FILTER属性:

private static final Callback[] CALLBACKS = new Callback[]{
        new BeanMethodInterceptor(),
        new BeanFactoryAwareMethodInterceptor(),
        NoOp.INSTANCE
};

private static final ConditionalCallbackFilter CALLBACK_FILTER = new ConditionalCallbackFilter(CALLBACKS);

它拥有三个Callback实现类,分别是:

  • BeanMethodInterceptor:@Bean方法拦截器。
  • BeanFactoryAwareMethodInterceptor:BeanFactoryAware#setBeanFactory()方法拦截器。
  • NoOp:空壳方法,什么也不做。

4. BeanFactoryAwareMethodInterceptor

生成的CGLIB代理类要保证@Bean方法的单例语义,首先可以确定的一点是:它必须依赖Spring IOC容器,也就是BeanFactory对象。 Spring是如何处理的呢?
生成的CGLIB代理类,默认会实现EnhancedConfiguration接口,用来标记它是通过Enhancer生成的ConfigurationClass增强类。 而EnhancedConfiguration接口又继承了BeanFactoryAware接口,也就是说CGLIB代理类必须重写setBeanFactory()方法,来存放beanFactory对象。
setBeanFactory()方法会被BeanFactoryAwareMethodInterceptor类拦截,看看它的intercept()方法。原来生成的CGLIB代理类会有一个名为$$beanFactory的属性,类型是BeanFactory,setBeanFactory()的逻辑仅仅是给$$beanFactory的属性赋值而已。

public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
    /**
     * 生成的CGLIB代理类会有一个名为$$beanFactory的属性,类型是BeanFactory
     * setBeanFactory()的逻辑就是给$$beanFactory的属性赋值
     */
    Field field = ReflectionUtils.findField(obj.getClass(), BEAN_FACTORY_FIELD);
    Assert.state(field != null, "Unable to find generated BeanFactory field");
    field.set(obj, args[0]);
    if (BeanFactoryAware.class.isAssignableFrom(ClassUtils.getUserClass(obj.getClass().getSuperclass()))) {
        return proxy.invokeSuper(obj, args);
    }
    return null;
}

5. BeanMethodInterceptor

重头戏来了,看名字就知道,BeanMethodInterceptor类是用来拦截@Bean方法的,我们直接看intercept()方法:

public Object intercept(Object enhancedConfigInstance, Method beanMethod, Object[] beanMethodArgs,
                        MethodProxy cglibMethodProxy) throws Throwable {
    /**
     * 获取BeanFactory
     * 生成的子类实现了BeanFactoryAware接口,会把BeanFactory赋值给属性 $$beanFactory
     */
    ConfigurableBeanFactory beanFactory = getBeanFactory(enhancedConfigInstance);
    // @Bean方法名 决定BeanName
    String beanName = BeanAnnotationHelper.determineBeanNameFor(beanMethod);

    if (BeanAnnotationHelper.isScopedProxy(beanMethod)) {
        String scopedBeanName = ScopedProxyCreator.getTargetBeanName(beanName);
        if (beanFactory.isCurrentlyInCreation(scopedBeanName)) {
            beanName = scopedBeanName;
        }
    }
    /**
     * 如果ConfigurationClass是FactoryBean实现类,需要创建代理类来增强getObject()方法返回缓存的bean实例
     */
    if (factoryContainsBean(beanFactory, BeanFactory.FACTORY_BEAN_PREFIX + beanName) &&
            factoryContainsBean(beanFactory, beanName)) {
        Object factoryBean = beanFactory.getBean(BeanFactory.FACTORY_BEAN_PREFIX + beanName);
        if (factoryBean instanceof ScopedProxyFactoryBean) {
        } else {
            return enhanceFactoryBean(factoryBean, beanMethod.getReturnType(), beanFactory, beanName);
        }
    }
    /**
     * 判断是否要调用父类方法,生成bean
     * 以singleton为例:首次getBean时,容器不存在,需要创建bean
     * 1.实例化bean时,会把FactoryMethod写入ThreadLocal
     * @see SimpleInstantiationStrategy#instantiate(org.springframework.beans.factory.support.RootBeanDefinition, java.lang.String, org.springframework.beans.factory.BeanFactory, java.lang.Object, java.lang.reflect.Method, java.lang.Object...)
     * 2.代理对象判断method已经被调用,则直接调用父类方法生成bean
     * 3.实例化完,会清空ThreadLocal
     * 4.再次调用,将直接进resolveBeanReference()从容器中获取缓存bean
     *
     * Spring调用了createBean(),就意味着需要调用父类方法生成bean,Spring本身保证单例语义
     * 用户触发的@Bean方法,需要从BeanFactory#getBean()获取,当容器内不存在bean时,Spring自然会调用createBean(),
     * 会再次进入到这里
     */
    if (isCurrentlyInvokedFactoryMethod(beanMethod)) {
        if (logger.isInfoEnabled() &&
                BeanFactoryPostProcessor.class.isAssignableFrom(beanMethod.getReturnType())) {
            logger.info(String.format("@Bean method %s.%s is non-static and returns an object " +
                            "assignable to Spring's BeanFactoryPostProcessor interface. This will " +
                            "result in a failure to process annotations such as @Autowired, " +
                            "@Resource and @PostConstruct within the method's declaring " +
                            "@Configuration class. Add the 'static' modifier to this method to avoid " +
                            "these container lifecycle issues; see @Bean javadoc for complete details.",
                    beanMethod.getDeclaringClass().getSimpleName(), beanMethod.getName()));
        }
        // 调用父类方法生成bean,对于单例bean,只会触发一次
        return cglibMethodProxy.invokeSuper(enhancedConfigInstance, beanMethodArgs);
    }
    // 从容器加载bean
    return resolveBeanReference(beanMethod, beanMethodArgs, beanFactory, beanName);
}

拦截方法主要做了以下几件事:

  1. 获取beanFactory
  2. 根据@Bean方法名生成beanName
  3. 如果是FactoryBean子类,则需要针对FactoryBean生成代理类,增强getObject()方法
  4. 判断是否要调用父类方法,生成bean
  5. 如果不需要调用父类方法,则从beanFactory去获取bean

重点在于第4步的判断,cglibMethodProxy#invokeSuper()会去调用父类的@Bean方法生成bean对象,而方法isCurrentlyInvokedFactoryMethod()决定了Spring要不要调用父类方法。说白了,要想保证单例,得保证cglibMethodProxy#invokeSuper()只调用一次。

Spring的解决方案是:用ThreadLocal记录FactoryMethod!!!

/**
 * FactoryMethod当前是否已调用?
 */
private boolean isCurrentlyInvokedFactoryMethod(Method method) {
    /**
     * Spring createBean()会将FactoryMethod写入到ThreadLocal
     * 再进这个方法就是true了,也就是回去调用父类方法生成bean
     */
    Method currentlyInvoked = SimpleInstantiationStrategy.getCurrentlyInvokedFactoryMethod();
    return (currentlyInvoked != null && method.getName().equals(currentlyInvoked.getName()) &&
            Arrays.equals(method.getParameterTypes(), currentlyInvoked.getParameterTypes()));
}

当我们调用getBean()方法时,如果这个bean是单例的,且容器内不存在bean对象时,Spring才会调用createBean()方法创建bean,否则直接返回容器内缓存的bean对象。也就是说,对于单例bean,Spring本身会保证**createBean()**方法只会触发一次,只要调用了**createBean()**,代理类就应该调用父类@Bean方法产生bean对象。
createBean()方法会调用SimpleInstantiationStrategy#instantiate()实例化bean,在这个方法里面Spring玩了点小花样,它在调用目标方法前将factoryMethod写入到ThreadLocal里了。
image.png
如此一来,在反射调用目标代理方法时,isCurrentlyInvokedFactoryMethod()方法就会返回true,代理方法就会去调用父类方法生成bean对象,代理方法执行完毕后,Spring会将ThreadLocal清空。当我们再手动去调用@Bean方法时,isCurrentlyInvokedFactoryMethod()方法就会返回false,代理方法将不再调用父类方法,而是通过BeanFactory#getBean()方法向容器拿bean,因为容器已经存在bean了,所以会直接返回,不会再调用factoryMethod方法了,这样就保证了父类方法只会触发一次,也就保证了bean的单例语义。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/129598.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

iServer使用影像服务(二)——影像服务发布为wmts和wms服务的加载

前言 自从SuperMap iServer10.2.0版本开始新增影像服务模块&#xff0c;并支持大规模影像&#xff08;栅格&#xff09;数据快速发布为影像服务&#xff0c;之后推出的版本中都陆陆续续对影像服务模块扩充了新功能、增强了新特性&#xff1b;如在SuperMap iServer10.2.1版本中…

JAVA注解使用

JAVA注解使用简介概念说明自定义注解注解格式元注解注解本质属性注解生成文档案例生成的源代码生成Doc简单的自定义反射演示注解定义调用类定义测试通过注解实现配置类定义枚举类定义注解配置类使用注解测试自动生成数据库生成演示数据库注解定义使用注解创建数据表使用注解创建…

[Java安全]—fastjson漏洞利用

前言 文章会参考很多其他文章的内容&#xff0c;记个笔记。 FASTJSON fastjson 组件是 阿里巴巴开发的序列化与 反序列化组件。 fastjson 组件 在反序列化不可信数据时会导致远程代码执行。 POJO POJO是 Plain OrdinaryJava Object 的缩写 &#xff0c;但是它通指没有使用 …

ZGC 垃圾收集器详解(过程演示)

理论部分就不细讲了&#xff0c;具体可以看《 jvm 虚拟机原理》&#xff0c;下面直接画图来演示 ZGC 垃圾回收过程。 第一步 初始状态&#xff0c;视图由 mark0 切换为 Remapped &#xff0c;其中&#xff0c;大方块是 region&#xff0c;小方块是对象&#xff0c;小方块上面数…

Win11的几个实用技巧系列之不能玩植物大战僵尸、如何彻底删除360所有文件

目录 Win11不能玩植物大战僵尸怎么办?Win11玩不了植物大战僵尸的解决方法 Win11玩不了植物大战僵尸的解决方法 win11如何彻底删除360所有文件?win11彻底删除360所有文件方法分享 win11如何卸载360&#xff1a; Win11不能玩植物大战僵尸怎么办?Win11玩不了植物大战僵尸的解…

记一次H3CIE实验考试

一、前言 直接上图 IE机试在12月19号考的&#xff0c;为避免成为小羊人&#xff0c;没去北京/杭州这2个固定地点&#xff0c;就在本省的协办单位考的。但是&#xff0c;还是中招了&#xff0c;5个同学一起去考的&#xff0c;全阳了。 华三机试一共有三套图&#xff0c;ACD&am…

1343:【例4-2】牛的旅行

1343&#xff1a;【例4-2】牛的旅行 时间限制: 1000 ms 内存限制: 65536 KB 【题目描述】 农民John的农场里有很多牧区。有的路径连接一些特定的牧区。一片所有连通的牧区称为一个牧场。但是就目前而言&#xff0c;你能看到至少有两个牧区不连通。现在&#xff0c;John…

【特殊的一年,过去了】再见2022,你好2023

现在是2022年12月30日&#xff0c;提前的新年快乐&#xff01; 各位阳过了吗&#xff1f;&#xff08; tips:最近新学会的打招呼方式:) &#xff09; 我已经阳康啦&#xff0c;所以本文是带有奥密克戎抗体的&#xff0c;各位不用担心~ – 2022可算是快接近尾声啦&#xff01;…

虹科案例|Vuzix辅助和增强现实技术的全球领导者

当今企业的新现实 Vuzix 大多数科技业内人士都认为&#xff0c;未来将是免提时代。总有一天&#xff0c;智能手机、平板电脑、台式电脑和笔记本电脑将被更直观的设备所取代。 然而&#xff0c;对于未来免提的条件&#xff0c;各方意见并不一致。未来的免提设备是将数字信息覆盖…

IntelliJ IDEA 详细使用教程 – 主题,字体,类和方法注释设置

IDEA是Java开发者最喜爱的开发工具之一&#xff0c;高端大气&#xff0c;智能化&#xff0c;个性化&#xff0c;每个开发者都喜欢设置自己喜欢的主题&#xff0c;字体&#xff0c;打造一个属于自己的IDE&#xff0c;本次介绍在IDEA中&#xff0c;如何设置主题&#xff0c;字体等…

聊聊AQS

Java中 AQS 是 AbstractQueuedSynchronizer 类&#xff0c;AQS 依赖 FIFO 队列来提供一个框架&#xff0c;这个框架用于实现锁以及锁相关的同步器&#xff0c;比如信号量、事件等。 在 AQS 中&#xff0c;主要有两部分功能&#xff0c;一部分是操作 state 变量&#xff0c;第二…

调用html5播放器时,出现播放器按钮太小的问题

用手机浏览器打开视频&#xff0c;有时会出现播放器按钮太小的情况&#xff0c;此时只需在<head>中加入下面这段viewport代码即可解决&#xff1a; <meta name"viewport" content"widthdevice-width, initial-scale1, maximum-scale1,minimum-scale1…

Docker下Mysql应用部署

目录 环境搭建 进入mysql 外部连接mysql 外部插入数据 查询容器数据 环境搭建 docker pull mysqlmkdir /root/mysql cd /root/mysqldocker run -id \ -p 3307:3306 \ --name my_sql \ -v $PWD/logs:/logs \ -v $PWD/data:/var/lib/mysql \ -v $PWD/conf:/etc/mysql/conf…

【开源项目】任务调度框架PowerJob介绍及源码解析

项目介绍 PowerJob&#xff08;原OhMyScheduler&#xff09;是全新一代分布式调度与计算框架&#xff0c;能让您轻松完成作业的调度与繁杂任务的分布式计算。 项目地址 源码&#xff1a;https://gitee.com/KFCFans/PowerJob官网&#xff1a;http://www.powerjob.tech/index…

前端期末考试试题及参考答案(01)

版权声明 本文原创作者&#xff1a;谷哥的小弟作者博客地址&#xff1a;http://blog.csdn.net/lfdfhl 一、 填空题 ______表示页面中一个内容区块或整个页面的标题。______表示页面中一块与上下文不相关的独立内容&#xff0c;比如一篇文章。CSS的引入方式有3种&#xff0c;分…

Python数据分析案例15——超市零售购物篮关联分析(apriori)

啤酒和纸尿裤的故事大多数人都听说过&#xff0c;纸尿裤的售卖提升了啤酒的销售额。 关联分析就是这样的作用&#xff0c;可以研究某种商品的售卖对另外的商品的销售起促进还是抑制的作用。 案例背景 本次案例背景是超市的零售数据&#xff0c;研究商品之间的关联规则。使用的…

移植SFUD,驱动SPI FLASH ZD25WQ80

1、关于SFUD SFUD (Serial Flash Universal Driver) 串行 Flash 通用驱动库&#xff0c;支持众多spi flash&#xff0c;关于SFUD的详细资料可参考&#xff1a;https://github.com/armink/SFUD。 2、为什么会有通用驱动 JEDEC &#xff08;固态技术协会&#xff09;针对串行 …

Python的22个万用公式,你确定不看看吗

前言 在大家的日常python程序的编写过程中&#xff0c;都会有自己解决某个问题的解决办法&#xff0c;或者是在程序的调试过程中&#xff0c;用来帮助调试的程序公式。 小编通过几十万行代码的总结处理&#xff0c;总结出了22个python万用公式&#xff0c;可以帮助大家解决在…

TypeScript中type和interface区别

typescript中interface介绍&#xff1a;TypeScript 中的接口 interface_疆~的博客-CSDN博客通常使用接口&#xff08;Interface&#xff09;来定义对象的类型。https://blog.csdn.net/qq_40323256/article/details/128478749 type type关键字是声明类型别名的关键字。用来给一…

windows 编译C++ boost库(超详细)

系列文章目录 文章目录系列文章目录前言一、windows二、b2.exe 参数前言 boost库其实不进行编译&#xff0c;大部分库也是可以正常使用的 而且也有一个开源工具vcpkg可以帮组我们下载编译&#xff0c;只是在国内用起来比较麻烦&#xff0c;而且还时常出bug 所以这里详细记录…