spring-reading./spring-annotation/spring-annotation-value/README.md

26 KiB
Raw Blame History

@Value

一、基本信息

✒️ 作者 - Lex 📝 博客 - 我的CSDN 📚 文章目录 - 所有文章 🔗 源码地址 - @Value源码

二、注解描述

@Value 注解是一个非常有用的功能它允许我们从配置文件中注入属性值到Java类的字段或方法参数中。这样我们可以将配置和代码分离使应用更容易配置和维护。

三、注解源码

@Value注解是 Spring 框架自 3.1 版本开始引入的一个核心注解主要目的是允许我们在Spring管理的bean中直接注入来自各种源如属性文件、系统属性等的值而不需要显式地编写代码来解析这些值。

/**
 * 用于字段或方法/构造函数参数级别的注解,
 * 表示被注解元素的默认值表达式。
 *
 * 通常用于基于表达式或属性的依赖注入。
 * 也支持动态解析处理器方法参数例如在Spring MVC中。
 *
 * 常见的使用场景是使用 `#{systemProperties.myProp}` 这样的SpELSpring表达式语言表达式来注入值。
 * 或者,使用 `${my.app.myProp}` 这样的属性占位符来注入值。
 *
 * 注意,@Value 注解的实际处理是由 BeanPostProcessor 执行的,
 * 这意味着我们**不能**在 BeanPostProcessor 或 BeanFactoryPostProcessor 类型中使用 @Value。
 * 请查阅 AutowiredAnnotationBeanPostProcessor 类的javadoc默认检查此注解的存在 *
 * @author Juergen Hoeller
 * @since 3.0
 * @see AutowiredAnnotationBeanPostProcessor
 * @see Autowired
 * @see org.springframework.beans.factory.config.BeanExpressionResolver
 * @see org.springframework.beans.factory.support.AutowireCandidateResolver#getSuggestedValue
 */
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Value {

    /**
     * 实际的值表达式,如 `#{systemProperties.myProp}`
     * 或属性占位符,如 `${my.app.myProp}`。
     */
    String value();
}

四、主要功能

  1. 提供属性注入
    • 允许从不同的配置源(如属性文件、系统属性等)直接向 Spring 管理的 beans 中注入值。
  2. 支持表达式
    • SpEL (Spring Expression Language) 表达式:例如,#{systemProperties.myProp} 可以从系统属性中获取名为 myProp 的值。
    • 属性占位符:例如,${my.app.myProp} 可以从预定义的配置源,如 application.propertiesapplication.yml 文件,获取名为 my.app.myProp 的属性值。
  3. 动态值解析
    • 与只能在启动时设置静态值相比,@Value 注解可以解析动态表达式,从而为字段或构造函数参数提供动态值。
  4. 用于字段、方法参数、构造函数参数和注解
    • 它可以被应用到这些元素上,以提供必要的值。
  5. 与其他注解协同工作
    • 尽管 @Value 本身是用于注入值的,但它经常与其他如 @Component@Service@Controller 之类的 Spring 注解一起使用,以创建完全由 Spring 管理和配置的 beans。
  6. 与属性解析器配合
    • 为了正确解析 @Value 中的表达式Spring 应用上下文中需要有一个属性解析器,例如 PropertySourcesPlaceholderConfigurer。在 Spring Boot 项目中,这已经默认配置好了。

五、最佳实践

首先来看看启动类入口,上下文环境使用AnnotationConfigApplicationContext此类是使用Java注解来配置Spring容器的方式构造参数我们给定了一个MyConfiguration组件类。

public class ValueApplication {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MyConfiguration.class);
    }
}

这里使用@Bean注解定义了一个Bean是为了确保 MyService 被 Spring 容器执行,另外使用@PropertySource注解从类路径下的application.properties文件中加载属性。这意味着我们可以在这个文件中定义属性,然后在应用中使用Environment对象来访问它们。

@Configuration
@PropertySource("classpath:application.properties")
public class MyConfiguration {

    @Bean
    public MyService myService(){
        return new MyService();
    }
}

application.properties文件在src/main/resources目录中,并添加以下内容。

app.name=My Spring Application
app.servers=server1,server2,server3
app.val1=10
app.val2=20

MyService类,展示了如何使用@Value注解的五种不同方式进行属性注入。

public class MyService implements InitializingBean {

    /**
     * 直接注入值
     */
    @Value("Some String Value")
    private String someString;

