@Value竟然能玩出這麼多花樣

蘇三說技術發表於2021-10-12

前言

對於從事java開發工作的小夥伴來說,spring框架肯定再熟悉不過了。spring給開發者提供了非常豐富的api,滿足我們日常的工作需求。

如果想要建立bean例項,可以使用@Controller、@Service、@Repository、@Component等註解。

如果想要依賴注入某個物件,可以使用@Autowired和@Resource註解。

如果想要開啟事務,可以使用@Transactional註解。

如果想要動態讀取配置檔案中的某個系統屬性,可以使用@Value註解。

等等,還有很多。。。

前面幾種常用的註解,在我以往的文章中已經介紹過了,在這裡就不過多講解了。

今天我們們重點聊聊@Value註解,因為它是一個非常有用,但極其容易被忽視的註解,絕大多數人可能只用過它的一部分功能,這是一件非常遺憾的事情。

所以今天有必要和大家一起,重新認識一下@Value

1. 由一個例子開始

假如在UserService類中,需要注入系統屬性到userName變數中。通常情況下,我們會寫出如下的程式碼:

@Service
public class UserService {

    @Value("${susan.test.userName}")
    private String userName;

    public String test() {
        System.out.println(userName);
        return userName;
    }
}

通過@Value註解指定系統屬性的名稱susan.test.userName,該名稱需要使用${}包起來。

這樣spring就會自動的幫我們把對應的系統屬性值,注入到userName變數中。

不過,上面功能的重點是要在applicationContext.properties檔案(簡稱:配置檔案)中配置同名的系統屬性:

#張三
susan.test.userName=\u5f20\u4e09

那麼,名稱真的必須完全相同嗎?

2. 關於屬性名

這時候,有些朋友可能會說:
@ConfigurationProperties配置類中,定義的引數名可以跟配置檔案中的系統屬性名不同。

比如,在配置類MyConfig類中定義的引數名是userName:

@Configuration
@ConfigurationProperties(prefix = "susan.test")
@Data
public class MyConfig {
    private String userName;
}

而配置檔案中配置的系統屬性名是:

susan.test.user-name=\u5f20\u4e09

類中用的userName,而配置檔案中用的user-name,不一樣。但測試之後,發現該功能能夠正常執行。

配置檔案中的系統屬性名用 駝峰標識小寫字母加中劃線的組合,spring都能找到配置類中的屬性名userName進行賦值。

由此可見,配置檔案中的系統屬性名,可以跟配置類中的屬性名不一樣。不過,有個前提,字首susan.test必須相同。

那麼,@Value註解中定義的系統屬性名也可以不一樣嗎?

答案:不能。如果不一樣,啟動專案時會直接報錯。


此外,如果只在@Value註解中指定了系統屬性名,但實際在配置檔案中沒有配置它,也會報跟上面一樣的錯。

所以,@Value註解中指定的系統屬性名,必須跟配置檔案中的相同。

3. 亂碼問題

不知道細心的小夥伴們有沒有發現,我配置的屬性值:張三,其實是轉義過的。

susan.test.userName=\u5f20\u4e09

為什麼要做這個轉義?

假如在配置檔案中配置中文的張三:

susan.test.userName=張三

最後獲取資料時,你會發現userName竟然出現了亂碼:

å¼ ä¸

what?

為什麼會出現亂碼?

答:在springboot的CharacterReader類中,預設的編碼格式是ISO-8859-1,該類負責.properties檔案中系統屬性的讀取。如果系統屬性包含中文字元,就會出現亂碼。


那麼,如何解決亂碼問題呢?

目前主要有如下三種方案:

  1. 手動將ISO-8859-1格式的屬性值,轉換成UTF-8格式。
  2. 設定encoding引數,不過這個只對@PropertySource註解有用。
  3. 將中文字元用unicode編碼轉義。

顯然@Value不支援encoding引數,所以方案2不行。

假如使用方案1,具體實現程式碼如下:

