2

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

 3 years ago
source link: https://www.cnblogs.com/eaglelihh/p/15009654.html
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

项目里想用@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呢?同样会加载不成功吗?又是因为什么原因呢?其实也可以用同样的方法来测试,和本文讲的如出一辙,小伙伴们可自行探究一下。

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK