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

发布于:2022-12-15 ⋅ 阅读:(521) ⋅ 点赞:(0)


这是我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;


    @Override
    public String toString() {
        return "ValueBean{" +
                "name='" + name + '\'' +
                ", systemPropertiesName='" + systemPropertiesName + '\'' +
                ", randomNumber=" + randomNumber +
                ", username='" + username + '\'' +
                ", properties=" + properties +
                '}';
    }
}

可以看到我这里都是用的属性注入,你也可以试一试其它的方法注入等((官网示例))
接下来我们向容器中注入 Coupon 和 ValueBean:

@Configuration
public class CouponConfig {

    @Bean
    public Coupon coupon() {
        return new Coupon(1,"满减",90);
    }

    @Bean
    ValueBean 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 {

    @Bean
    public Coupon coupon() {
        return new Coupon();
    }

    @Bean
    ValueBean 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 class
	ConfigurationClassParser 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 first
			processMemberClasses(configClass, sourceClass, filter);
		}

		// Process any @PropertySource annotations
		for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable(
				sourceClass.getMetadata(), PropertySources.class,
				org.springframework.context.annotation.PropertySource.class)) {
			if (this.environment instanceof ConfigurableEnvironment) {
				processPropertySource(propertySource);
			}
		}

		// Process any @ComponentScan annotations
		Set<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 immediately
				Set<BeanDefinitionHolder> scannedBeanDefinitions =
						this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
				// Check the set of scanned definitions for any further config classes and parse recursively if needed
				for (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 annotations
		processImports(configClass, sourceClass, getImports(sourceClass), filter, true);

		// Process any @ImportResource annotations
		AnnotationAttributes 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 methods
		Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass);
		for (MethodMetadata methodMetadata : beanMethods) {
			configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass));
		}

		// Process default methods on interfaces
		processInterfaces(configClass, sourceClass);

		// Process superclass, if any
		if (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 recurse
				return sourceClass.getSuperClass();
			}
		}

		// No superclass -> processing is complete
		return null;
	}

上面这个方法里内含的特别多的内容,我相信能解决你对和 @Configuration 配合使用的注解的所有疑惑(包含了 @PropertySource 注解的处理),所以我并没有进行过多的删减,希望你能理清 @Configuration 以及对应的其它注解的处理时机

5. 总结

这篇文章,我主要向你介绍了:

  • @Value 的Bean属性注入原理和使用
  • @PropertySource 的解析时机和使用
  • 两个注解的配合使用方式

最后,我希望你看完本篇文章后,我希望你掌握如何使用@Value与@PropertySource实现外部化配置注入,也希望你指出我在文章中的错误点,希望我们一起进步,也希望你能给我的文章点个赞,原创不易!


网站公告

今日签到

点亮在社区的每一天
去签到