@Service
public class UserService {

    @Value(value = "${susan.test.userName}")
    private String userName;

    public String test() {
        String userName1 = new String(userName.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
        System.out.println();
        return userName1;
    }
}

確實可以解決亂碼問題。

但如果專案中包含大量中文系統屬性值,每次都需要加這樣一段特殊轉換程式碼。出現大量重複程式碼,有沒有覺得有點噁心?

反轉我被噁心到了。

那麼,如何解決程式碼重複問題呢?

答:將屬性值的中文內容轉換成unicode。

類似於這樣的:

susan.test.userName=\u5f20\u4e09

這種方式同樣能解決亂碼問題,不會出現噁心的重複程式碼。但需要做一點額外的轉換工作,不過這個轉換非常容易,因為有現成的線上轉換工具。

推薦使用這個工具轉換:http://www.jsons.cn/unicode/

在這裡順便告訴你一個小祕密:如果你使用的是.yml.yaml格式的配置檔案,並不會出現中文亂碼問題。

這又是為什麼?

因為.yml或.yaml格式的配置檔案,最終會使用UnicodeReader類進行解析,它的init方法中,首先讀取BOM檔案頭資訊,如果頭資訊中有UTF8、UTF16BE、UTF16LE,就採用對應的編碼,如果沒有,則採用預設UTF8編碼。

需要注意的是:亂碼問題一般出現在本地環境,因為本地直接讀取的.properties配置檔案。在dev、test、生產等環境,如果從zookeeper、apollo、nacos等配置中心中獲取系統引數值,走的是另外的邏輯,並不會出現亂碼問題。

4.預設值

有時候,預設值是我們非常頭疼的問題。

為什麼這樣說呢?

因為很多時候使用java的預設值,並不能滿足我們的日常工作需求。

比如有這樣一個需求:如果配置了系統屬性,userName就用配置的屬性值。如果沒有配置,則userName用預設值susan。

有些朋友可能認為可以這樣做:

@Value(value = "${susan.test.userName}")
private String userName = "susan";

在定義引數時直接給個預設值,但如果仔細想想這招是行不通的的。因為設定userName預設值的時機,比@Value註解依賴注入屬性值要早,也就是說userName初始化好了預設值,後面還是會被覆蓋。

那麼,到底該如何設定預設值呢?

答:使用:

例如:

@Value(value = "${susan.test.userName:susan}")
private String userName;

在需要設定預設值的系統屬性名後,加:符號。緊接著,在:右邊設定預設值。

建議大家平時在使用@Value時,儘量都設定一個預設值。如果不需要預設值,寧可設定一個空。比如:

@Value(value = "${susan.test.userName:}")
private String userName;

為什麼這麼說?

假如有這種場景:在business層中包含了UserService類,business層被api服務和job服務都引用了。但UserService類中@Value的userName只在api服務中有用,在job服務中根本用不到該屬性。

對於job服務來說,如果不在.properties檔案中配置同名的系統屬性,則服務啟動時就會報錯。

這個坑,我之前踩過多次。所以,建議大家,使用@Value註解時,最好給引數設定一個預設值,以防止出現類似的問題。

5. static變數

前面我們已經見識過,如何使用@Value註解,給類的成員變數注入系統屬性值

那麼,問題來了,靜態變數可以自動注入系統屬性值不?

我們一起看看,假如將上面的userName定義成static的:

@Value("${susan.test.userName}")
private static String userName;

程式可以正常啟動,但是獲取到userName的值卻是null。

由此可見,被static修飾的變數通過@Value會注入失敗。

作為好奇寶寶的你,此時肯定想問:如何才能給靜態變數注入系統屬性值呢?

答:這就需要使用如下的騷程式碼了:

@Service
public class UserService {

    private static String userName;

    @Value("${susan.test.userName}")
    public void setUserName(String userName) {
        UserService.userName = userName;
    }

