在Spring的@Configuration类中直接调用@Bean方法

之前也有过疑问,但是一直懒得看,这次无意中看到了一个问答,于是认真的研究了下。
singleton.png

References:

大概翻译下参考链接中StackOverflow的那个问题,问题如下,在这段代码中:

@Configuration
@EnableWebSecurity
@AllArgsConstructor
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    ...

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(authenticationProvider());
    }


    @Bean
    public AuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
        daoAuthenticationProvider.setUserDetailsService(userDetailsService);
        return daoAuthenticationProvider;
    }
}

Spring会给标记了@Bean的authenticationProvider()方法创建相应的Bean实例,默认是Singleton。然后在configure方法中调用了authenticationProvider方法,看起来authenticationProvider也返回了实例,但是authenticationProvider()方法却没有被真正调用(可以打日志查看)。而且不管调用多少次或者在其他调用都是一样,返回的都是同一个实例。那Spring是给添加了@Bean注解的的方法施加了魔法吗?

答:
Spring确实施加了魔法,查看Spring文档,有这么一段(可以复制去搜索)

This is where the magic comes in: All @Configuration classes are subclassed at startup-time with CGLIB. In the subclass, the child method checks the container first for any cached (scoped) beans before it calls the parent method and creates a new instance.(所有@Configuration的类在启动时会用CGLIB生成子类。在子类中,被重写的方法在调用父类方法创建实例之前会首先检查容器是否有缓存的bean)
这意味着@Bean方法的调用被CGLIB代理了,所以返回了缓存的bean(没有创建新的实例)。
@Bean默认的scope是SINGLETON,如果你指定了scope比如PROTOTYPE,那么调用仍会被传递到原始的方法,即创建新的实例。
要注意的是,这不适用于static方法,按照Spring文档所述:
Calls to static @Bean methods never get intercepted by the container, not even within @Configuration classes (as described earlier in this section), due to technical limitations: CGLIB subclassing can override only non-static methods. As a consequence, a direct call to another @Bean method has standard Java semantics, resulting in an independent instance being returned straight from the factory method itself.(对静态@Bean方法的调用永远不会被容器拦截,即使在@Configuration类中也是如此(如本节前面所述),由于技术限制:CGLIB子类只能覆盖非静态方法,因此,直接调用另一个标准Java语法的@Bean方法,会从工厂方法本身返回一个独立的实例。)

到这其实就说的的已经很清楚了,也可以去Spring文档中搜索并查看相关部分,就能很好的理解Spring是怎么施加魔法的。但是我忽然就是很想知道具体的细节,他代码层究竟是怎么实现的。

那么就来扒一扒代码吧,首先添加下面这段代码中的第3行,将CGLIB生成的类保存到磁盘中。

public static void main(String[] args) {
    // 该设置用于输出cglib动态代理产生的类
    System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "D:\\tmp");
    // 该设置用于输出jdk动态代理产生的类,输出的文件路径为your project下。
    System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");

    SpringApplication.run(WechatApplication.class, args);
}

以文章开始的那段代码为例,在configure方法中添加断点,并以debug方式启动,启动后走到这里停住。
Snipaste_2019-12-24_11-10-07.png

F5进去查看,进来的不是直接到authenticationProvider方法中,而是到BeanMethodInterceptorintercept方法中,说明已经被CGLIB代理了。

/**
 * 增强@Bean方法,检查提供的BeanFactory是否存在此bean对象。
 * Enhance a {@link Bean @Bean} method to check the supplied BeanFactory for the
 * existence of this bean object.
 * @throws Throwable as a catch-all for any exception that may be thrown when invoking the
 * super implementation of the proxied method i.e., the actual {@code @Bean} method
 */