    /**
     * 从属性文件中注入值方式
     */
    @Value("${app.name}")
    private String appName;

    /**
     * 使用默认值方式
     */
    @Value("${app.description:我是默认值}")
    private String appDescription;

    /**
     * 注入列表和属性
     */
    @Value("#{'${app.servers}'.split(',')}")
    private List<String> servers;

    /**
     * 使用Spring的SpEL
     */
    @Value("#{${app.val1} + ${app.val2}}")
    private int sumOfValues;

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("直接注入值: " + someString);
        System.out.println("从属性文件中注入值: " + appName);
        System.out.println("使用默认值: " + appDescription);
        System.out.println("注入列表和属性: " + servers);
        System.out.println("使用Spring的SpEL: " + sumOfValues);
    }
}

运行结果发现, @Value 注解都被正确地解析并注入了预期的值。

直接注入值: Some String Value
从属性文件中注入值: My Spring Application
使用默认值: 我是默认值
注入列表和属性: [server1, server2, server3]
使用SpringSpEL: 30

六、时序图

sequenceDiagram
Title: @Value注解时序图
AbstractAutowireCapableBeanFactory->>AbstractAutowireCapableBeanFactory:applyMergedBeanDefinitionPostProcessors(mbd,beanType,beanName)<br>应用Bean定义的后置处理器
AbstractAutowireCapableBeanFactory->>AutowiredAnnotationBeanPostProcessor:postProcessMergedBeanDefinition(beanDefinition,beanType,beanName)<br>处理已合并的Bean定义
AutowiredAnnotationBeanPostProcessor->>AutowiredAnnotationBeanPostProcessor:findAutowiringMetadata(beanName,clazz,pvs)<br>查找自动注入的元数据
AutowiredAnnotationBeanPostProcessor->>AutowiredAnnotationBeanPostProcessor:buildAutowiringMetadata(clazz)<br>构建自动注入的元数据
AutowiredAnnotationBeanPostProcessor->>ReflectionUtils:doWithLocalFields(clazz,fc)<br>处理类的本地字段
ReflectionUtils->>AutowiredAnnotationBeanPostProcessor:解析有@Value注解的字段
AutowiredAnnotationBeanPostProcessor->>ReflectionUtils:doWithLocalMethods(clazz,fc)<br>处理类的本地方法
ReflectionUtils->>AutowiredAnnotationBeanPostProcessor:解析有@Value注解的方法
AutowiredAnnotationBeanPostProcessor->>AutowiredAnnotationBeanPostProcessor:injectionMetadataCache.put(cacheKey, metadata)<br>将元数据存入缓存
AbstractAutowireCapableBeanFactory->>AbstractAutowireCapableBeanFactory:populateBean(beanName,mbd,bw)<br>填充Bean的属性值
AbstractAutowireCapableBeanFactory->>AutowiredAnnotationBeanPostProcessor:postProcessProperties(pvs,bean,beanName)<br>后处理Bean的属性
AutowiredAnnotationBeanPostProcessor->>AutowiredAnnotationBeanPostProcessor:findAutowiringMetadata(beanName,clazz,pvs)<br>再次查找自动注入的元数据
Note right of AutowiredAnnotationBeanPostProcessor:<br>从缓存中获取注入的元数据
AutowiredAnnotationBeanPostProcessor->>InjectionMetadata:inject(bean, beanName, pvs)<br>执行实际的属性注入
InjectionMetadata->>AutowiredFieldElement:inject(target, beanName, pvs)<br>注入特定的字段元素
AutowiredFieldElement->>AutowiredFieldElement:resolveFieldValue(field,bean,beanName)<br>解析字段的值
AutowiredFieldElement->>DefaultListableBeanFactory:resolveDependency(desc, beanName, autowiredBeanNames, typeConverter)<br>解析字段的依赖
DefaultListableBeanFactory->>AutowiredFieldElement:返回解析后的value值<br>返回解析后的属性值
AutowiredFieldElement->>Field:field.set(bean, value)<br>设置Bean字段的值

七、源码分析

前置条件

