SpringBoot解析@Value注解型解析注入时机及原理分析

 更新时间:2024年12月10日 09:34:02   作者:流光的咸鱼  
@Value注解可以用来将外部的值动态注入到Bean中,可以获取配置文件中的属性值和通过SpEl表达式获取bean的属性或方法

@Value的使用

@Value 注解可以用来将外部的值动态注入到 Bean 中,在 @Value 注解中,可以使${} 与 #{} ,它们的区别如下:

(1)@Value("${}"):可以获取对应属性文件中定义的属性值。

(2)@Value("#{}"):表示 SpEl 表达式通常用来获取 bean 的属性,或者调用 bean 的某个方法。

根据注入的内容来源,@ Value属性注入功能可以分为两种:通过配置文件进行属性注入和通过非配置文件进行属性注入。

基于配置文件的注入

单个注入

application.properties
user.nick = mimi
@RestController
public class DemoController {

    @Value("${user.nick:如果需要默认值格式(:默认值)}")
    private String name;
}

注意点:当文件类型是 xx.properties是如果存在中文的话,比如:

application.properties
user.nick = 秘密

就会出现乱码,这是因为在SpringBoot的CharacterReader类中,默认的编码格式是ISO-8859-1,该类负责.properties文件中系统属性的读取。如果系统属性包含中文字符,就会出现乱码。

解决的方法大概有三种:

(1)改为yml、yaml格式的;(2)配置文件里中文转义掉;(3)自己使用的时候手动转换

这里说下yml为什么可以,因为.yml或.yaml格式的配置文件,最终会使用UnicodeReader类进行解析,它的init方法中,首先读取BOM文件头信息,如果头信息中有UTF8、UTF16BE、UTF16LE,就采用对应的编码,如果没有,则采用默认UTF8编码。

静态变量注入

默认被static修饰的变量通过@Value会注入失败,我们注解可以写到方法上:

private static String name;
@Value("${user.nick:默认值}")
public void setName(String name) {
    this.name = name;
}

非配置文件的注入

基本类型

@Value注解对这8中基本类型和相应的包装类,有非常良好的支持,例如:

@Value("${user.test:30000}")
private Integer size;
@Value("${user.test:30000}")
private int size;

数组

但只用上面的基本类型是不够的,特别是很多需要批量处理数据的场景中。这时候可以使用数组,它在日常开发中使用的频率很高。我们在定义数组时可以这样写:

@Value("${cs.test:1,2,3,4,5}")
private int[] array;

spring默认使用逗号分隔参数值。如果用空格分隔,例如:@Value("${kuku.test:1 2 3 4 5}") spring会自动把空格去掉,导致数据中只有一个值:12345,所以注意千万别搞错了。

如果我们把数组定义成:short、int、long、char、string类型,spring是可以正常注入属性值的。

但如果把数组定义成:float、double类型,启动项目时就会直接报错。如果使用int的包装类Integer[],比如:

@Value("${cs.test:1,2,3,4,5}")
private Integer[] array;

启动项目时同样会报异常。此外,定义数组时一定要注意属性值的类型,必须完全一致才可以,如果出现下面这种情况:

@Value("${cs.test:1.0,aa,3,4,5}")
private int[] array;

属性值中包含了1.0和aa,显然都无法将该字符串转换成int。

集合类

List是如何注入属性值的:

user.test = 10,11,12,13
@Value("${cs.test}")
private List<Integer> test;

其它

注入bean,一般都是用的@Autowired或者@Resource注解,@Value注解也可以注入bean,它是这么做的:

@Value("#{roleService}")
private RoleService roleService;

通过EL表达式,@Value注解已经可以注入bean了。既然能够拿到bean实例,接下来,可以再进一步,获取成员变量、常量、方法、静态方法:

@Value("#{roleService.DEFAULT_AGE}")
private int myAge;

前面的内容都是基于bean的,但有时我们需要调用静态类,比如:Math、xxxUtil等静态工具类的方法,可以这么写:

// 注入系统的路径分隔符到path中
@Value("#{T(java.io.File).separator}")
private String path;
// 注入一个随机数到randomValue中
@Value("#{T(java.lang.Math).random()}")
private double randomValue;