    public String test() {
        return userName;
    }
}

提供一個靜態引數的setter方法,在該方法上使用@Value注入屬性值,並且同時在該方法中給靜態變數賦值。

有些細心的朋友可能會發現,@Value註解在這裡竟然使用在setUserName方法上了,也就是對應的setter方法,而不是在變數上。

有趣,有趣,這種用法有點高階喔。

不過,通常情況下,我們一般會在pojo實體類上,使用lombok的@Data、@Setter、@Getter等註解,在編譯時動態增加setter或getter方法,所以@Value用在方法上的場景其實不多。

6.變數型別

上面的內容,都是用的字串型別的變數進行舉例的。其實,@Value註解還支援其他多種型別的系統屬性值的注入。

6.1 基本型別

眾所周知,在Java中的基本資料型別有4類8種,然我們一起回顧一下:

  • 整型:byte、short、int、long
  • 浮點型:float、double
  • 布林型:boolean
  • 字元型:char

相對應地提供了8種包裝類:

  • 整型:Byte、Short、Integer、Long
  • 浮點型:Float、Double
  • 布林型:Boolean
  • 字元型:Character

@Value註解對這8中基本型別和相應的包裝類,有非常良好的支援,例如:

@Value("${susan.test.a:1}")
private byte a;

@Value("${susan.test.b:100}")
private short b;

@Value("${susan.test.c:3000}")
private int c;

@Value("${susan.test.d:4000000}")
private long d;

@Value("${susan.test.e:5.2}")
private float e;

@Value("${susan.test.f:6.1}")
private double f;

@Value("${susan.test.g:false}")
private boolean g;

@Value("${susan.test.h:h}")
private char h;

@Value("${susan.test.a:1}")
private byte a1;

@Value("${susan.test.b:100}")
private Short b1;

@Value("${susan.test.c:3000}")
private Integer c1;

@Value("${susan.test.d:4000000}")
private Long d1;

@Value("${susan.test.e:5.2}")
private Float e1;

@Value("${susan.test.f:6.1}")
private Double f1;

@Value("${susan.test.g:false}")
private Boolean g1;

@Value("${susan.test.h:h}")
private Character h1;

有了這些常用的資料型別,我們在定義變數型別時,可以非常愉快的玩耍了,不用做額外的轉換。

6.2 陣列

但只用上面的基本型別是不夠的,特別是很多需要批量處理資料的場景中。這時候可以使用陣列,它在日常開發中使用的頻率很高。

我們在定義陣列時可以這樣寫:

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

spring預設使用逗號分隔引數值。

如果用空格分隔,例如:

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

spring會自動把空格去掉,導致資料中只有一個值:12345,注意千萬別搞錯了。

順便說一下,定義陣列的時候,裡面還是有挺多門道的。比如上面列子中,我的資料是:1,2,3,4,5。

如果我們把陣列定義成:short、int、long、char、string型別,spring是可以正常注入屬性值的。

但如果把陣列定義成:float、double型別,啟動專案時就會直接報錯。


小夥伴們,下巴驚掉了沒?

按理說,1,2,3,4,5用float、double是能夠表示的呀,為什麼會報錯?

如果使用int的包裝類,比如:

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

啟動專案時同樣會報上面的異常。

此外,定義陣列時一定要注意屬性值的型別,必須完全一致才可以,如果出現下面這種情況:

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

屬性值中包含了1.0和abc,顯然都無法將該字串轉換成int。

6.3 集合類

有了基本型別和陣列,的確讓我們更加方便了。但對資料的處理,只用陣列這一種資料結構是遠遠不夠的,下面給大家介紹一下其他的常用資料結構。

6.3.1 List

List是陣列的變種,它的長度是可變的,而陣列的長度是固定的。

我們看看List是如何注入屬性值的:

@Value("${susan.test.list}")
private List<String> list;

最關鍵的是看配置檔案:

susan.test.list[0]=10
susan.test.list[1]=11
susan.test.list[2]=12
susan.test.list[3]=13

當你滿懷希望的啟動專案,準備使用這個功能的時候,卻發現竟然報錯了。


what?

看來@Value不支援這種直接的List注入。

那麼,如何解決這個問題呢?

有人說用@ConfigurationProperties

需要定義一個MyConfig類:

@Configuration
@ConfigurationProperties(prefix = "susan.test")
@Data
public class MyConfig {
    private List<String> list;
}

然後在呼叫的地方這樣寫:

@Service
public class UserService {