在Spring中AutowiredAnnotationBeanPostProcessor是处理@Value等注解的关键类,它实现了下述两个接口。因此,为了深入理解@Value的工作方式,研究这个类是非常有用的。简而言之,为了完全理解@Value的工作机制了解下述接口确实是必要的。这两个接口提供了对bean生命周期中关键阶段的干预从而允许进行属性注入和其他相关的操作。

  1. MergedBeanDefinitionPostProcessor接口
    • 此接口提供的postProcessMergedBeanDefinition方法允许后处理器修改合并后的bean定义。合并后的bean定义是一个已经考虑了所有父bean定义属性的bean定义。对于@Value注解的处理,这一步通常涉及到收集需要被解析的@Value注解信息并准备对其进行后续处理。
    • 🔗 MergedBeanDefinitionPostProcessor接口传送门
  2. InstantiationAwareBeanPostProcessor接口
    • 此接口提供了几个回调方法允许后处理器在bean实例化之前和实例化之后介入bean的创建过程。特别是postProcessProperties方法允许后处理器对bean的属性进行操作。对于@Value注解这通常涉及到实际地解析注解中的表达式或属性占位符并将解析得到的值注入到bean中。
    • 🔗 InstantiationAwareBeanPostProcessor接口传送门

收集阶段

org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor#postProcessMergedBeanDefinition方法中主要确保给定的bean定义与其预期的自动装配元数据一致。

@Override
public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName) {
    // 对于给定的bean名称和类型它首先尝试查找相关的InjectionMetadata这可能包含了该bean的字段和方法的注入信息
    InjectionMetadata metadata = findAutowiringMetadata(beanName, beanType, null);
    
    // 使用找到的InjectionMetadata来验证bean定义中的配置成员是否与预期的注入元数据匹配。
    metadata.checkConfigMembers(beanDefinition);
}

org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor#findAutowiringMetadata方法中确保了始终为给定的bean名称和类获取最新和相关的InjectionMetadata,并利用缓存机制优化性能。

private InjectionMetadata findAutowiringMetadata(String beanName, Class<?> clazz, @Nullable PropertyValues pvs) {
    // 如果beanName为空则使用类名作为缓存键。
    String cacheKey = (StringUtils.hasLength(beanName) ? beanName : clazz.getName());
    // 首先尝试从并发缓存中获取InjectionMetadata。
    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);
                }
                // 为给定的类构建新的InjectionMetadata。
                metadata = buildAutowiringMetadata(clazz);
                // 将新构建的元数据更新到缓存中。
                this.injectionMetadataCache.put(cacheKey, metadata);
            }
        }
    }
    // 返回找到的或新构建的元数据。
    return metadata;
}

org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor#buildAutowiringMetadata方法中,查找类及其所有父类中的字段和方法,以找出所有带有自动装配注解的字段和方法,并为它们创建一个统一的InjectionMetadata对象。

private InjectionMetadata buildAutowiringMetadata(final Class<?> clazz) {
    // 检查类是否含有自动装配注解若无则直接返回空的InjectionMetadata。
    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);
            if (ann != null) {
                // 忽略静态字段。
                if (Modifier.isStatic(field.getModifiers())) {
                    if (logger.isInfoEnabled()) {
                        logger.info("Autowired annotation is not supported on static fields: " + field);
                    }
                    return;
                }
                boolean required = determineRequiredStatus(ann);
                // 创建一个新的AutowiredFieldElement并加入到列表。
                currElements.add(new AutowiredFieldElement(field, required));
            }
        });

        // 处理类中的所有方法。
        ReflectionUtils.doWithLocalMethods(targetClass, method -> {
            Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
            if (!BridgeMethodResolver.isVisibilityBridgeMethodPair(method, bridgedMethod)) {
                return;
            }
            // 查找方法上的自动装配注解。
            MergedAnnotation<?> ann = findAutowiredAnnotation(bridgedMethod);
            if (ann != null && method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) {
                // 忽略静态方法。
                if (Modifier.isStatic(method.getModifiers())) {
                    if (logger.isInfoEnabled()) {
                        logger.info("Autowired annotation is not supported on static methods: " + method);
                    }
                    return;
                }
                // 只处理带参数的方法。
                if (method.getParameterCount() == 0) {
                    if (logger.isInfoEnabled()) {
                        logger.info("Autowired annotation should only be used on methods with parameters: " +
                                    method);
                    }
                }
                boolean required = determineRequiredStatus(ann);
                PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz);
                // 创建一个新的AutowiredMethodElement并加入到列表。
                currElements.add(new AutowiredMethodElement(method, required, pd));
            }
        });

        // 将当前类的注入元素加入到总的注入元素列表的开头。
        elements.addAll(0, currElements);
        // 处理父类。
        targetClass = targetClass.getSuperclass();
    }
    // 循环直至Object类。
    while (targetClass != null && targetClass != Object.class);

    // 返回为元素列表创建的新的InjectionMetadata。
    return InjectionMetadata.forElements(elements, clazz);
}

