• 【源码】“@Value 注入失败”引发的一系列骚操作


    背景

    项目里想用@Value注入一个字段,可没想到怎么都注入不成功,但换另一种方式就可以,于是就想了解一下@Value注解不成功的原因。

    本文的代码是基于Spring的5.3.8版本

    模拟@Value成功的场景

    首先为了搞清楚@Value注解不成功的原理,我们先用最简单的代码模拟一下它注入成功的例子:

    在resources文件夹下定义了application.yml,内容如下:
    my:
      value: hello
    
    定义一个配置类:
    @Configuration
    @Data
    public class Config {
        @Value("${my.value}")
        private String myValue;
    }
    
    定义一个测试类:
    public class Main {
        public static void main(String[] args) {
            AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class);
            Config config = context.getBean(Config.class);
            System.out.println(config);
        }
    }
    
    输出:
    Config(myValue=${my.value})
    

    上面的代码做了几件事情:

    1. resources/application.yml文件中定义了my.value=hello
    2. 定义了一个Config类,利用@value注解将hello注入到字段myValue
    3. 定义了一个Main类测试效果

    测试类做了几件事情:

    1. 使用AnnotationConfigApplicationContext这个容器加载配置类
    2. 获取配置类Config
    3. 输出注入的字段myValue

    从结果来看,并没有注入成功,我的第一感觉就是没有把我们的application.yml文件里的内容加载到environment里面,那我们就来看看environment里面都有什么内容,如下代码:

    public class Main {
        public static void main(String[] args) {
            AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class);
            ConfigurableEnvironment environment = context.getEnvironment();
            System.out.println(environment);
        }
    }
    

    从结果来看:

    1. environment并没有包含我们application.yml文件里的内容
    2. 但它包含了其他两个东西,分别是systemPropertiessystemEnvironment

    那我们就需要把application.yml文件里的内容加载到environment,需要考虑以下两个问题:

    1. 怎么解析yml文件的内容
    2. 怎么把解析的内容放到environment

    针对问题一:可以利用spring自带的YamlPropertySourceLoader这个类的load()方法,它会返回一个List<PropertySource<?>>

    针对问题二:我们可以先来看一下默认的内容是怎么放进去的,看一下getEnvironment()的源码:

    public abstract class AbstractApplicationContext extends DefaultResourceLoader
    		implements ConfigurableApplicationContext {
    	public ConfigurableEnvironment getEnvironment() {
    		if (this.environment == null) {
    			this.environment = createEnvironment();
    		}
    		return this.environment;
    	}
    	protected ConfigurableEnvironment createEnvironment() {
    		return new StandardEnvironment();
    	}
    } 
    

    从上面可以看出默认创建的是一个StandardEnvironment,我们再来看一下它的初始化:

    public class StandardEnvironment extends AbstractEnvironment {
    	public static final String SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME = "systemEnvironment";
    
    	public static final String SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME = "systemProperties";
    
    	@Override
    	protected void customizePropertySources(MutablePropertySources propertySources) {
    		propertySources.addLast(
    				new PropertiesPropertySource(SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, getSystemProperties()));
    		propertySources.addLast(
    				new SystemEnvironmentPropertySource(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, getSystemEnvironment()));
    	}
    }
    
    public abstract class AbstractEnvironment implements ConfigurableEnvironment {
    	public AbstractEnvironment() {
    		this(new MutablePropertySources());
    	}
    
    	protected AbstractEnvironment(MutablePropertySources propertySources) {
    		this.propertySources = propertySources;
    		this.propertyResolver = createPropertyResolver(propertySources);
    		customizePropertySources(propertySources);
    	}
    }
    

    从上面代码可以看出,在StandardEnvironment.customizePropertySources()的方法中,是通过propertySources.addLast()方法添加进去的,那我们可以照葫芦画瓢,如下:

    public class Main {
        public static void main(String[] args) throws IOException {
            AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class);
            ConfigurableEnvironment environment = context.getEnvironment();
            System.out.println(environment);
            YamlPropertySourceLoader loader = new YamlPropertySourceLoader();
            List<PropertySource<?>> propertySources = loader.load("my-properties",
                    new FileSystemResource("/Users/tiger/spring-boot-study/src/main/resources/application.yml"));
            environment.getPropertySources().addLast(propertySources.get(0));
            System.out.println(environment);
        }
    }
    

    从上面结果可以看出,我们已经成功把我们的application.yml文件内容放到environment中了

    那我们把测试代码改成:

    public class Main {
        public static void main(String[] args) throws IOException {
            AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class);
            YamlPropertySourceLoader loader = new YamlPropertySourceLoader();
            List<PropertySource<?>> propertySources = loader.load("my-properties",
                    new FileSystemResource("/Users/tiger/spring-boot-study/src/main/resources/application.yml"));
            context.getEnvironment().getPropertySources().addLast(propertySources.get(0));
            Config config = context.getBean(Config.class);
            System.out.println(config);
        }
    }
    
    输出:
    Config(myValue=${my.value})
    

    从上面的结果可以看出,还是没有得到我们想要的结果,这是因为conig类会提前初始化,是在refresh()方法中的finishBeanFactoryInitialization()方法进行的,所以我们要在这一步之前把我们的内容放到environment

    翻了一翻refresh()这个方法,发现在prepareRefresh()这个方法里有一个initPropertySources()的方法,注释写着初始化一系列的资源,所以我们可以在这个方法里面加载我们的配置文件,于是变成:

    public class Main {
        public static void main(String[] args) {
            AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class) {
                @SneakyThrows
                @Override
                public void initPropertySources() {
                    YamlPropertySourceLoader loader = new YamlPropertySourceLoader();
                    List<PropertySource<?>> propertySources = loader.load("my-properties",
                            new FileSystemResource("/Users/tiger/spring-boot-study/src/main/resources/application.yml"));
                    getEnvironment().getPropertySources().addLast(propertySources.get(0));
                }
            };
            Config config = context.getBean(Config.class);
            System.out.println(config);
        }
    }
    
    输出:
    Config(myValue=hello)
    

    到目前为止,我们模拟了@Value注入成功的场景,项目里面应该不会出现这种资源没有加载的问题,因为这些事情spring boot都帮我们做好了

    所以直接在@Configuration类下直接用@Value是没有问题的

    模拟注入不成功的场景

    现在我们就来模拟一下注入不成功的场景,配置类改成如下:

    @Configuration
    @Data
    public class Config {
        @Value("${my.value}")
        private String myValue;
    
        @Bean
        public MyBeanFactoryPostProcessor myBeanFactoryPostProcessor() {
            return new MyBeanFactoryPostProcessor();
        }
    
        public static class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
            @Override
            public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
            }
        }
    }
    

    输出结果:

    Config(myValue=null)
    

    这就是我项目上遇到的问题,在配置类中再生成一个BeanFactoryPostProcessor后,@Value就注入不成功了

    但只要把这个方法写成static就可以了,如下:

    @Configuration
    @Data
    public class Config {
        @Value("${my.value}")
        private String myValue;
    
        @Bean
        public static MyBeanFactoryPostProcessor myBeanFactoryPostProcessor() {
            return new MyBeanFactoryPostProcessor();
        }
    
        public static class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
            @Override
            public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
            }
        }
    }
    

    输出结果:

    Config(myValue=hello)
    

    看看为什么没有注入成功

    @Value是由AutowiredAnnotationBeanPostProcessor.postProcessProperties()处理的,所以我们就以这里为入口进行调试。

    我们先把static去掉:

    发现没有执行到上述方法,那我们再把static加上,看一下成功的情况:

    可以看到,是可以到这个方法的,而且知道这个方法是被AbstractAutowireCapableBeanFactory.populateBean()调用的,我们再看一下这里的情况:

    从上图可以看出,getBeanPostProcessorCache().instantiationAware是有AutowiredAnnotationBeanPostProcessor这个实例的

    那我们再来看一下不加static这里的情况:

    果然,没有注入成功的原因是在创建config实例的时候,还没有创建AutowiredAnnotationBeanPostProcessor实例

    我们来看一下这个getBeanPostProcessorCache().instantiationAware是什么东西,又是如何生成的

    发现只有在AbstractBeanFactory.getBeanPostProcessorCache()这个方法会将InstantiationAwareBeanPostProcessor添加到instantiationAware,如下:

    public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport implements ConfigurableBeanFactory {
    	BeanPostProcessorCache getBeanPostProcessorCache() {
    		BeanPostProcessorCache bpCache = this.beanPostProcessorCache;
    		if (bpCache == null) {
    			bpCache = new BeanPostProcessorCache();
    			for (BeanPostProcessor bp : this.beanPostProcessors) {
    				if (bp instanceof InstantiationAwareBeanPostProcessor) {
    					bpCache.instantiationAware.add((InstantiationAwareBeanPostProcessor) bp);
    					if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
    						bpCache.smartInstantiationAware.add((SmartInstantiationAwareBeanPostProcessor) bp);
    					}
    				}
    				if (bp instanceof DestructionAwareBeanPostProcessor) {
    					bpCache.destructionAware.add((DestructionAwareBeanPostProcessor) bp);
    				}
    				if (bp instanceof MergedBeanDefinitionPostProcessor) {
    					bpCache.mergedDefinition.add((MergedBeanDefinitionPostProcessor) bp);
    				}
    			}
    			this.beanPostProcessorCache = bpCache;
    		}
    		return bpCache;
    	}
    }
    

    从上面的代码看出,本质还是从this.beanPostProcessors获取的,我们来看一下什么时候会把AutowiredAnnotationBeanPostProcessor添加到容器中,如下:

    从上图可知:AutowiredAnnotationBeanPostProcessor是在refresh()方法中的registerBeanPostProcessors()方法注入的

    我们再来看一下加static方法的config类是什么时候加载的:

    再来看一下不加static方法的config类是什么时候加载的

    我们来总结一下提到的方法在refresh()方法中的顺序:

    invokeBeanFactoryPostProcessors(); ——> 不加static的时候,在这一步加载config类
    
    registerBeanPostProcessors();  ——> 注册AutowiredAnnotationBeanPostProcessor
    
    finishBeanFactoryInitialization(); 加static的时候,在这一步加载config类
    

    所以我们就知道原因了:当不加static字段时候,加载config类的时候,我们的AutowiredAnnotationBeanPostProcessor还没有注册,所以就会不成功,而当加上static后,我们加载config类的时候,我们的AutowiredAnnotationBeanPostProcessor已经注册好了。

    为什么加static和不加static的加载顺序是不一样的呢

    spring容器会在invokeBeanFactoryPostProcessors()这一步会加载所有的BeanFactoryPostProcessor,如果用static修饰的话,则不会加载config类,反之会加载。原因如下:

    上图已经给出了原因,如果生成bean的工厂方法是static方法就不会加载,反之会加载。

    我们不加static,能不能也让它注入成功呢?

    那无非就是在加载config类之前,把AutowiredAnnotationBeanPostProcessor提前加载到容器就可以了,那我们来看一下源码是怎么加载这个实例的:

    我们同样可以依葫芦画瓢,看看在哪里提前加载比较合适,发现postProcessBeanFactory()这个方法比较合适,于是改成:

    public class Main {
        public static void main(String[] args) {
            AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class) {
                @SneakyThrows
                @Override
                public void initPropertySources() {
                    YamlPropertySourceLoader loader = new YamlPropertySourceLoader();
                    List<PropertySource<?>> propertySources = loader.load("my-properties",
                            new FileSystemResource("/Users/tiger/spring-boot-study/src/main/resources/application.yml"));
                    getEnvironment().getPropertySources().addLast(propertySources.get(0));
                }
    
                @Override
                protected void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
                    String ppName = "org.springframework.context.annotation.internalAutowiredAnnotationProcessor";
                    beanFactory.addBeanPostProcessor(getBean(ppName, BeanPostProcessor.class));
                }
            };
            Config config = context.getBean(Config.class);
            System.out.println(config);
        }
    }
    
    输出:
    Config(myValue=${my.value})
    

    从结果来看,还是没注入成功啊,经过一番调试,发现是在下面步骤中出了问题:

    我们来看一下加载成功的情况:

    embeddedValueResolver是在下面步骤中被添加进去的:

    可以看出是在refresh()中的finishBeanFactoryInitialization()这个方法里面添加进去的,所以我们也要提前搞一下:

    public class Main {
        public static void main(String[] args) {
            AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class) {
                @SneakyThrows
                @Override
                public void initPropertySources() {
                    YamlPropertySourceLoader loader = new YamlPropertySourceLoader();
                    List<PropertySource<?>> propertySources = loader.load("my-properties",
                            new FileSystemResource("/Users/tiger/spring-boot-study/src/main/resources/application.yml"));
                    getEnvironment().getPropertySources().addLast(propertySources.get(0));
                }
    
                @Override
                protected void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
                    String ppName = "org.springframework.context.annotation.internalAutowiredAnnotationProcessor";
                    beanFactory.addBeanPostProcessor(getBean(ppName, BeanPostProcessor.class));
                    beanFactory.addEmbeddedValueResolver(strVal -> getEnvironment().resolvePlaceholders(strVal));
                }
            };
            Config config = context.getBean(Config.class);
            System.out.println(config);
        }
    }
    
    输出:
    Config(myValue=hello)
    

    好了,大功告成!

    总结

    看到这里,相信大家都知道@Value为什么加载不成功了吧,主要就是因为加载顺序的关系,可以看出最简单的方法就是在方法上加一个static,后面的探究主要是地对Spring容器加载顺序的理解

    本文探究的是在配置类里存在BeanFactoryPostProcessor,如果换成BeanPostProcessor呢?同样会加载不成功吗?又是因为什么原因呢?其实也可以用同样的方法来测试,和本文讲的如出一辙,小伙伴们可自行探究一下。

    有什么问题欢迎一起探讨~~~

  • 相关阅读:
    IOS开发中Xcode常用插件安装与管理(转)
    IOS开发中摇一摇是怎么实现的
    IOS中APP开发常用的一些接口
    数据结构——不相交集(并查集)
    数据结构——(最小)堆(完全二叉树)
    JDK1.7 中的HashMap源码分析
    Java中hashCode()方法以及HashMap()中hash()方法
    《Java多线程核心技术》读书摘要
    Java for LeetCode 237 Delete Node in a Linked List
    Java类变量、实例变量的初始化顺序
  • 原文地址:https://www.cnblogs.com/eaglelihh/p/15009654.html
Copyright © 2020-2023  润新知