SpringBoot基礎篇之@Value中哪些你不知道的知識點

SpringBoot基礎篇@Value中哪些你不知道的知識點

看到這個標題,有點誇張了啊,@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;
    }
}

說明

  • 當上面的JwtEditorJwt對象,在相同的包路徑下面的時候,不需要上面的主動註冊,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. 項目

系列博文,配合閱讀效果更好哦

1. 一灰灰Blog

盡信書則不如,以上內容,純屬一家之言,因個人能力有限,難免有疏漏和錯誤之處,如發現bug或者有更好的建議,歡迎批評指正,不吝感激

下面一灰灰的個人博客,記錄所有學習和工作中的博文,歡迎大家前去逛逛

一灰灰blog

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章