还可以进行逻辑运算:

// 拼接
@Value("#{roleService.roleName + '' + roleService.DEFAULT_AGE}")
private String value;
// 逻辑判断
@Value("#{roleService.DEFAULT_AGE > 16 and roleService.roleName.equals('苏三')}")
private String operation;

上面这么多@Value的用法,归根揭底就是${}和#{}的用法,我们来看看两者的区别:

  • ${}:主要用于获取配置文件中的系统属性值,可以设置默认值。如果在配置文件中找不到属性的配置,则注入时用默认值,如果都没有会直接报错
  • #{}:主要用于通过spring的EL表达式,获取bean的属性,或者调用bean的某个方法,还有调用类的静态常量和静态方法,如果是调用类的静态方法,则需要加T(包名 + 方法名称)。

AutowiredAnnotationBeanPostProcessor 类介绍

首先解析的都是我们的Spring管理的Bean,我们的Bean又有配置型Configuration、服务型Controller、Service等的,但他们都是@Component的,那解析@Value的时候是什么时候呢,其实就是创建Bean的时候,也就是实例化的时候,而实例化又分懒加载的和随着SpringBoot启动就会创建的在刷新方法里的 finishBeanFactoryInitialization 会对不是懒加载的Bean进行实例化,这就涉及到Bean的生命周期啦,其实解析和属性注入都是通过后置处理器进行的。

  • 解析:doCreateBean 方法里的 applyMergedBeanDefinitionPostProcessors执行后置处理器进行收集,实际收集的处理器是:AutowiredAnnotationBeanPostProcessor
  • 注入:populateBean 方法里的 postProcessProperties 执行后置处理器进行注入,实际注入的处理器还是:AutowiredAnnotationBeanPostProcessor

我们先看下 AutowiredAnnotationBeanPostProcessor类图:

这个后置处理是什么时候加载进来的呢?我们来看下:

@Value 解析

AutowiredAnnotationBeanPostProcessor 构造方法:

可以看到实例化的时候,已经把 @Autowired和@Value初始化到 autowiredAnnotationTypes 集合中了。

我们先看下解析的方法:

主要方法就是 findAutowiringMetadata,我们进去看一下:

核心方法就是 buildAutowiringMetadata,进行分析我们进去看看:

private InjectionMetadata buildAutowiringMetadata(Class<?> clazz) {

        // 如果没有 Autowired Value 注解信息就返回 EMPTY
		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<>();
            // 遍历Class中的所有field,根据注解判断每个field是否需要被注入
			ReflectionUtils.doWithLocalFields(targetClass, field -> {
            // 看看field是不是有注解@Autowired 或 @Value
				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;
					}
                    // 确定带注解的字段是否存在required并且是true 默认是true
					boolean required = determineRequiredStatus(ann);
                    // AutowiredFieldElement 对象包装一下
					currElements.add(new AutowiredFieldElement(field, required));
				}
			});
            // 遍历Class中的所有method,根据注解判断每个method是否需要注入
			ReflectionUtils.doWithLocalMethods(targetClass, method -> {
                // 这个方法会查找给定方法的桥接方法。桥接方法用于处理子类重写泛型方法的情况,以保证编译后的代码在运行时的类型安全。
				Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
                // 这个方法用于检查 method 和其找到的 bridgedMethod 是否是可见的桥接方法对。也就是说,它会检查这两个方法的访问权限是否匹配。这一行的意思是,如果 method 和 bridgedMethod 不是可见的桥接方法对,那么就退出当前方法,不继续执行后续代码。这确保了只有在 method 和 bridgedMethod 之间的可见性匹配时,才会继续进行后续的逻辑处理。
				if (!BridgeMethodResolver.isVisibilityBridgeMethodPair(method, bridgedMethod)) {
					return;
				}
                // 看看方法是不是有注解@Autowired 或 @Value
				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);
						}
					}
                    // 判断是不是有 required
					boolean required = determineRequiredStatus(ann);
                    // 获取目标class中某成员拥有读或写方法与桥接方法一致的PropertyDescriptor
					PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz);
                    // AutowiredMethodElement 对象包装一下
					currElements.add(new AutowiredMethodElement(method, required, pd));
				}
			});

			elements.addAll(0, currElements);
            // 递归调用
			targetClass = targetClass.getSuperclass();
		}
		while (targetClass != null && targetClass != Object.class);
        // 包装成 InjectionMetadata 对象  targetClass属性就是当前的类   injectedElements属性就是分析的字段或者方法
		return InjectionMetadata.forElements(elements, clazz);
	}

