• DependsOn注解失效问题排查



    前言

    最近几天遇到一个比较有意思的问题,发现Spring的DependsOn注解失效,令我大为费解。经过一段排查,还是有收获的,记录下来,自己警醒,也给大家避雷。
    为了去掉敏感信息,本文所有代码均为示例,并不是实际线上代码!!!


    一、现象描述

    现象复现版本

    Java:Java8;Spring:Spring5.3.7

    1.1.背景描述

    我们实例化某个对象时,需要从配置中心热加载里获取某个属性,使得对象初始化时获取配置中心的数据,形如:

    public class MyProxy implements FactoryBean {
    
        private Class<?> proxyClass;
    
        public void setProxyClass(Class<?> proxyClass) {
            this.proxyClass = proxyClass;
        }
    
        @Override
        public Object getObject() throws Exception {
            return proxyClass.newInstance();
        }
    
        @Override
        public Class<?> getObjectType() {
            return proxyClass;
        }
    }
    
    
    
    @Configuration
    public class ProxyConfig {
    
        @Bean
        public MyProxy proxyService1() {
            System.out.println("init proxyService1");
            MyProxy proxy = new MyProxy();
            proxy.setProxyClass(ProxyService1.class);
            proxy.setInfo(MyConfig.info);
            return proxy;
        }
    
        @Bean(name = "myConfig", initMethod = "init", destroyMethod = "destroy")
        public MyConfig myConfig() {
            return new MyConfig();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    myConfig实例化之后会调用init方法,从远程的配置中心拉取配置信息,将MyConfig类里的变量值进行设置;理想情况,在proxyService1()方法执行,并执行实例化的时候读取到的MyConfig.info应该是配置中心里配置的值,而不是MyConfig类里定义的info初始化值。
    Test环境验证了一下,可以读取到配置中心的值,就美滋滋的上线了。
    结果,线上验收时,没有生效!!!!

    1.2.第一次修改,使用DependsOn注解

    猜测了下原因,因为Spring实例化对象的顺序并不能保证每次运行都一致,也不能保证不同环境实例化对象顺序一致,所以线上应该是先执行了proxyService1(),后执行了myConfig();这样的话proxyService1()执行的时候读取不到配置中心配置的info的值。
    猜测到原因之后,进行了以下的改动,加了DependsOn注解,强制这俩方法的运行顺序

    @Configuration
    public class ProxyConfig {
    
        @Bean
        @DependsOn("myConfig")
        public MyProxy proxyService1() {
            System.out.println("init proxyService1");
            MyProxy proxy = new MyProxy();
            proxy.setProxyClass(ProxyService1.class);
            proxy.setInfo(MyConfig.info);
            return proxy;
        }
    
        @Bean(name = "myConfig", initMethod = "init", destroyMethod = "destroy")
        public MyConfig myConfig() {
            return new MyConfig();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    Test环境验证了一下,可以读取到配置中心的值,就美滋滋的上线了。
    结果,不出意外的情况下又出意外,线上验收时,没有生效!!!!

    1.3.第二次修改,设置方法入参

    这次我修改了proxyService1方法的入参,强制执行时使用myConfig对象

    @Configuration
    public class ProxyConfig {
    
        @Bean
        public MyProxy proxyService1(MyConfig myConfig) {
            System.out.println("init proxyService1");
            MyProxy proxy = new MyProxy();
            proxy.setProxyClass(ProxyService1.class);
            proxy.setInfo(MyConfig.info);
            return proxy;
        }
    
        @Bean(name = "myConfig", initMethod = "init", destroyMethod = "destroy")
        public MyConfig myConfig() {
            return new MyConfig();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    所幸这次成功了
    但是,这是为啥呢?

    二、看看源码

    2.1.Spring实例化的源码

    众所周知,按照咱们的理解,Spring不管怎样,要基于某个definition实例化某个对象的时候,都需要调用AbstractBeanFactory下的getBean方法,最终会调用doGetBean方法,此处为了显眼,我们就直接放截图,不相干的逻辑暂时折叠:
    DependsOn注解生效逻辑
    实在找不到原因,只好调试源码了。

    2.2.调试

    我们给proxyService1方法增加断点,发现比较奇怪的是,其他实例进行属性注入的时候,会调用该方法。进一步查看,发现它是通过SimpleInstantiationStrategy类里的instantiate方法进行实例化的,没有调用getBean方法,也就没有解析DependsOn注解
    FactoryMethod实例化
    FactoryMethod是指加了@Bean注解的方法

    那么问题来了,其他service相互注入,并不是注入proxyService1,为什么会调用该factoryMethod呢???
    看看调用栈来剖析下吧~
    让我们回顾这两篇文章:

    1. @Resource注解的逻辑
    2. @Autowired注解的逻辑
      不管是使用@Resource注解还是@Autowired注解,多数情况都会进入到根据类型进行注入,可以看上面两篇博客。
      不过在上面两篇博客,我们没有详细分析,根据类型查找可用对象的逻辑,也就是findAutowireCandidates方法中的
    String[] candidateNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
    				this, requiredType, true, descriptor.isEager());
    
    • 1
    • 2

    不过,我们先stop一下,如果我们是Spring的开发者,我们该如何来写这个根据类型查找可用实例的逻辑???

    3. 普通加了@Service或者@Component注解的类型,直接用它的类型
    4. 加了@Bean的FactoryMethod的方法,根据方法返回值的类型
    
    • 1
    • 2

    但是如果是FactoryBean呢???
    我们的MyProxy类就是一个FactoryBean!!!!

    1. 加了@Service、@Component注解,那我们应该会是根据FactoryBean里的泛型或者接口方法getObjectType来判断类型
    2. @Bean的返回值是FactoryBean,那我们根据方法返回值的泛型来判断。
      感性认知的话,我们都会这么干,那我们看看Spring是怎么干的,而且像我们MyProxy这种没有指定FactoryBean的泛型的实现,Spring又是如何处理的呢?
    在DefaultListableBeanFactory#doGetBeanNamesForType方法里,遍历所有的beanDefinition,查找beanDefinition的类型
    在AbstractBeanFactory的isTypeMatch方法,调用了getTypeForFactoryBean
    
    • 1
    • 2

    我们具体来读一下getTypeForFactoryBean方法的逻辑

    protected ResolvableType getTypeForFactoryBean(String beanName, RootBeanDefinition mbd, boolean allowInit) {
    		
    		// ....不重要的逻辑先忽略
    
    		// Consider factory methods
    		String factoryBeanName = mbd.getFactoryBeanName();
    		String factoryMethodName = mbd.getFactoryMethodName();
    
    		// Scan the factory bean methods
    		if (factoryBeanName != null) {
    			if (factoryMethodName != null) {
    				// Try to obtain the FactoryBean's object type from its factory method
    				// declaration without instantiating the containing bean at all.
    				BeanDefinition factoryBeanDefinition = getBeanDefinition(factoryBeanName);
    				Class<?> factoryBeanClass;
    				if (factoryBeanDefinition instanceof AbstractBeanDefinition &&
    						((AbstractBeanDefinition) factoryBeanDefinition).hasBeanClass()) {
    					factoryBeanClass = ((AbstractBeanDefinition) factoryBeanDefinition).getBeanClass();
    				}
    				else {
    					RootBeanDefinition fbmbd = getMergedBeanDefinition(factoryBeanName, factoryBeanDefinition);
    					factoryBeanClass = determineTargetType(factoryBeanName, fbmbd);
    				}
    				if (factoryBeanClass != null) {
    				// 这个方法很重要,就是解析返回值是FactoryBean的方法,实际交给Spring容器的对象类型
    				// 而我们的MyProxy在实现FactoryBean时没有指定泛型,导致此处返回的result是个?
    				// 所以result.resolve()是个null
    					result = getTypeForFactoryBeanFromMethod(factoryBeanClass, factoryMethodName);
    					if (result.resolve() != null) {
    						return result;
    					}
    				}
    			}
    			// If not resolvable above and the referenced factory bean doesn't exist yet,
    			// exit here - we don't want to force the creation of another bean just to
    			// obtain a FactoryBean's object type...
    			if (!isBeanEligibleForMetadataCaching(factoryBeanName)) {
    				return ResolvableType.NONE;
    			}
    		}
    
    		// If we're allowed, we can create the factory bean and call getObjectType() early
    		if (allowInit) {
    			FactoryBean<?> factoryBean = (mbd.isSingleton() ?
    					getSingletonFactoryBeanForTypeCheck(beanName, mbd) :
    					getNonSingletonFactoryBeanForTypeCheck(beanName, mbd));
    			if (factoryBean != null) {
    				// Try to obtain the FactoryBean's object type from this early stage of the instance.
    				// 执行了FactoryBean的方法,获取到FactoryBean的实例后,再调用其getObjectType获取真正的返回类型
    				Class<?> type = getTypeForFactoryBean(factoryBean);
    				if (type != null) {
    					return ResolvableType.forClass(type);
    				}
    				// No type found for shortcut FactoryBean instance:
    				// fall back to full creation of the FactoryBean instance.
    				return super.getTypeForFactoryBean(beanName, mbd, true);
    			}
    		}
    //不重要的逻辑先忽略
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60

    可以看到上面的注释写的是先根据getTypeForFactoryBeanFromMethod查找返回值是FactoryBean时方法的实际要交给Spring容器管理对象的类型,如果查找不到,会调用getSingletonFactoryBeanForTypeCheck方法进行FactoryMethod的实例化。也就是会最终调用SimpleInstantiationStrategy类里的instantiate方法,而不是getBean方法进行实例化,也就忽略了DependsOn注解的处理逻辑。

    2.3.验证

    为了验证上述逻辑,我们新增了指定了FactoryBean泛型的类ProxyFactoryBean和可以在方法调用时指定FactoryBean泛型的MyFactoryBean

    public class ProxyFactoryBean implements FactoryBean<ProxyService3> {
        @Override
        public ProxyService3 getObject() throws Exception {
            return ProxyService3.class.newInstance();
        }
    
        @Override
        public Class<?> getObjectType() {
            return ProxyService3.class;
        }
    }
    
    public class MyFactoryBean<T> implements FactoryBean<T> {
    
        private Class<T> proxyClass;
    
        public void setProxyClass(Class<T> proxyClass) {
            this.proxyClass = proxyClass;
        }
    
        @Override
        public T getObject() throws Exception {
            return proxyClass.newInstance();
        }
    
        @Override
        public Class<T> getObjectType() {
            return proxyClass;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    然后我们重写ProxyConfig类

    @Configuration
    public class ProxyConfig {
    
        @Bean
        @Lazy
        public MyProxy proxyService1() {
            System.out.println("init proxyService1");
            MyProxy proxy = new MyProxy();
            proxy.setProxyClass(ProxyService1.class);
            return proxy;
        }
    
        @Bean
        @Lazy
        // 注意看此处,指定了方法返回值的泛型
        public MyFactoryBean<ProxyService2> proxyService2() {
            System.out.println("init proxyService2");
            MyFactoryBean<ProxyService2> proxy = new MyFactoryBean<>();
            proxy.setProxyClass(ProxyService2.class);
            return proxy;
        }
    
        @Bean
        @Lazy
        public ProxyFactoryBean proxyService3() {
            System.out.println("init proxyService3");
            return new ProxyFactoryBean();
        }
    
        @Bean
        @Lazy
        public MyRealService myRealService() {
            System.out.println("init myRealService");
            return new MyRealService();
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    我们的四个对象都没有作为其他对象的属性注入,且都加了@Lazy。理论上应该所有FactoryMethod都不会被调用实例化。我们运行一下看看:
    实验结果
    同样是FactoryBean,不管是类型定义时指定了泛型的ProxyFactoryBean还是在方法实现时指定了方法返回值的MyFactoryBean,都没有被启动。只有MyProxy,在其他属性注入时会被强制启动。

    如果我们把MyFactoryBean的方法返回值的泛型去掉呢???

    	@Bean
        @Lazy
        // 参见此处,我们虽然在方法实现里面指定了MyFactoryBean的proxyClass,但是方法返回值没有明确泛型
        public MyFactoryBean proxyService2() {
            System.out.println("init proxyService2");
            MyFactoryBean<ProxyService2> proxy = new MyFactoryBean<>();
            proxy.setProxyClass(ProxyService2.class);
            return proxy;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    实验结果2
    可以看到,当FactoryMethod如果通过编译的class信息没有办法分析出FactoryBean返回的实际类型时,该FactoryMethod就会被执行,然后调用FactoryBean实例的getObjectType,以便让Spring确认其真实的返回值类型。

    结果实验结果我们可以发现,也就是当放入到spring容器中的对象类型不明确时,就会被调用,不管它是不是Lazy的,而且调用的时候也是直接通过反射调用该方法,不处理其DependsOn注解

    总结

    尽量不要使得放入到spring容器中的对象类型不明确!!!
    但是有时候也不可避免,譬如本人发现该类型的案例是由于公司的rpc组件导致的,利用不明确的FactoryBean实现类,设置不同的对象,来进行rpc的proxy对象生成。

  • 相关阅读:
    【数据结构•并查集】
    一文彻底讲透@Async注解的原理和使用方法
    wayland(xdg_wm_base) + egl + opengles 渲染旋转的 3D 立方体实例(十一)
    Python和BeautifulSoup库的魔力:解析TikTok视频页面
    LeetCode 周赛 346(2023/05/21)仅 68 人 AK 的最短路问题
    代码随想录算法训练营第三十九天| 62.不同路径 63. 不同路径 II
    uni-app直播从0到1实战
    『FastTunnel』荣获GVP的开源内网穿透工具,动手搭建属于自己的内网穿透平台
    使用 JMeter 和 Docker 进行服务存根
    k8s概念之GVK与GVR
  • 原文地址:https://blog.csdn.net/liangsheng_g/article/details/133686498