SpringBoot解析@Value注解型解析注入時機及原理分析
@Value的使用
@Value 注解可以用來將外部的值動態(tài)注入到 Bean 中,在 @Value 注解中,可以使${} 與 #{} ,它們的區(qū)別如下:
(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編碼。

靜態(tài)變量注入
默認被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;數組
但只用上面的基本類型是不夠的,特別是很多需要批量處理數據的場景中。這時候可以使用數組,它在日常開發(fā)中使用的頻率很高。我們在定義數組時可以這樣寫:
@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實例,接下來,可以再進一步,獲取成員變量、常量、方法、靜態(tài)方法:
@Value("#{roleService.DEFAULT_AGE}")
private int myAge;前面的內容都是基于bean的,但有時我們需要調用靜態(tài)類,比如:Math、xxxUtil等靜態(tài)工具類的方法,可以這么寫:
// 注入系統的路徑分隔符到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的用法,歸根揭底就是${}和#{}的用法,我們來看看兩者的區(qū)別:
- ${}:主要用于獲取配置文件中的系統屬性值,可以設置默認值。如果在配置文件中找不到屬性的配置,則注入時用默認值,如果都沒有會直接報錯
- #{}:主要用于通過spring的EL表達式,獲取bean的屬性,或者調用bean的某個方法,還有調用類的靜態(tài)常量和靜態(tài)方法,如果是調用類的靜態(tài)方法,則需要加T(包名 + 方法名稱)。
AutowiredAnnotationBeanPostProcessor 類介紹
首先解析的都是我們的Spring管理的Bean,我們的Bean又有配置型Configuration、服務型Controller、Service等的,但他們都是@Component的,那解析@Value的時候是什么時候呢,其實就是創(chuàng)建Bean的時候,也就是實例化的時候,而實例化又分懶加載的和隨著SpringBoot啟動就會創(chuàng)建的在刷新方法里的 finishBeanFactoryInitialization 會對不是懶加載的Bean進行實例化,這就涉及到Bean的生命周期啦,其實解析和屬性注入都是通過后置處理器進行的。
- 解析:doCreateBean 方法里的 applyMergedBeanDefinitionPostProcessors執(zhí)行后置處理器進行收集,實際收集的處理器是:AutowiredAnnotationBeanPostProcessor
- 注入:populateBean 方法里的 postProcessProperties 執(zhí)行后置處理器進行注入,實際注入的處理器還是: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) {
// 不支持靜態(tài)類
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 不是可見的橋接方法對,那么就退出當前方法,不繼續(xù)執(zhí)行后續(xù)代碼。這確保了只有在 method 和 bridgedMethod 之間的可見性匹配時,才會繼續(xù)進行后續(xù)的邏輯處理。
if (!BridgeMethodResolver.isVisibilityBridgeMethodPair(method, bridgedMethod)) {
return;
}
// 看看方法是不是有注解@Autowired 或 @Value
MergedAnnotation<?> ann = findAutowiredAnnotation(bridgedMethod);
if (ann != null && method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) {
// 靜態(tài)方法略過
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 屬性注入




總結
以上為個人經驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關文章
SpringBoot中使用Flyway進行數據庫遷移的詳細流程
本文介紹了如何在Spring Boot項目中使用Flyway進行數據庫遷移,Flyway通過SQL腳本管理數據庫變更,支持自動執(zhí)行和版本控制,避免了手動執(zhí)行SQL腳本的錯誤和維護困難,需要的朋友可以參考下2025-02-02
java jackson 將對象轉json時,忽略子對象的某個屬性操作
這篇文章主要介紹了java jackson 將對象轉json時,忽略子對象的某個屬性操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-10-10
程序包org.springframework.boot不存在的問題解決
本文主要介紹了程序包org.springframework.boot不存在的問題解決,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2024-09-09