可以看到会对类的方法的属性进行遍历以及父亲的递归,对于字段会忽略掉static修饰的,对于方法会也会忽略掉static以及参数为空的。最后解析到的属性会包装成 AutowiredFieldElement ,方法会包装成 AutowiredMethodElement ,最后统一放进集合中,包装成 InjectionMetadata 对象返回,并放进缓存。

我们拿个例子:

@Value("${cs.list}")
private List<Integer> list;
private static String name;
@Value("${cs.mimi}")
public void setName(String name) {
    this.name = name;
}

@Value 注入

这里针对上述的例子进行注入

可以看到最后 element.inject 就是在解析阶段调用对应的注入方法进行注入。

AutowiredFieldElement # inject 属性注入

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • Java 选择、冒泡排序、折半查找(实例讲解)

    Java 选择、冒泡排序、折半查找(实例讲解)

    下面小编就为大家带来一篇Java 选择、冒泡排序、折半查找(实例讲解)。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-08-08
  • Netty中序列化的作用及自定义协议详解

    Netty中序列化的作用及自定义协议详解

    这篇文章主要介绍了Netty中序列化的作用及自定义协议详解,Netty自身就支持很多种协议比如Http、Websocket等等,但如果用来作为自己的RPC框架通常会自定义协议,所以这也是本文的重点,需要的朋友可以参考下
    2023-12-12
  • Java复合语句的使用方法详解

    Java复合语句的使用方法详解

    这篇文章主要介绍了Java编程中复合语句,结合相关的具体实例介绍了其用法,需要的朋友可以参考下
    2017-09-09
  • Java中Synchronized锁的使用和原理详解

    Java中Synchronized锁的使用和原理详解

    这篇文章主要介绍了Java中Synchronized锁的使用和原理详解,synchronized是 Java 内置的关键字,它提供了一种独占的加锁方式,synchronized的获取和释放锁由JVM实现,用户不需要显示的释放锁,非常方便,需要的朋友可以参考下
    2023-07-07
  • 详解Java中的线程让步yield()与线程休眠sleep()方法

    详解Java中的线程让步yield()与线程休眠sleep()方法

    Java中的线程让步会让线程让出优先级,而休眠则会让线程进入阻塞状态等待被唤醒,这里我们对比线程等待的wait()方法,来详解Java中的线程让步yield()与线程休眠sleep()方法
    2016-07-07
  • SpringBoot整合EasyExcel实现批量导入导出

    SpringBoot整合EasyExcel实现批量导入导出

    这篇文章主要为大家详细介绍了SpringBoot整合EasyExcel实现批量导入导出功能的相关知识,文中的示例代码讲解详细,需要的小伙伴可以参考下
    2024-03-03
  • @RequestBody,@RequestParam和@Param的区别说明

    @RequestBody,@RequestParam和@Param的区别说明

    这篇文章主要介绍了@RequestBody,@RequestParam和@Param的区别说明,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-03-03
  • ThreadLocal作用原理与内存泄露示例解析

    ThreadLocal作用原理与内存泄露示例解析

    这篇文章主要为大家介绍了ThreadLocal作用原理与内存泄露示例解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-09-09
  • Java服务端服务监控:Prometheus与Spring Boot Actuator的集成方式

    Java服务端服务监控:Prometheus与Spring Boot Actuator的集成方式

    本文介绍了如何将Prometheus与SpringBootActuator集成,实现对Java服务端应用的监控,通过集成,可以利用Prometheus的强大监控能力,及时发现和解决性能问题
    2024-12-12
  • Java语言之包和继承详解

    Java语言之包和继承详解

    这篇文章主要介绍了java的包和继承,结合实例形式详细分析了Java继承的概念、原理、用法及相关操作注意事项,需要的朋友可以参考下
    2021-09-09

最新评论