    @Autowired
    private MyConfig myConfig;

    public String test() {
        System.out.println(myConfig.getList());
        return null;
    }
}

這種方法確實能夠完成List注入。但是,只能說明@ConfigurationProperties註解的強大,跟@Value有半毛錢的關係?

答:沒有。

那麼,問題來了,用@Value如何實現這個功能呢?

答:使用spring的EL表示式。

List的定義改成:

@Value("#{'${susan.test.list}'.split(',')}")
private List<String> list;

使用#號加大括號的EL表示式。

然後配置檔案改成:

susan.test.list=10,11,12,13

跟定義陣列時的配置檔案一樣。

6.3.2 Set

Set也是一種儲存資料的集合,它比較特殊,裡面儲存的資料不會重複。

我們可以這樣定義Set:

@Value("#{'${susan.test.set}'.split(',')}")
private Set<String> set;

配置檔案是這樣的:

susan.test.set=10,11,12,13

Set跟List的用法極為相似。

但為了證明本節的獨特之處,我打算說點新鮮的內容。

如何給List或者Set設定預設值空呢?

有些朋友可能會說:這還不簡單,直接在@Value的$表示式後面加個:號不就行了。

具體程式碼如下:

@Value("#{'${susan.test.set:}'.split(',')}")
private Set<String> set;

結果卻跟想象中不太一樣:

Set集合怎麼不是空的,而是包含了一個空字串的集合?

好吧,那我在:號後加null,總可以了吧?

Set集合也不是空的,而是包含了一個"null"字串的集合。

這也不行,那也不行,該如何是好?

答:使用EL表示式的empty方法。

具體程式碼如下:

@Value("#{'${susan.test.set:}'.empty ? null : '${susan.test.set:}'.split(',')}")
private Set<String> set;

執行之後,結果對了:

其實List也有類似的問題,也能使用該方法解決問題。

在這裡溫馨的提醒一下,該判斷的表示式比較複雜,自己手寫非常容易寫錯,建議複製貼上之後根據實際需求改改。

6.3.3 Map

還有一種比較常用的集合是map,它支援key/value鍵值對的形式儲存資料,並且不會出現相同key的資料。

我們可以這樣定義Map:

@Value("#{${susan.test.map}}")
private Map<String, String> map;

配置檔案是這樣的:

susan.test.map={"name":"蘇三", "age":"18"}

這種用法跟上面稍微有一點區別。

設定預設值的程式碼如下:

@Value("#{'${susan.test.map:}'.empty ? null : '${susan.test.map:}'}")
private Map<String, String> map;

7 EL高階玩法

前面我們已經見識過spring EL表示式的用法了,在設定空的預設值時特別有用。

其實,empty方法只是它很普通的用法,還有更高階的用法,不信我們一起看看。

7.1 注入bean

以前我們注入bean,一般都是用的@Autowired或者@Resource註解。例如:

@Service
public class RoleService {
    public String getRoleName() {
        return "管理員";
    }
}

@Service
public class UserService {

    @Autowired
    private RoleService roleService;

    public String test() {
        System.out.println(roleService.getRoleName());
        return null;
    }
}

但我要告訴你的是@Value註解也可以注入bean,它是這麼做的:

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

通過這種方式,可以注入id為roleService的bean。

7.2 bean的變數和方法

通過EL表示式,@Value註解已經可以注入bean了。既然能夠拿到bean例項,接下來,可以再進一步。

在RoleService類中定義了:成員變數、常量、方法、靜態方法。

@Service
public class RoleService {
    public static final int DEFAULT_AGE = 18;
    public int id = 1000;