@Override
@Nullable
public Object intercept(Object enhancedConfigInstance, Method beanMethod, Object[] beanMethodArgs,
            MethodProxy cglibMethodProxy) throws Throwable {

    ConfigurableBeanFactory beanFactory = getBeanFactory(enhancedConfigInstance);
    String beanName = BeanAnnotationHelper.determineBeanNameFor(beanMethod);

    // 确定bean是否有@Scope注解,因为SINGLETON和PROTOTYPE的处理不一样
    // Determine whether this bean is a scoped-proxy
    if (BeanAnnotationHelper.isScopedProxy(beanMethod)) {
        String scopedBeanName = ScopedProxyCreator.getTargetBeanName(beanName);
        if (beanFactory.isCurrentlyInCreation(scopedBeanName)) {
            beanName = scopedBeanName;
        }
    }

    // 要处理Bean间方法引用的情况,我们必须显式检查容器已缓存的实例。
    // To handle the case of an inter-bean method reference, we must explicitly check the
    // container for already cached instances.

    // 首先,检查所请求的bean是否为FactoryBean。如果就创建一个子类代理,拦截getObject()的调用并返回缓存的bean实例。这确保从@Bean方法中调用FactoryBean与在XML中引用FactoryBean的语义相同。参阅SPR-6602。
    // First, check to see if the requested bean is a FactoryBean. If so, create a subclass
    // proxy that intercepts calls to getObject() and returns any cached bean instance.
    // This ensures that the semantics of calling a FactoryBean from within @Bean methods
    // is the same as that of referring to a FactoryBean within XML. See SPR-6602.
    if (factoryContainsBean(beanFactory, BeanFactory.FACTORY_BEAN_PREFIX + beanName) &&
            factoryContainsBean(beanFactory, beanName)) {
        Object factoryBean = beanFactory.getBean(BeanFactory.FACTORY_BEAN_PREFIX + beanName);
        if (factoryBean instanceof ScopedProxyFactoryBean) {
            // Scoped proxy factory beans are a special case and should not be further proxied
        }
        else {
            // It is a candidate FactoryBean - go ahead with enhancement
            return enhanceFactoryBean(factoryBean, beanMethod.getReturnType(), beanFactory, beanName);
        }
    }

    // 检查给定的方法是否对应于容器的当前调用的工厂方法。仅比较方法名称和参数类型,以解决协变返回类型的潜在问题(当前仅在Groovy类中发生)。简单来说就是如果是容器调用的@bean方法就会进到这个if块中,如果是其他方法调用了@bean方法就不会进去(比如本文开始的那段代码)
    if (isCurrentlyInvokedFactoryMethod(beanMethod)) {
        // The factory is calling the bean method in order to instantiate and register the bean
        // (i.e. via a getBean() call) -> invoke the super implementation of the method to actually
        // create the bean instance.
        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()));
        }
        return cglibMethodProxy.invokeSuper(enhancedConfigInstance, beanMethodArgs);
    }

    return resolveBeanReference(beanMethod, beanMethodArgs, beanFactory, beanName);
}

上面这段代码其实去除了前两个if就很简单了,第一个是处理@Scope注解的,第二个是处理FactoryBean的,此例中忽略。然后是isCurrentlyInvokedFactoryMethod方法,第一次调用authenticationProvider是在config方法中,所以不会进到if中,下面会调用resolveBeanReference处理bean依赖,看下:

private Object resolveBeanReference(Method beanMethod, Object[] beanMethodArgs,
        ConfigurableBeanFactory beanFactory, String beanName) {

    // 用户(即不是工厂)正在通过直接或间接调用bean方法来请求此bean。在某些自动装配场景中,该Bean可能已经被标记为“in creation”。 如果是这样,暂时将递增状态设置为false,以避免发生异常。
    // The user (i.e. not the factory) is requesting this bean through a call to
    // the bean method, direct or indirect. The bean may have already been marked
    // as 'in creation' in certain autowiring scenarios; if so, temporarily set
    // the in-creation status to false in order to avoid an exception.
    boolean alreadyInCreation = beanFactory.isCurrentlyInCreation(beanName);
    try {
        if (alreadyInCreation) {
            beanFactory.setCurrentlyInCreation(beanName, false);
        }
        boolean useArgs = !ObjectUtils.isEmpty(beanMethodArgs);
        if (useArgs && beanFactory.isSingleton(beanName)) {
            // 这段注释没看懂,不过不影响,@bean方法没有参数就不会进来,看起来似乎是处理参数依赖的。
            // Stubbed null arguments just for reference purposes,
            // expecting them to be autowired for regular singleton references?
            // A safe assumption since @Bean singleton arguments cannot be optional...
            for (Object arg : beanMethodArgs) {
                if (arg == null) {
                    useArgs = false;
                    break;
                }
            }
        }
        // 重点在这个方法中
        Object beanInstance = (useArgs ? beanFactory.getBean(beanName, beanMethodArgs) :
                beanFactory.getBean(beanName));
        if (!ClassUtils.isAssignableValue(beanMethod.getReturnType(), beanInstance)) {
            // Detect package-protected NullBean instance through equals(null) check
            if (beanInstance.equals(null)) {
                if (logger.isDebugEnabled()) {
                    logger.debug(String.format("@Bean method %s.%s called as bean reference " +
                            "for type [%s] returned null bean; resolving to null value.",
                            beanMethod.getDeclaringClass().getSimpleName(), beanMethod.getName(),
                            beanMethod.getReturnType().getName()));
                }
                beanInstance = null;
            }
            else {
                String msg = String.format("@Bean method %s.%s called as bean reference " +
                        "for type [%s] but overridden by non-compatible bean instance of type [%s].",
                        beanMethod.getDeclaringClass().getSimpleName(), beanMethod.getName(),
                        beanMethod.getReturnType().getName(), beanInstance.getClass().getName());
                try {
                    BeanDefinition beanDefinition = beanFactory.getMergedBeanDefinition(beanName);
                    msg += " Overriding bean of same name declared in: " + beanDefinition.getResourceDescription();
                }
                catch (NoSuchBeanDefinitionException ex) {
                    // Ignore - simply no detailed message then.
                }
                throw new IllegalStateException(msg);
            }
        }
        Method currentlyInvoked = SimpleInstantiationStrategy.getCurrentlyInvokedFactoryMethod();
        if (currentlyInvoked != null) {
            String outerBeanName = BeanAnnotationHelper.determineBeanNameFor(currentlyInvoked);
            beanFactory.registerDependentBean(beanName, outerBeanName);
        }
        return beanInstance;
    }
    finally {
        if (alreadyInCreation) {
            beanFactory.setCurrentlyInCreation(beanName, true);
        }
    }
}

处理bean的依赖可以跳过开始的那几个判断,因为没什么用,直接到beanFactory.getBean(beanName)。熟悉Spring IoC的可能已经知道这个方法了。getBean这段我摘取了参考链接Spring Bean单例缓存源码分析文中的部分,为了文章的连贯就不全部贴出来了,详细的代码分析可以查看参考链接。

/**
 * 检查缓存中或者实例工厂中是否有对应的实例,为什么首先会使用这段代码呢
 *
 * 因为在创建单例bean的时候会存在依赖注入的情况,而在创建依赖的时候为了避免循环依赖
 * Spring创建bean的原则是不等bean创建完成就会创建bean的ObjectFactory提早曝光也就是
 * 将ObjectFactory加入缓存中,一旦下个bean创建的时候需要依赖上一个bean,则直接使用ObjectFactory
 */
// Eagerly check singleton cache for manually registered singletons.
Object sharedInstance = getSingleton(beanName);
// 从缓存中获取 Bean 后,若其不为 null 且 args 为空
if (sharedInstance != null && args == null) {
    if (logger.isDebugEnabled()) {
        if (isSingletonCurrentlyInCreation(beanName)) {
            logger.debug("Returning eagerly cached instance of singleton bean '" + beanName +
                    "' that is not fully initialized yet - a consequence of a circular reference");
        }
        else {
            logger.debug("Returning cached instance of singleton bean '" + beanName + "'");
        }
    }
    /**
     * 为什么会有这么一段呢?因为我们从缓存中获取的 bean 是最原始的 Bean ,并不一定使我们最终想要的 Bean
     * 返回对应的实例,有时候存在诸如BeanFactory的情况并不是直接返回实例本身而是返回指定方法返回的实例 比如工厂bean中定义的factory-method方法中返回的bean
     */
    bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
} else {
    ...
    // Create bean instance.
    if (mbd.isSingleton()) {
        sharedInstance = getSingleton(beanName, () -> {
            try {
                return createBean(beanName, mbd, args);
            }
            catch (BeansException ex) {
                // Explicitly remove instance from singleton cache: It might have been put there
                // eagerly by the creation process, to allow for circular reference resolution.
                // Also remove any beans that received a temporary reference to the bean.
                destroySingleton(beanName);
                throw ex;
            }
        });
        bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
    }
    ...
}

getSingleton方法检查了singleton实例的缓存,第一次调用肯定是没有缓存的,因为此时还没有创建实例。走到else里面,跳过一些代码最终来到了创建bean的部分,调用createBean后又会回到intercept方法中(如果你打了断点)。不过这一次是容器调用了authenticationProvider方法,所以isCurrentlyInvokedFactoryMethod这个判断会进去,最终调用cglibMethodProxy.invokeSuper这个方法,下面就是CGLIB生成类中的方法,直接debug是进不去的。
Snipaste_2019-12-24_13-50-09.png

方法的注释写着会调用原始的方法,那么我想知道它是怎么实现的。这是之前加的一行代码就有用了,我们已经把CGLIB生成的类保存下来了。在相应的路径下可以找到这些文件,由于CGLIB的FastClass机制,所以这里看到了3个和WebSecurityConfig相关的类。
Snipaste_2019-12-24_13-53-18.png

这里根据上面debug图中看到的fci.f2.invoke可以知道,调用的是类WebSecurityConfig$$EnhancerBySpringCGLIB$$a3d2c62f$$FastClassBySpringCGLIB$$117d3f0a的invoke方法,那么直接反编译这个类查看invoke方法:
Snipaste_2019-12-24_14-03-41.png

可以看到又调用了webSecurityConfig$$EnhancerBySpringCGLIB$$a3d2c62f.CGLIB$authenticationProvider$5();继续反编译WebSecurityConfig$$EnhancerBySpringCGLIB$$a3d2c62f.class查看CGLIB$authenticationProvider$5方法。
Snipaste_2019-12-24_14-06-01.png

直接调用了super.authenticationProvider();也就是我们自己写的方法,绕了一圈又回来了。为什么要绕呢,当然是为了增强功能了,可以看到CGLIB$authenticationProvider$5方法下面还有个authenticationProvider方法,如果有增强的功能就会调用这个方法,方法中先检查是否有CALLBACK(感兴趣可以去研究下CGLIB的CALLBACK机制),如果有,就调用CALLBACK的拦截方法增强,没有还是直接调用父类的方法。

既然转了一圈又回到了父类中来,就继续debug查看。到了bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd)这一行,从FactoryBean中获取刚刚创建的bean。然后再更新一些状态就完成创建了,这样不管下一次再调用这个方法或者是其他地方需要注入,都使用用已创建的bean实例,因此不会有多个实例。到此才算完整了解释了多次调用方法问什么不会创建新的实例而是返回同一个bean实例这个问题。

Spring在bean的管理方面做了大量的工作,本文仅分析了直接调用方法这一块,想了解更详细的内容可以查看参考链接中的文章或者源码和相关书籍。

圣诞节快乐。

标签: none

添加新评论

ali-01.gifali-58.gifali-09.gifali-23.gifali-04.gifali-46.gifali-57.gifali-22.gifali-38.gifali-13.gifali-10.gifali-34.gifali-06.gifali-37.gifali-42.gifali-35.gifali-12.gifali-30.gifali-16.gifali-54.gifali-55.gifali-59.gif

加载中……