2

Configuration注解一定要加吗?

 3 years ago
source link: https://segmentfault.com/a/1190000040646880
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Configuration注解一定要加吗?

发布于 13 分钟前

在上一期SpringBoot自定义starter中,我们讲到自动配置类是可以不加@Configuration注解的,但是在特定的场景会引发一个小小的问题,今天我们就来聊一下这个奇怪的小知识吧

先定义两个Bean, 其中Foo依赖Boo

public class Bar {

    public Bar(){
        System.out.println("init");
    }
}
public class Foo {

    public Foo(Bar bar){
    }
}
@Configuration
public class FooConfig {

    @Bean
    public Foo foo(){
        return new Foo(bar());
    }

    @Bean
    public Bar bar(){
        return new Bar();
    }
}

配置类中有个写法: new Foo(bar()),相信大家也这样写过,但是不知道大家有没有思考过这样一个问题:

Spring里面的Bean默认都是单例的,没错吧?

那么请问new Bar()这一行代码在程序启动时执行了几次?

发现问题了吗?

正常来说,应该是有两次

一:将bar注入到容器中时调用一次

二:将foo注入容器中时调用到bar()方法,又调用一次new Bar()

那此时将会产生一个血案,Spring容器中的bar对象与foo持有的bar对象不是同一个实例!

分析到这,觉得是1次的小伙伴扣1,2次的小伙伴扣2

那我们都能发现这个问题,Spring的开发者肯定也能想到,所以到底是几次呢?我们来运行一下看看吧

每次运行new Bar()就会调用一次Bar对象的构造器,输出init语句

编写测试类

public class Main {

    public static void main(String[] args) {
        new AnnotationConfigApplicationContext(FooConfig.class);
    }
}

嘿,答案是一次,这要是B站我不得来一句"猜对的小伙伴把不愧是我打在公屏上"

那么如果把Configuration注解去掉呢?

!!!啊这....

此时Bar对象居然被实例化了两次,血案发生了!

没错,这就我在开头跟大家说的,如果不加@Configuration注解在某些特订的场景会引发一个小问题,这个特定的场景就是:在@Bean注解修饰的方法中调用了另一个@Bean注解修饰的方法。

到这里,文章标题的问题已经回答大家了,@Configuration注解可以不加,但是会出现问题。

但是,有时候Spring建议你不要加!就是在我说的没有特定的场景的时候。为什么呢?

这就需要大家思考一下,按正常的逻辑,Bar对象肯定是要被实例化两次的,但是为什么加了@Configuration注解后只实例化了一次呢?Spring到底做了什么?

我们打开@Configuraiton注解,里面有这样的一个属性proxyBeanMethods,默认值为ture,Spring对它的描述是这样的:

指定@Bean方法是否应该被代理,以便执行Bean生命周期行为,例如,即使在用户代码中直接调用@Bean方法的情况下,也要返回共享的单子Bean实例。这个功能需要方法拦截,通过运行时生成的CGLIB子类来实现。

默认情况下是true,允许通过配置类内的方法进行调用。如果不需要这样做,因为这个特定配置的每个@Bean方法都是自成一体的,被设计成供容器使用的普通工厂方法,那么将这个标志切换为false,以避免CGLIB子类处理。
关闭bean方法拦截可以有效地处理@Bean方法。它在行为上等同于移除@Configuration。

所以当我们加上@Configuraiton注解时,最后Spring其实使用的是一个CGLIB代理类

什么是CGLIB动态代理

CGLIB是一个功能强大的高性能代码生成库。它是JDK动态代理的补充,因为它提供了不实现接口的代理类(通过继承)。

还是刚才的例子,这里再添加一个CGLIB的使用样例

public class Test {

    public static void main(String[] args) {
        Enhancer enhancer = new Enhancer();
    // 设置继承的父类
        enhancer.setSuperclass(FooConfig.class);
    // 设置拦截的方法
        enhancer.setCallback((MethodInterceptor) (obj, method, args1, methodProxy) -> {
            // 调用父类方法前打印一行日志
      System.out.println("cglib proxy!");
            return methodProxy.invokeSuper(obj, args1);
        });
        final FooConfig fooConfig = (FooConfig) enhancer.create();
        fooConfig.bar();
    }
}

Spring中的实现方式

Spring中的实现方式同样体现在它的回调方法中,它的回调方法还是有些复杂的,我这里只摘出它的逻辑部分

@Override
@Nullable
public Object intercept(Object enhancedConfigInstance, Method beanMethod, Object[] beanMethodArgs,
                        MethodProxy cglibMethodProxy) throws Throwable {
  // 取出BeanFactory
  ConfigurableBeanFactory beanFactory = getBeanFactory(enhancedConfigInstance);
  // 通过beanMethod确定beanName
  String beanName = BeanAnnotationHelper.determineBeanNameFor(beanMethod);
  // 当前执行的方法是否与beanMethod相同, 如果正在执行foo,当在foo中调用到bar时则会返回false
  if (isCurrentlyInvokedFactoryMethod(beanMethod)) {
    // 调用父类方法
    return cglibMethodProxy.invokeSuper(enhancedConfigInstance, beanMethodArgs);
  }
  // 直接从容器中取出Bean
  return resolveBeanReference(beanMethod, beanMethodArgs, beanFactory, beanName);
}

这里贴出源码地址:ConfigurationClassEnhancer.BeanMethodInterceptor#intercept

感兴趣的小伙伴可以自己研究下细节

今天和小伙伴们聊了一下@Configuration的必要性,以及一点点它的原理,希望大家有所收获~,我们下期再见~

想要了解更多精彩内容,欢迎关注公众号:程序员阿鉴

个人博客空间:https://zijiancode.cn


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK