本文主要是介绍Spring注解驱动开发(九):利用@Value与@PropertySource实现外部化配置注入,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
文章目录
- 1. 背景介绍
- 2. @Value 详解
- 2.1 非配置文件注入属性
- 2.2 配置文件注入
- 2.3 #{...}和${...}的区别
- 3. @PropertySource
- 4. 源码扩展
- 4.1 @Value 注解注入Bean 属性时机
- 4.2 @PropertySource 解析时机
- 5. 总结
这是我Spring Frame 专栏的第九篇文章,在 Spring注解驱动开发(八):设置Bean初始化和销毁的回调 这篇文章中,我向你详细介绍了如何定制Bean生命周期初始化和销毁的回调函数,如果你未读过那篇文章,但是对内容感兴趣的话,我建议你去阅读一下
1. 背景介绍
请你想象以下场景:
加入你发布了一个项目,里面的一些系统属性信息都是以硬编码的形式存在的,过了一段时间,由于需求或者其它原因,你不得不修改这些属性信息,你必须重新打包部署整个项目…
你是否经历过这样的场景,一种常见的解决方式就是将一些可配置的属性值抽离到配置文件中,以后我们只需要修改配置文件并重启项目就好了
这篇文章我会详细介绍如何利用 @Value与 @PropertySource实现外部化配置
2. @Value 详解
首先我们先来通过源码来了解一下 @Value 到底是用来作什么的:
💡 强烈建议你对照着注释来看源码,Spring 的注释写的还是非常好的
/*** Annotation used at the field or method/constructor parameter level* that indicates a default value expression for the annotated element.** A common use case is to inject values using* #{systemProperties.myProp} style SpEL (Spring Expression Language)* expressions. Alternatively, values may be injected using* ${my.app.myProp} style property placeholders.*
**/
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Value {/*** The actual value expression such as #{systemProperties.myProp}* or property placeholder such as ${my.app.myProp}.*/String value();
}
我把核心注释放在了代码里面,从源码中我们可以得到以下信息:
- @Value 注解主要用在 字段、方法、构造器参数级别
- @Value 有两种用法:
- #{systemProperties.myProp} : SpEl 表达式
- ${my.app.myProp} : 用于注入配置文件中的值
接下来我就详细讲述一下@Value 的两种常见用法
2.1 非配置文件注入属性
我们可以利用 @Value 注解将外部的值动态注入到bean的属性中,我这里主要介绍一下几种用法
- 注入字符串等常量
- 注入操作系统参数
- 注入SpEl 表达式结果
- 注入其它 Bean 的属性
- 注入配置文件
我们先定义一个 ValueBean 用来演示以上用法:
public class Coupon {private Integer id;private String couponType;private Integer profit;public Coupon(Integer id, String couponType, Integer profit) {this.id = id;this.couponType = couponType;this.profit = profit;System.out.println("Coupon 实例化完成");}// 省略 getter,setter 方法
}import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;/*** @author NanCheng* @version 1.0* @date 2022/9/16 19:16*/
public class ValueBean {/*** 注入常量值*/@Value("jack")private String name;/*** 注入系统属性*/@Value("#{systemProperties['os.name']}")private String systemPropertiesName;/*** 注入 SpEL表达式的运算结果*/@Value("#{ T(java.lang.Integer).MAX_VALUE * 1.0d }")private double randomNumber;/*** 注入其它 Bean 的值*/@Value("#{coupon.couponType}")private String couponType;/*** 注入配置文件资源*/@Value("classpath:/application.properties")private Resource properties;@Overridepublic String toString() {return "ValueBean{" +"name='" + name + '\'' +", systemPropertiesName='" + systemPropertiesName + '\'' +", randomNumber=" + randomNumber +", username='" + username + '\'' +", properties=" + properties +'}';}
}
可以看到我这里都是用的属性注入,你也可以试一试其它的方法注入等((官网示例))
接下来我们向容器中注入 Coupon 和 ValueBean:
@Configuration
public class CouponConfig {@Beanpublic Coupon coupon() {return new Coupon(1,"满减",90);}@BeanValueBean valueBean() {return new ValueBean();}
}
最后利用测试类验证以下属性是否成功注入到了 ValueBean 对象中:
public class CouponMain {public static void main(String[] args) {try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(CouponConfig.class)){System.out.println("------------容器初始化完成---------------");System.out.println("Coupon: "+context.getBean(Coupon.class));System.out.println("ValueBean: "+context.getBean(ValueBean.class));} catch (BeansException e) {e.printStackTrace();}}
}
从结果可以明显看到,属性都被成功注入了
至此,我向你简单地介绍了 @Value 注解的第一种用法
2.2 配置文件注入
除了上面讲述的注入方式外,我们还可以将外部配置文件的属性信息注入到Bean 的属性中:
我们先在resource下新建一个application.properties,并在其中设置几个属性值
coupon.id=1
coupon.couponType=满减
coupon.profit=80
接下来我对 Coupon 类进行修改:
public class Coupon {@Value("${coupon.id}")private Integer id;@Value("${coupon.couponType}")private String couponType;@Value("${coupon.profit}")private Integer profit;// 省略 getter,setter,toString 方法
}
⭐️ 注意我的属性值均是利用 @Value 从配置文件中读取的,并且@Value 的属性值均是 ${配置内容}
的格式
接下来我们修改配置类,利用 @PropertySource
注解将application.properties 的配置信息加载到Spring 上下文环境中
@PropertySource(value = {"classpath:/application.properties"},encoding = "UTF-8")
@Configuration
public class CouponConfig {@Beanpublic Coupon coupon() {return new Coupon();}@BeanValueBean valueBean() {return new ValueBean();}
}
接下来运行测试类,查看属性值是否被成功注入到了 Coupon 对象中:
可以看到结果符合预期,配置文件的内容被成功注入到了 Coupon 对象中
2.3 #{…}和${…}的区别
通过上面两个内容的讲述,这里我总结一下 #{…} 与 ${,} 的区别
- #{···}:用于执行SpEl表达式,并将内容赋值给属性
- ${···}:主要用于加载外部属性文件中的值
- ${···}和#{···}可以混合使用,但是必须#{}在外面,${}在里面(这是由Spring 解析顺序决定的)
3. @PropertySource
同样的,我们还是先看一下 @PropertySource 的源码,了解一下它的用法:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(PropertySources.class)
public @interface PropertySource {String name() default "";// 配置文件路径名称的集合String[] value();// 如果查找失败是否忽略此配置文件boolean ignoreResourceNotFound() default false;// 解码配置文件的字符集!!! 应该和配置文件字符集相同String encoding() default "";Class<? extends PropertySourceFactory> factory() default PropertySourceFactory.class;}
Spring 在这个注解的注释上详细介绍了它的用途以及用法,由于注释太长,我这里帮你整理一下:
@PropertySource
注解注释提供了一种方便的声明机制,用于将 PropertySource (配置文件对应的对象信息)添加到 Spring 的环境中,与@Configuration
配合使用@PropertySource
注解可以将properties配置文件中的key/value数据存储到Spring的 Environment 中- 我们也可以使用 @Value注解 配合
${}
占位符为 Bean 的属性注入值
其实在上面的2.2 中我们就用到了这个注解并且配合 @Value 实现了配置文件属性的注入,这里我就不再重复演示了,但是上面说了,它把属性存储到了 Spring 的 Environment 中,这里我们来验证一下 Enviroment 中是否有这些属性值信息:
我们从上下文环境中获取 Enviroment 变量,并从中获取 application.properties 中配置的属性值:
public class CouponMain {public static void main(String[] args) {try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(CouponConfig.class)){System.out.println("------------容器初始化完成---------------");System.out.println("Coupon: "+context.getBean(Coupon.class));System.out.println("ValueBean: "+context.getBean(ValueBean.class));ConfigurableEnvironment environment = context.getEnvironment();System.out.println("coupon.id===>"+environment.getProperty("coupon.id"));System.out.println("coupon.couponType===>"+environment.getProperty("coupon.couponType"));System.out.println("coupon.profit===>"+environment.getProperty("coupon.profit"));} catch (BeansException e) {e.printStackTrace();}}
}
从结果中可以看到我们成功的从 Enviroment 中获取了配置文件中的信息,这也给了我们一个启发:以后我们可以利用@PropertySource 配合 Enviroment 灵活的从容器中获取外部的配置信息
至此,我相信你能够使用两个注解向 Bean 中注入外部化配置信息了。
4. 源码扩展
4.1 @Value 注解注入Bean 属性时机
你是否好奇,被标注@Value 注解的属性是什么时候以何种方式注入到 Bean 中的呢?,其实你可以从 @Value 的注释上发现端倪,它是依赖
AutowiredAnnotationBeanPostProcessor#postProcessProperties
方法:
/*** <p>Note that actual processing of the {@code @Value} annotation is performed* by a {@link org.springframework.beans.factory.config.BeanPostProcessor* BeanPostProcessor} which in turn means that you <em>cannot</em> use* {@code @Value} within* {@link org.springframework.beans.factory.config.BeanPostProcessor* BeanPostProcessor} or* {@link org.springframework.beans.factory.config.BeanFactoryPostProcessor BeanFactoryPostProcessor}* types. Please consult the javadoc for the {@link AutowiredAnnotationBeanPostProcessor}* class (which, by default, checks for the presence of this annotation).**/
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Value {String value();}
我们看一眼 AutowiredAnnotationBeanPostProcessor#postProcessProperties
方法;
public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) {// 查找 @Value 和 @Autowired 注解信息InjectionMetadata metadata = findAutowiringMetadata(beanName, bean.getClass(), pvs);// 利用封装的注解信息反射注入对应的字段信息metadata.inject(bean, beanName, pvs);return pvs;
}
那么核心方法就是 AutowiredAnnotationBeanPostProcessor#findAutowiringMetadata
private InjectionMetadata findAutowiringMetadata(String beanName, Class<?> clazz, @Nullable PropertyValues pvs) {// Fall back to class name as cache key, for backwards compatibility with custom callers.String cacheKey = (StringUtils.hasLength(beanName) ? beanName : clazz.getName());// Quick check on the concurrent map first, with minimal locking.// 从缓存中找出该Bean的依赖注入注解(@Value 和 @Autowired )信息InjectionMetadata metadata = this.injectionMetadataCache.get(cacheKey);if (InjectionMetadata.needsRefresh(metadata, clazz)) {// 双重检测synchronized (this.injectionMetadataCache) {metadata = this.injectionMetadataCache.get(cacheKey);if (InjectionMetadata.needsRefresh(metadata, clazz)) {if (metadata != null) {metadata.clear(pvs);}// 没有就去构建metadata = buildAutowiringMetadata(clazz);this.injectionMetadataCache.put(cacheKey, metadata);}}}return metadata;}
那么核心构建逻辑就来到了 AutowiredAnnotationBeanPostProcessor#buildAutowiringMetadata
private InjectionMetadata buildAutowiringMetadata(final Class<?> clazz) {if (!AnnotationUtils.isCandidateClass(clazz, this.autowiredAnnotationTypes)) {return InjectionMetadata.EMPTY;}List<InjectionMetadata.InjectedElement> elements = new ArrayList<>();Class<?> targetClass = clazz;do {final List<InjectionMetadata.InjectedElement> currElements = new ArrayList<>();// 循环遍历每一个非静态字段ReflectionUtils.doWithLocalFields(targetClass, field -> {MergedAnnotation<?> ann = findAutowiredAnnotation(field);});// 循环遍历每一个非静态方法ReflectionUtils.doWithLocalMethods(targetClass, method -> {Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);MergedAnnotation<?> ann = findAutowiredAnnotation(bridgedMethod);if (ann != null && method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) {boolean required = determineRequiredStatus(ann);PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz);currElements.add(new AutowiredMethodElement(method, required, pd));}});elements.addAll(0, currElements);targetClass = targetClass.getSuperclass();}while (targetClass != null && targetClass != Object.class);return InjectionMetadata.forElements(elements, clazz);}
我们最后看一下AutowiredAnnotationBeanPostProcessor#findAutowiredAnnotation
private final Set<Class<? extends Annotation>> autowiredAnnotationTypes = new LinkedHashSet<>(4);
// 构造器
public AutowiredAnnotationBeanPostProcessor() {// 添加依赖注入的注解this.autowiredAnnotationTypes.add(Autowired.class);this.autowiredAnnotationTypes.add(Value.class);
}private MergedAnnotation<?> findAutowiredAnnotation(AccessibleObject ao) {MergedAnnotations annotations = MergedAnnotations.from(ao);// 循环遍历 autowiredAnnotationTypes,看看方法上是否包含该注解for (Class<? extends Annotation> type : this.autowiredAnnotationTypes) {MergedAnnotation<?> annotation = annotations.get(type);if (annotation.isPresent()) {return annotation;}}return null;
}
⭐️ 从上面的源码分析可以看出,@Value 向Bean注入属性的核心就是:查找每一个字段或者方法上面是否标注了 @Value , @Autowired
注解,之后构建 AutowiredMethodElement
,利用反射去为对应的属性赋值
4.2 @PropertySource 解析时机
这里我再向你展示以下 @PropertyScource 是如何解析的
你是否还记得我在向你介绍这个注解的时候说过它是和 @Configuration 注解配合使用的,这就意味着
解析 @Configuration 的时候也会被解析
,那么核心就是 @Configuration 注解是什么时候解析的呢?
其解析时机是ConfigurationClassPostProcessor#processConfigBeanDefinitions
(这里我只是向你说明以下它的解析时机,由于篇幅原因,我就不解释我是怎样找到这个类的,我会在后面的源码文章详细说明)
那么我们就从 ConfigurationClassPostProcessor#processConfigBeanDefinitions
开始探索:
public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {// Parse each @Configuration classConfigurationClassParser parser = new ConfigurationClassParser(this.metadataReaderFactory, this.problemReporter, this.environment,this.resourceLoader, this.componentScanBeanNameGenerator, registry);Set<BeanDefinitionHolder> candidates = new LinkedHashSet<>(configCandidates);Set<ConfigurationClass> alreadyParsed = new HashSet<>(configCandidates.size());// 开始解析 @Configuration 标注的类parser.parse(candidates);parser.validate();
}
这个方法很长,这里我只展示了和 @PropertySource 相关的部分,该方法调用了 ConfigurationClassParser
配置类解析器的 parse() 方法来解析候选的配置类
public void parse(Set<BeanDefinitionHolder> configCandidates) {parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName());
}
由于我们是注解模式,所以调用了重载的方法来解析 beanDefinition 信息
protected final void parse(AnnotationMetadata metadata, String beanName) throws IOException {processConfigurationClass(new ConfigurationClass(metadata, beanName), DEFAULT_EXCLUSION_FILTER);
}protected void processConfigurationClass(ConfigurationClass configClass, Predicate<String> filter) throws IOException {// ......// Recursively process the configuration class and its superclass hierarchy.SourceClass sourceClass = asSourceClass(configClass, filter);do {// 开始处理配置类sourceClass = doProcessConfigurationClass(configClass, sourceClass, filter);}while (sourceClass != null);this.configurationClasses.put(configClass, configClass);}
从上面可以看到,调用了 doProcessConfigurationClass
来真正处理配置类
protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass, Predicate<String> filter)throws IOException {if (configClass.getMetadata().isAnnotated(Component.class.getName())) {// Recursively process any member (nested) classes firstprocessMemberClasses(configClass, sourceClass, filter);}// Process any @PropertySource annotationsfor (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable(sourceClass.getMetadata(), PropertySources.class,org.springframework.context.annotation.PropertySource.class)) {if (this.environment instanceof ConfigurableEnvironment) {processPropertySource(propertySource);}}// Process any @ComponentScan annotationsSet<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);if (!componentScans.isEmpty() &&!this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {for (AnnotationAttributes componentScan : componentScans) {// The config class is annotated with @ComponentScan -> perform the scan immediatelySet<BeanDefinitionHolder> scannedBeanDefinitions =this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());// Check the set of scanned definitions for any further config classes and parse recursively if neededfor (BeanDefinitionHolder holder : scannedBeanDefinitions) {BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();if (bdCand == null) {bdCand = holder.getBeanDefinition();}if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {parse(bdCand.getBeanClassName(), holder.getBeanName());}}}}// Process any @Import annotationsprocessImports(configClass, sourceClass, getImports(sourceClass), filter, true);// Process any @ImportResource annotationsAnnotationAttributes importResource =AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ImportResource.class);if (importResource != null) {String[] resources = importResource.getStringArray("locations");Class<? extends BeanDefinitionReader> readerClass = importResource.getClass("reader");for (String resource : resources) {String resolvedResource = this.environment.resolveRequiredPlaceholders(resource);configClass.addImportedResource(resolvedResource, readerClass);}}// Process individual @Bean methodsSet<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass);for (MethodMetadata methodMetadata : beanMethods) {configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass));}// Process default methods on interfacesprocessInterfaces(configClass, sourceClass);// Process superclass, if anyif (sourceClass.getMetadata().hasSuperClass()) {String superclass = sourceClass.getMetadata().getSuperClassName();if (superclass != null && !superclass.startsWith("java") &&!this.knownSuperclasses.containsKey(superclass)) {this.knownSuperclasses.put(superclass, configClass);// Superclass found, return its annotation metadata and recursereturn sourceClass.getSuperClass();}}// No superclass -> processing is completereturn null;}
上面这个方法里内含的特别多的内容,我相信能解决你对和 @Configuration 配合使用的注解的所有疑惑(包含了 @PropertySource 注解的处理),所以我并没有进行过多的删减,希望你能理清 @Configuration 以及对应的其它注解的处理时机
5. 总结
这篇文章,我主要向你介绍了:
- @Value 的Bean属性注入原理和使用
- @PropertySource 的解析时机和使用
- 两个注解的配合使用方式
最后,我希望你看完本篇文章后,我希望你掌握如何使用@Value与@PropertySource实现外部化配置注入,也希望你指出我在文章中的错误点,希望我们一起进步,也希望你能给我的文章点个赞,原创不易!
这篇关于Spring注解驱动开发(九):利用@Value与@PropertySource实现外部化配置注入的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!