Spring注解驱动开发(九):利用@Value与@PropertySource实现外部化配置注入

本文主要是介绍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();
}

我把核心注释放在了代码里面,从源码中我们可以得到以下信息:

  1. @Value 注解主要用在 字段、方法、构造器参数级别
  2. @Value 有两种用法:
    1. #{systemProperties.myProp} : SpEl 表达式
    2. ${my.app.myProp} : 用于注入配置文件中的值

接下来我就详细讲述一下@Value 的两种常见用法

2.1 非配置文件注入属性

我们可以利用 @Value 注解将外部的值动态注入到bean的属性中,我这里主要介绍一下几种用法

  1. 注入字符串等常量
  2. 注入操作系统参数
  3. 注入SpEl 表达式结果
  4. 注入其它 Bean 的属性
  5. 注入配置文件

我们先定义一个 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实现外部化配置注入的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/260962

相关文章

基于SpringBoot+Mybatis实现Mysql分表

《基于SpringBoot+Mybatis实现Mysql分表》这篇文章主要为大家详细介绍了基于SpringBoot+Mybatis实现Mysql分表的相关知识,文中的示例代码讲解详细,感兴趣的小伙伴可... 目录基本思路定义注解创建ThreadLocal创建拦截器业务处理基本思路1.根据创建时间字段按年进

SpringBoot3实现Gzip压缩优化的技术指南

《SpringBoot3实现Gzip压缩优化的技术指南》随着Web应用的用户量和数据量增加,网络带宽和页面加载速度逐渐成为瓶颈,为了减少数据传输量,提高用户体验,我们可以使用Gzip压缩HTTP响应,... 目录1、简述2、配置2.1 添加依赖2.2 配置 Gzip 压缩3、服务端应用4、前端应用4.1 N

Java编译生成多个.class文件的原理和作用

《Java编译生成多个.class文件的原理和作用》作为一名经验丰富的开发者,在Java项目中执行编译后,可能会发现一个.java源文件有时会产生多个.class文件,从技术实现层面详细剖析这一现象... 目录一、内部类机制与.class文件生成成员内部类(常规内部类)局部内部类(方法内部类)匿名内部类二、

SpringBoot实现数据库读写分离的3种方法小结

《SpringBoot实现数据库读写分离的3种方法小结》为了提高系统的读写性能和可用性,读写分离是一种经典的数据库架构模式,在SpringBoot应用中,有多种方式可以实现数据库读写分离,本文将介绍三... 目录一、数据库读写分离概述二、方案一:基于AbstractRoutingDataSource实现动态

Python FastAPI+Celery+RabbitMQ实现分布式图片水印处理系统

《PythonFastAPI+Celery+RabbitMQ实现分布式图片水印处理系统》这篇文章主要为大家详细介绍了PythonFastAPI如何结合Celery以及RabbitMQ实现简单的分布式... 实现思路FastAPI 服务器Celery 任务队列RabbitMQ 作为消息代理定时任务处理完整

Springboot @Autowired和@Resource的区别解析

《Springboot@Autowired和@Resource的区别解析》@Resource是JDK提供的注解,只是Spring在实现上提供了这个注解的功能支持,本文给大家介绍Springboot@... 目录【一】定义【1】@Autowired【2】@Resource【二】区别【1】包含的属性不同【2】@

springboot循环依赖问题案例代码及解决办法

《springboot循环依赖问题案例代码及解决办法》在SpringBoot中,如果两个或多个Bean之间存在循环依赖(即BeanA依赖BeanB,而BeanB又依赖BeanA),会导致Spring的... 目录1. 什么是循环依赖?2. 循环依赖的场景案例3. 解决循环依赖的常见方法方法 1:使用 @La

Java枚举类实现Key-Value映射的多种实现方式

《Java枚举类实现Key-Value映射的多种实现方式》在Java开发中,枚举(Enum)是一种特殊的类,本文将详细介绍Java枚举类实现key-value映射的多种方式,有需要的小伙伴可以根据需要... 目录前言一、基础实现方式1.1 为枚举添加属性和构造方法二、http://www.cppcns.co

使用Python实现快速搭建本地HTTP服务器

《使用Python实现快速搭建本地HTTP服务器》:本文主要介绍如何使用Python快速搭建本地HTTP服务器,轻松实现一键HTTP文件共享,同时结合二维码技术,让访问更简单,感兴趣的小伙伴可以了... 目录1. 概述2. 快速搭建 HTTP 文件共享服务2.1 核心思路2.2 代码实现2.3 代码解读3.

Elasticsearch 在 Java 中的使用教程

《Elasticsearch在Java中的使用教程》Elasticsearch是一个分布式搜索和分析引擎,基于ApacheLucene构建,能够实现实时数据的存储、搜索、和分析,它广泛应用于全文... 目录1. Elasticsearch 简介2. 环境准备2.1 安装 Elasticsearch2.2 J