org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor#autowiredAnnotationTypes字段中,主要的用途是告诉AutowiredAnnotationBeanPostProcessor哪些注解它应该处理。当Spring容器解析bean定义并创建bean实例时如果这个bean的字段、方法或构造函数上的注解被包含在这个autowiredAnnotationTypes集合中,那么AutowiredAnnotationBeanPostProcessor就会对它进行处理。

public AutowiredAnnotationBeanPostProcessor() {
   this.autowiredAnnotationTypes.add(Autowired.class);
   this.autowiredAnnotationTypes.add(Value.class);
   // ... [代码部分省略以简化]
}

注入阶段

org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor#postProcessProperties方法中用于处理bean属性的后处理特别是通过@Value等注解进行的属性注入。

@Override
public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) {
    // 获取与bean名称和类相关的InjectionMetadata。
    // 这包括该bean需要进行注入的所有字段和方法。
    InjectionMetadata metadata = findAutowiringMetadata(beanName, bean.getClass(), pvs);
    
    try {
        // 使用获取到的InjectionMetadata实际进行属性的注入。
        metadata.inject(bean, beanName, pvs);
    }
    // 如果在注入过程中出现BeanCreationException直接抛出。
    catch (BeanCreationException ex) {
        throw ex;
    }
    // 捕获其他异常并以BeanCreationException的形式抛出提供详细的错误信息。
    catch (Throwable ex) {
        throw new BeanCreationException(beanName, "Injection of autowired dependencies failed", ex);
    }
    // 返回原始的PropertyValues因为这个方法主要关注依赖注入而不是修改属性。
    return pvs;
}

org.springframework.beans.factory.annotation.InjectionMetadata#inject方法中,主要目的是将所有需要注入的元素(例如带有@Value等注解的字段或方法注入到目标bean中。

public void inject(Object target, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
    // 获取已经检查的元素。通常,在初始化阶段,所有的元素都会被检查一次。
    Collection<InjectedElement> checkedElements = this.checkedElements;

    // 如果已经有检查过的元素,则使用它们,否则使用所有注入的元素。
    Collection<InjectedElement> elementsToIterate =
        (checkedElements != null ? checkedElements : this.injectedElements);

    // 如果有需要注入的元素...
    if (!elementsToIterate.isEmpty()) {
        // 遍历每个元素并注入到目标bean中。
        for (InjectedElement element : elementsToIterate) {
            // 对每个元素(字段或方法)执行注入操作。
            element.inject(target, beanName, pvs);
        }
    }
}

org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.AutowiredFieldElement#inject方法中首先检查字段的值是否已经被缓存。如果已缓存则从缓存中获取否则重新解析。然后它确保字段是可访问的特别是对于私有字段并将解析的值设置到目标bean的相应字段中。

@Override
protected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
    // 获取代表带有@Autowired注解的字段的Field对象。
    Field field = (Field) this.member;

    Object value;
    // 如果字段的值已经被缓存(即先前已解析过),则尝试从缓存中获取。
    if (this.cached) {
        try {
            // 从缓存中获取已解析的字段值。
            value = resolvedCachedArgument(beanName, this.cachedFieldValue);
        }
        catch (NoSuchBeanDefinitionException ex) {
            // 如果缓存中的bean已被意外删除 -> 重新解析。
            value = resolveFieldValue(field, bean, beanName);
        }
    }
    else {
        // 如果字段值未被缓存,直接解析。
        value = resolveFieldValue(field, bean, beanName);
    }

    // 如果解析到的值不为null...
    if (value != null) {
        // 使字段可访问这是必要的特别是当字段是private时。
        ReflectionUtils.makeAccessible(field);
        // 实际将解析的值注入到目标bean的字段中。
        field.set(bean, value);
    }
}

org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.AutowiredFieldElement#resolveFieldValue方法中,通过beanFactory.resolveDependency方法从Spring的bean工厂中解析字段的值。

@Nullable
private Object resolveFieldValue(Field field, Object bean, @Nullable String beanName) {
    // ... [代码部分省略以简化]
    Object value;
    try {
        // 通过`beanFactory.resolveDependency`方法从Spring的bean工厂中解析字段的值
        value = beanFactory.resolveDependency(desc, beanName, autowiredBeanNames, typeConverter);
    }
    catch (BeansException ex) {
        throw new UnsatisfiedDependencyException(null, beanName, new InjectionPoint(field), ex);
    }
    // ... [代码部分省略以简化]
    return value;
}

八、注意事项

  1. SpEL表达式
    • @Value可以用来解析Spring Expression Language (SpEL) 表达式。确保我们的表达式是正确的,以防止运行时错误。
  2. 默认值
    • 我们可以为@Value注解提供默认值,以防止某个属性在属性文件中未被定义。例如:@Value("${some.property:default}")
  3. 类型转换
    • 确保@Value提供的值可以被转换为字段或方法参数的类型。Spring会尝试自动进行这种转换但不一定总是成功。
  4. 不适用于复杂类型
    • 尽管@Value可以用于简单的类型如字符串、整数、枚举等但不应用于复杂的bean注入这时应该使用@Autowired@Inject
  5. 不可用于BeanPostProcessorBeanFactoryPostProcessor
    • @Value注解在BeanPostProcessorBeanFactoryPostProcessor实现中是不起作用的因为它们在Spring容器生命周期中的处理时机早于@Value的处理。
  6. 占位符解析器的配置
    • 要使用属性占位符(如${property.name}),需要确保已配置了PropertySourcesPlaceholderConfigurerPropertyPlaceholderConfigurer
  7. 环境变量与系统属性
    • 我们可以使用@Value来引用环境变量或系统属性,例如:@Value("${JAVA_HOME}")
  8. 防止注入敏感信息
    • 不要使用@Value来注入敏感信息如密码除非它们是适当加密的。考虑使用专门的解决方案如Spring Cloud Config的Vault集成。
  9. 循环依赖
    • 尽管与@Autowired不同,但需要注意的是,使用@Value可能间接导致循环依赖尤其是当注入的值是其他bean的属性时。
  10. 性能考虑
    • 大量使用SpEL表达式可能对性能产生轻微的影响因为这些表达式需要在运行时进行解析。

九、总结

最佳实践总结

  1. 启动类入口
    • 使用AnnotationConfigApplicationContext来启动Spring上下文该上下文支持基于Java注解的配置。
    • 在创建上下文时,为其提供了MyConfiguration作为配置类。
  2. 配置类
    • MyConfiguration类标记为@Configuration表示它提供了bean定义的配置信息。
    • 使用@PropertySource指定一个属性文件application.properties来为上下文加载属性。
    • 定义了一个beanMyService确保其在Spring容器中被创建和初始化。
  3. 属性文件
    • application.properties文件中定义了几个属性,这些属性可以在应用程序中使用。
  4. 属性注入
    • MyService类中,展示了如何使用@Value注解进行五种不同方式的属性注入从直接注入字符串值到使用SpEL表达式。
  5. 注入结果的验证
    • 实现InitializingBean接口并重写afterPropertiesSet方法来验证注入的属性值。
    • 运行应用后,该方法会打印出所有注入属性的值,从而验证@Value注解正确地解析并注入了预期的值。

源码分析总结

  1. 核心后处理器
    • AutowiredAnnotationBeanPostProcessor是处理@Value等注解的主要后处理器。它实现了两个关键的接口,MergedBeanDefinitionPostProcessorInstantiationAwareBeanPostProcessor这两个接口允许在bean的生命周期中的关键阶段进行干预为属性注入提供了机制。
  2. 收集阶段
    • postProcessMergedBeanDefinition方法中后处理器确保给定的bean定义与预期的自动装配元数据一致。主要任务是为给定的bean名称和类型查找相关的InjectionMetadata这可能包含了该bean的字段和方法的注入信息。
  3. 注入阶段
    • postProcessProperties方法中后处理器处理bean属性的注入特别是通过@Value进行的注入。具体来说它会获取与bean名称和类相关的InjectionMetadata,然后使用这些元数据来注入属性。
  4. 字段注入
    • AutowiredFieldElement#inject方法中首先会检查字段的值是否已经被缓存。如果已缓存则从缓存中获取否则重新解析。然后将解析的值注入到目标bean的字段中。
  5. 值解析
    • AutowiredFieldElement#resolveFieldValue方法中后处理器从Spring的bean工厂中解析字段的值。它主要通过beanFactory.resolveDependency方法来完成这一工作。