看到這個標題,有點誇張了啊,@Value
這個誰不知道啊,不就是綁定配置么,還能有什么特殊的玩法不成?
(如果下面列出的這些問題,已經熟練掌握,那確實沒啥往下面看的必要了)
@Value
對應的配置不存在,會怎樣?- 默認值如何設置
- 配置文件中的列表可以直接映射到列表屬性上么?
- 配置參數映射為簡單對象的三種配置方式
- 除了配置注入,字面量、SpEL支持是否了解?
- 遠程(如db,配置中心,http)配置注入可行否?
接下來,限於篇幅問題,將針對上面提出的問題的前面幾條進行說明,最后兩個放在下篇
I. 項目環境
先創建一個用於測試的SpringBoot項目,源碼在最后貼出,友情提示源碼閱讀更友好
1. 項目依賴
本項目借助SpringBoot 2.2.1.RELEASE
+ maven 3.5.3
+ IDEA
進行開發
2. 配置文件
在配置文件中,加一些用於測試的配置信息
application.yml
auth:
jwt:
token: TOKEN.123
expire: 1622616886456
whiteList: 4,5,6
blackList:
- 100
- 200
- 300
tt: token:tt_token; expire:1622616888888
II. 使用case
1. 基本姿勢
通過${}
來引入配置參數,當然前提是所在的類被Spring托管,也就是我們常說的bean
如下,一個常見的使用姿勢
@Component
public class ConfigProperties {
@Value("${auth.jwt.token}")
private String token;
@Value("${auth.jwt.expire}")
private Long expire;
}
2. 配置不存在,拋異常
接下來,引入一個配置不存在的注入,在項目啟動的時候,會發現拋出異常,導致無法正常啟動
/**
* 不存在,使用默認值
*/
@Value("${auth.jwt.no")
private String no;
拋出的異常屬於BeanCreationException
, 對應的異常提示 Caused by: java.lang.IllegalArgumentException: Could not resolve placeholder 'auth.jwt.no' in value "${auth.jwt.no}"
所以為了避免上面的問題,一般來講,建議設置一個默認值,規則如 ${key:默認值}
, 在分號右邊的就是默認值,當沒有相關配置時,使用默認值初始化
/**
* 不存在,使用默認值
*/
@Value("${auth.jwt.no}")
private String no;
3. 列表配置
在配置文件中whiteList,對應的value是 4,5,6
, 用英文逗號分隔,對於這種格式的參數值,可以直接賦予List<Long>
/**
* 英文逗號分隔,轉列表
*/
@Value("${auth.jwt.whiteList}")
private List<Long> whiteList;
上面這個屬於正確的使用姿勢,但是下面這個卻不行了
/**
* yml數組,無法轉換過來,只能根據 "auth.jwt.blackList[0]", "auth.jwt.blackList[1]" 來取對應的值
*/
@Value("${auth.jwt.blackList:10,11,12}")
private String[] blackList;
雖然我們的配置參數 auth.jwt.blackList
是數組,但是就沒法映射到上面的blackList (即使換成 List<String>
也是不行的,並不是因為聲明為String[]
的原因)
我們可以通過查看Evnrionment來看一下配置是怎樣的
通過auth.jwt.blackList
是拿不到配置信息的,只能通過auth.jwt.blackList[0]
, auth.jwt.blackList[1]
來獲取
那么問題來了,怎么解決這個呢?
要解決問題,關鍵就是需要知道@Value
的工作原理,這里直接給出關鍵類 org.springframework.context.support.PropertySourcesPlaceholderConfigurer
關鍵點就在上面圈出的地方,找到這里,我們就可以動手開擼,一個比較猥瑣的方法,如下
// 使用自定義的bean替代Spring的
@Primary
@Component
public class MyPropertySourcesPlaceHolderConfigure extends PropertySourcesPlaceholderConfigurer {
@Autowired
protected Environment environment;
/**
* {@code PropertySources} from the given {@link Environment}
* will be searched when replacing ${...} placeholders.
*
* @see #setPropertySources
* @see #postProcessBeanFactory
*/
@Override
public void setEnvironment(Environment environment) {
super.setEnvironment(environment);
this.environment = environment;
}
@SneakyThrows
@Override
protected void processProperties(ConfigurableListableBeanFactory beanFactoryToProcess, ConfigurablePropertyResolver propertyResolver) throws BeansException {
// 實現一個拓展的PropertySource,支持獲取數組格式的配置信息
Field field = propertyResolver.getClass().getDeclaredField("propertySources");
boolean access = field.isAccessible();
field.setAccessible(true);
MutablePropertySources propertySource = (MutablePropertySources) field.get(propertyResolver);
field.setAccessible(access);
PropertySource source = new PropertySource<Environment>(ENVIRONMENT_PROPERTIES_PROPERTY_SOURCE_NAME, this.environment) {
@Override
@Nullable
public String getProperty(String key) {
// 對數組進行兼容
String ans = this.source.getProperty(key);
if (ans != null) {
return ans;
}
StringBuilder builder = new StringBuilder();
String prefix = key.contains(":") ? key.substring(key.indexOf(":")) : key;
int i = 0;
while (true) {
String subKey = prefix + "[" + i + "]";
ans = this.source.getProperty(subKey);
if (ans == null) {
return i == 0 ? null : builder.toString();
}
if (i > 0) {
builder.append(",");
}
builder.append(ans);
++i;
}
}
};
propertySource.addLast(source);
super.processProperties(beanFactoryToProcess, propertyResolver);
}
}
說明:
- 上面這種實現姿勢很不優雅,講道理應該有更簡潔的方式,有請知道的老哥指教一二
4. 配置轉實體類
通常,@Value
只修飾基本類型,如果我想將配置轉換為實體類,可性否?
當然是可行的,而且還有三種支持姿勢
PropertyEditor
Converter
Formatter
接下來針對上面配置的auth.jwt.tt
進行轉換
auth:
jwt:
tt: token:tt_token; expire:1622616888888
映射為Jwt對象
@Data
public class Jwt {
private String source;
private String token;
private Long expire;
// 實現string轉jwt的邏輯
public static Jwt parse(String text, String source) {
String[] kvs = StringUtils.split(text, ";");
Map<String, String> map = new HashMap<>(8);
for (String kv : kvs) {
String[] items = StringUtils.split(kv, ":");
if (items.length != 2) {
continue;
}
map.put(items[0].trim().toLowerCase(), items[1].trim());
}
Jwt jwt = new Jwt();
jwt.setSource(source);
jwt.setToken(map.get("token"));
jwt.setExpire(Long.valueOf(map.getOrDefault("expire", "0")));
return jwt;
}
}
4.1 PropertyEditor
請注意PropertyEditor
是java bean規范中的,主要用於對bean的屬性進行編輯而定義的接口,Spring提供了支持;我們希望將String轉換為bean屬性類型,一般來講就是一個POJO,對應一個Editor
所以自定義一個 JwtEditor
public class JwtEditor extends PropertyEditorSupport {
@Override
public void setAsText(String text) throws IllegalArgumentException {
setValue(Jwt.parse(text, "JwtEditor"));
}
}
接下來就需要注冊這個Editor
@Configuration
public class AutoConfiguration {
/**
* 注冊自定義的 propertyEditor
*
* @return
*/
@Bean
public CustomEditorConfigurer editorConfigurer() {
CustomEditorConfigurer editorConfigurer = new CustomEditorConfigurer();
editorConfigurer.setCustomEditors(Collections.singletonMap(Jwt.class, JwtEditor.class));
return editorConfigurer;
}
}
說明
- 當上面的
JwtEditor
與Jwt
對象,在相同的包路徑下面的時候,不需要上面的主動注冊,Spring會自動注冊 (就是這么貼心)
上面這個配置完畢之后,就可以正確的被注入了
/**
* 借助 PropertyEditor 來實現字符串轉對象
*/
@Value("${auth.jwt.tt}")
private Jwt tt;
4.2 Converter
Spring的Converter接口也比較常見,至少比上面這個用得多一些,使用姿勢也比較簡單,實現接口、然后注冊即可
public class JwtConverter implements Converter<String, Jwt> {
@Override
public Jwt convert(String s) {
return Jwt.parse(s, "JwtConverter");
}
}
注冊轉換類
/**
* 注冊自定義的converter
*
* @return
*/
@Bean("conversionService")
public ConversionServiceFactoryBean conversionService() {
ConversionServiceFactoryBean factoryBean = new ConversionServiceFactoryBean();
factoryBean.setConverters(Collections.singleton(new JwtConverter()));
return factoryBean;
}
再次測試,同樣可以注入成功
4.3 Formatter
最后再介紹一個Formatter的使用姿勢,它更常見於本地化相關的操作
public class JwtFormatter implements Formatter<Jwt> {
@Override
public Jwt parse(String text, Locale locale) throws ParseException {
return Jwt.parse(text, "JwtFormatter");
}
@Override
public String print(Jwt object, Locale locale) {
return JSONObject.toJSONString(object);
}
}
同樣注冊一下(請注意,我們使用注冊Formatter時,需要將前面Converter的注冊bean給注釋掉)
@Bean("conversionService")
public FormattingConversionServiceFactoryBean conversionService2() {
FormattingConversionServiceFactoryBean factoryBean = new FormattingConversionServiceFactoryBean();
factoryBean.setConverters(Collections.singleton(new JwtConverter()));
factoryBean.setFormatters(Collections.singleton(new JwtFormatter()));
return factoryBean;
}
當Converter與Formatter同時存在時,后者優先級更高
5. 小結
限於篇幅,這里就暫告一段落,針對前面提到的幾個問題,做一個簡單的歸納小結
@Value
聲明的配置不存在時,拋異常(項目會起不來)- 通過設置默認值(語法
${xxx:defaultValue})
可以解決上面的問題 yaml
配置中的數組,無法直接通過@Value
綁定到列表/數組上- 配置值為英文逗號分隔的場景,可以直接賦值給列表/數組
- 不支持將配置文件中的值直接轉換為非簡單對象,如果有需要有三種方式
- 使用
PropertyEditor
實現類型轉換 - 使用
Converter
實現類型轉換 (更推薦使用這種方式) - 使用
Formater
實現類型轉換
- 使用
除了上面的知識點之外,針對最開始提出的問題,給出答案
@Value
支持字面量,也支持SpEL表達式- 既然支持SpEL表達式,當然就可以實現我們需求的遠程配置注入了
既然已經看到這里了,那么就再提兩個問題吧,在SpringCloud微服務中,如果使用了SpringCloud Config,也是可以通過@Value
來注入遠程配置的,那么這個原理又是怎樣的呢?
@Value
綁定的配置,如果想實現動態刷新,可行么?如果可以怎么玩?
(順手不介意的話,關注下微信公眾號"一灰灰blog", 下篇博文就給出答案)
III. 不能錯過的源碼和相關知識點
0. 項目
- 工程:https://github.com/liuyueyi/spring-boot-demo
- 源碼: https://github.com/liuyueyi/spring-boot-demo/tree/master/spring-boot/002-properties-value
系列博文,配合閱讀效果更好哦
- 【基礎系列】實現一個自定義配置加載器(應用篇)
- 【基礎系列】SpringBoot配置信息之默認配置
- 【基礎系列】SpringBoot配置信息之配置刷新
- 【基礎系列】SpringBoot基礎篇配置信息之自定義配置指定與配置內引用
- 【基礎系列】SpringBoot基礎篇配置信息之多環境配置信息
- 【基礎系列】SpringBoot基礎篇配置信息之如何讀取配置信息
1. 一灰灰Blog
盡信書則不如,以上內容,純屬一家之言,因個人能力有限,難免有疏漏和錯誤之處,如發現bug或者有更好的建議,歡迎批評指正,不吝感激
下面一灰灰的個人博客,記錄所有學習和工作中的博文,歡迎大家前去逛逛
- 一灰灰Blog個人博客 https://blog.hhui.top
- 一灰灰Blog-Spring專題博客 http://spring.hhui.top