    public String getRoleName() {
        return "管理員";
    }

    public static int getParentId() {
        return 2000;
    }
}

在呼叫的地方這樣寫:

@Service
public class UserService {

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

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

    @Value("#{roleService.getRoleName()}")
    private String myRoleName;

    @Value("#{roleService.getParentId()}")
    private String myParentId;

    public String test() {
        System.out.println(myAge);
        System.out.println(id);
        System.out.println(myRoleName);
        System.out.println(myParentId);
        return null;
    }
}

在UserService類中通過@Value可以注入:成員變數、常量、方法、靜態方法獲取到的值,到相應的成員變數中。

一下子有沒有豁然開朗的感覺,有了這些,我們可以通過@Value註解,實現更多的功能了,不僅僅限於注入系統屬性。

7.3 靜態類

前面的內容都是基於bean的,但有時我們需要呼叫靜態類,比如:Math、xxxUtil等靜態工具類的方法,該怎麼辦呢?

答:用T加括號。

示例1:

@Value("#{T(java.io.File).separator}")
private String path;

可以注入系統的路徑分隔符到path中。

示例2:

@Value("#{T(java.lang.Math).random()}")
private double randomValue;

可以注入一個隨機數到randomValue中。

7.4 邏輯運算

通過上面介紹的內容,我們可以獲取到絕大多數類的變數和方法的值了。但有了這些值,還不夠,我們能不能在EL表示式中加點邏輯?

拼接字串:

@Value("#{roleService.roleName + '' + roleService.DEFAULT_AGE}")
private String value;

邏輯判斷:

@Value("#{roleService.DEFAULT_AGE > 16 and roleService.roleName.equals('蘇三')}")
private String operation;

三目運算:

@Value("#{roleService.DEFAULT_AGE > 16 ? roleService.roleName: '蘇三' }")
private String realRoleName;

還有很多很多功能,我就不一一列舉了。

EL表示式實在太強大了,對這方面如果感興趣的小夥伴可以找我私聊。

8 ${}和#{}的區別

上面巴拉巴拉說了這麼多@Value的牛逼用法,歸根揭底就是${}#{}的用法。

下面重點說說${}和#{}的區別,這可能是很多小夥伴比較關心的話題。

8.1 ${}

主要用於獲取配置檔案中的系統屬性值。

例如:

@Value(value = "${susan.test.userName:susan}")
private String userName;

通過:可以設定預設值。如果在配置檔案中找不到susan.test.userName的配置,則注入時用預設值。

如果在配置檔案中找不到susan.test.userName的配置,也沒有設定預設值,則啟動專案時會報錯。

8.2 #{}

主要用於通過spring的EL表示式,獲取bean的屬性,或者呼叫bean的某個方法。還有呼叫類的靜態常量和靜態方法。

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

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

@Value("#{roleService.getRoleName()}")
private String myRoleName;

@Value("#{T(java.lang.Math).random()}")
private double randomValue;

如果是呼叫類的靜態方法,則需要加T(包名 + 方法名稱)。

例如:T(java.lang.Math)。

好了,今天的內容就介紹到這裡,希望對你會有所幫助。隨便劇透一下,後面的文章會繼續介紹:

  1. @Value的原理
  2. @Value動態重新整理屬性值的原因
  3. @ConfigurationProperties註解的用法,它也非常強大。

好不好奇?趕緊關注一波呀。

最後說一句(求關注,別白嫖我)

如果這篇文章對您有所幫助,或者有所啟發的話,幫忙掃描下發二維碼關注一下,您的支援是我堅持寫作最大的動力。

求一鍵三連:點贊、轉發、在看。

關注公眾號:【蘇三說技術】,在公眾號中回覆:面試、程式碼神器、開發手冊、時間管理有超讚的粉絲福利,另外回覆:加群,可以跟很多BAT大廠的前輩交流和學習。

相關文章