天天看點

spring-@Value【用法、資料來源、動态重新整理】

作者:臭豬比

1. @Value的用法

2. @Value資料來源

3. @Value動态重新整理的問題

下面我們一個個來整理一下,将這幾個問題搞定,助大家在疫情期間面試能夠過關斬将,拿高薪。

@Value的用法

系統中需要連接配接db,連接配接db有很多配置資訊。

系統中需要發送郵件,發送郵件需要配置郵件伺服器的資訊。

還有其他的一些配置資訊。

我們可以将這些配置資訊統一放在一個配置檔案中,上線的時候由運維統一修改。

那麼系統中如何使用這些配置資訊呢,spring中提供了@Value注解來解決這個問題。

通常我們會将配置資訊以key=value的形式存儲在properties配置檔案中。

通過@Value("${配置檔案中的key}")來引用指定的key對應的value。

@Value使用步驟

步驟一:使用@PropertySource注解引入配置檔案

将@PropertySource放在類上面,如下

@PropertySource({"配置檔案路徑1","配置檔案路徑2"...})
           
@PropertySource注解有個value屬性,字元串數組類型,可以用來指定多個配置檔案的路徑。

如:

@Component
@PropertySource({"classpath:com/javacode2018/lesson002/demo18/db.properties"})
public class DbConfig {
}
           

步驟二:使用@Value注解引用配置檔案的值

通過@Value引用上面配置檔案中的值:

文法

@Value("${配置檔案中的key:預設值}")
@Value("${配置檔案中的key}")
           

如:

@Value("${password:123}")
           
上面如果password不存在,将123作為值
@Value("${password}")
           
上面如果password不存在,值為${password}

假如配置檔案如下

jdbc.url=jdbc:mysql://localhost:3306/javacode2018?characterEncoding=UTF-8
jdbc.username=javacode
jdbc.password=javacode
           

使用方式如下:

@Value("${jdbc.url}")
private String url;

@Value("${jdbc.username}")
private String username;

@Value("${jdbc.password}")
private String password;
           

下面來看案例

案例

來個配置檔案db.properties

jdbc.url=jdbc:mysql://localhost:3306/javacode2018?characterEncoding=UTF-8
jdbc.username=javacode
jdbc.password=javacode
           

來個配置類,使用@PropertySource引入上面的配置檔案

package com.javacode2018.lesson002.demo18.test1;

import org.springframework.beans.factory.annotation.Configurable;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.PropertySource;

@Configurable
@ComponentScan
@PropertySource({"classpath:com/javacode2018/lesson002/demo18/db.properties"})
public class MainConfig1 {
}
           

來個類,使用@Value來使用配置檔案中的資訊

package com.javacode2018.lesson002.demo18.test1;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class DbConfig {

    @Value("${jdbc.url}")
    private String url;

    @Value("${jdbc.username}")
    private String username;

    @Value("${jdbc.password}")
    private String password;

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public String toString() {
        return "DbConfig{" +
                "url='" + url + '\'' +
                ", username='" + username + '\'' +
                ", password='" + password + '\'' +
                '}';
    }
}
           
上面重點在于注解@Value注解,注意@Value注解中的

來個測試用例

package com.javacode2018.lesson002.demo18;

import com.javacode2018.lesson002.demo18.test1.DbConfig;
import com.javacode2018.lesson002.demo18.test1.MainConfig1;
import org.junit.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class ValueTest {

    @Test
    public void test1() {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        context.register(MainConfig1.class);
        context.refresh();

        DbConfig dbConfig = context.getBean(DbConfig.class);
        System.out.println(dbConfig);
    }
}
           

運作輸出

DbConfig{url='jdbc:mysql://localhost:3306/javacode2018?characterEncoding=UTF-8', username='javacode', password='javacode'}
           

上面用起來比較簡單,很多用過的人看一眼就懂了,這也是第一個問題,多數人都是ok的,下面來看@Value中資料來源除了配置檔案的方式,是否還有其他方式。

@Value資料來源

通常情況下我們@Value的資料來源于配置檔案,不過,還可以用其他方式,比如我們可以将配置檔案的内容放在資料庫,這樣修改起來更容易一些。

我們需要先了解一下@Value中資料來源于spring的什麼地方。

spring中有個類

org.springframework.core.env.PropertySource
           
可以将其了解為一個配置源,裡面包含了key->value的配置資訊,可以通過這個類中提供的方法擷取key對應的value資訊

内部有個方法:

public abstract Object getProperty(String name);
           

通過name擷取對應的配置資訊。

系統有個比較重要的接口

org.springframework.core.env.Environment
           

用來表示環境配置資訊,這個接口有幾個方法比較重要

String resolvePlaceholders(String text);
MutablePropertySources getPropertySources();
           

resolvePlaceholders用來解析${text}的,@Value注解最後就是調用這個方法來解析的。

getPropertySources傳回MutablePropertySources對象,來看一下這個類

public class MutablePropertySources implements PropertySources {

    private final List<PropertySource<?>> propertySourceList = new CopyOnWriteArrayList<>();

}
           

内部包含一個propertySourceList清單。

spring容器中會有一個Environment對象,最後會調用這個對象的resolvePlaceholders方法解析@Value。

大家可以捋一下,最終解析@Value的過程:

1. 将@Value注解的value參數值作為Environment.resolvePlaceholders方法參數進行解析
2. Environment内部會通路MutablePropertySources來解析
3. MutablePropertySources内部有多個PropertySource,此時會周遊PropertySource清單,調用PropertySource.getProperty方法來解析key對應的值
           

通過上面過程,如果我們想改變@Value資料的來源,隻需要将配置資訊包裝為PropertySource對象,丢到Environment中的MutablePropertySources内部就可以了。

下面我們就按照這個思路來一個。

來個郵件配置資訊類,内部使用@Value注入郵件配置資訊

package com.javacode2018.lesson002.demo18.test2;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

/**
 * 郵件配置資訊
 */
@Component
public class MailConfig {

    @Value("${mail.host}")
    private String host;

    @Value("${mail.username}")
    private String username;

    @Value("${mail.password}")
    private String password;

    public String getHost() {
        return host;
    }

    public void setHost(String host) {
        this.host = host;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public String toString() {
        return "MailConfig{" +
                "host='" + host + '\'' +
                ", username='" + username + '\'' +
                ", password='" + password + '\'' +
                '}';
    }
}
           

再來個類DbUtil,getMailInfoFromDb方法模拟從db中擷取郵件配置資訊,存放在map中

package com.javacode2018.lesson002.demo18.test2;

import java.util.HashMap;
import java.util.Map;

public class DbUtil {
    /**
     * 模拟從db中擷取郵件配置資訊
     *
     * @return
     */
    public static Map<String, Object> getMailInfoFromDb() {
        Map<String, Object> result = new HashMap<>();
        result.put("mail.host", "smtp.qq.com");
        result.put("mail.username", "路人");
        result.put("mail.password", "123");
        return result;
    }
}
           

來個spring配置類

package com.javacode2018.lesson002.demo18.test2;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan
public class MainConfig2 {
}
           

下面是重點代碼

@Test
public void test2() {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();

    /*下面這段是關鍵 start*/
    //模拟從db中擷取配置資訊
    Map<String, Object> mailInfoFromDb = DbUtil.getMailInfoFromDb();
    //将其丢在MapPropertySource中(MapPropertySource類是spring提供的一個類,是PropertySource的子類)
    MapPropertySource mailPropertySource = new MapPropertySource("mail", mailInfoFromDb);
    //将mailPropertySource丢在Environment中的PropertySource清單的第一個中,讓優先級最高
    context.getEnvironment().getPropertySources().addFirst(mailPropertySource);
    /*上面這段是關鍵 end*/

    context.register(MainConfig2.class);
    context.refresh();
    MailConfig mailConfig = context.getBean(MailConfig.class);
    System.out.println(mailConfig);
}
           
注釋比較詳細,就不詳細解釋了。

直接運作,看效果

MailConfig{host='smtp.qq.com', username='路人', password='123'}
           

有沒有感覺很爽,此時你們可以随意修改DbUtil.getMailInfoFromDb,具體資料是從db中來,還是從redis或者其他媒體中來,任由大家發揮。

上面重點是下面這段代碼,大家需要了解

/*下面這段是關鍵 start*/
//模拟從db中擷取配置資訊
Map<String, Object> mailInfoFromDb = DbUtil.getMailInfoFromDb();
//将其丢在MapPropertySource中(MapPropertySource類是spring提供的一個類,是PropertySource的子類)
MapPropertySource mailPropertySource = new MapPropertySource("mail", mailInfoFromDb);
//将mailPropertySource丢在Environment中的PropertySource清單的第一個中,讓優先級最高
context.getEnvironment().getPropertySources().addFirst(mailPropertySource);
/*上面這段是關鍵 end*/
           

咱們繼續看下一個問題

如果我們将配置資訊放在db中,可能我們會通過一個界面來修改這些配置資訊,然後儲存之後,希望系統在不重新開機的情況下,讓這些值在spring容器中立即生效。

@Value動态重新整理的問題的問題,springboot中使用@RefreshScope實作了。

實作@Value動态重新整理

先了解一個知識點

這塊需要先講一個知識點,用到的不是太多,是以很多人估計不太了解,但是非常重要的一個點,我們來看一下。

這個知識點是自定義bean作用域,對這塊不了解的先看一下這篇文章:bean作用域詳解

bean作用域中有個地方沒有講,來看一下@Scope這個注解的源碼,有個參數是:

ScopedProxyMode proxyMode() default ScopedProxyMode.DEFAULT;
           

這個參數的值是個ScopedProxyMode類型的枚舉,值有下面4中

public enum ScopedProxyMode {
    DEFAULT,
    NO,
    INTERFACES,
    TARGET_CLASS;
}
           

前面3個,不講了,直接講最後一個值是幹什麼的。

當@Scope中proxyMode為TARGET_CLASS的時候,會給目前建立的bean通過cglib生成一個代理對象,通過這個代理對象來通路目标bean對象。

了解起來比較晦澀,還是來看代碼吧,容易了解一些,來個自定義的Scope案例。

自定義一個bean作用域的注解

package com.javacode2018.lesson002.demo18.test3;

import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;

import java.lang.annotation.*;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Scope(BeanMyScope.SCOPE_MY) //@1
public @interface MyScope {
    /**
     * @see Scope#proxyMode()
     */
    ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;//@2
}
           

@1:使用了@Scope注解,value為引用了一個常量,值為my,一會下面可以看到。

@2:注意這個地方,參數名稱也是proxyMode,類型也是ScopedProxyMode,而@Scope注解中有個和這個同樣類型的參數,spring容器解析的時候,會将這個參數的值賦給@MyScope注解上面的@Scope注解的proxyMode參數,是以此處我們設定proxyMode值,最後的效果就是直接改變了@Scope中proxyMode參數的值。此處預設值取的是ScopedProxyMode.TARGET_CLASS

@MyScope注解對應的Scope實作如下

package com.javacode2018.lesson002.demo18.test3;

import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.config.Scope;
import org.springframework.lang.Nullable;

/**
 * @see MyScope 作用域的實作
 */
public class BeanMyScope implements Scope {

    public static final String SCOPE_MY = "my"; //@1

    @Override
    public Object get(String name, ObjectFactory<?> objectFactory) { 
        System.out.println("BeanMyScope >>>>>>>>> get:" + name); //@2
        return objectFactory.getObject(); //@3
    }

    @Nullable
    @Override
    public Object remove(String name) {
        return null;
    }

    @Override
    public void registerDestructionCallback(String name, Runnable callback) {

    }

    @Nullable
    @Override
    public Object resolveContextualObject(String key) {
        return null;
    }

    @Nullable
    @Override
    public String getConversationId() {
        return null;
    }
}
           

@1:定義了一個常量,作為作用域的值

@2:這個get方法是關鍵,自定義作用域會自動調用這個get方法來建立bean對象,這個地方輸出了一行日志,為了一會友善看效果

@3:通過objectFactory.getObject()擷取bean執行個體傳回。

下面來建立個類,作用域為上面自定義的作用域

package com.javacode2018.lesson002.demo18.test3;

import org.springframework.stereotype.Component;

import java.util.UUID;

@Component
@MyScope //@1 
public class User {

    private String username;

    public User() { 
        System.out.println("---------建立User對象" + this); //@2
        this.username = UUID.randomUUID().toString(); //@3
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

}
           

@1:使用了自定義的作用域@MyScope

@2:構造函數中輸出一行日志

@3:給username指派,通過uuid随機生成了一個

來個spring配置類,加載上面@Compontent标注的元件

package com.javacode2018.lesson002.demo18.test3;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@ComponentScan
@Configuration
public class MainConfig3 {
}
           

下面重點來了,測試用例

@Test
public void test3() throws InterruptedException {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
    //将自定義作用域注冊到spring容器中
    context.getBeanFactory().registerScope(BeanMyScope.SCOPE_MY, new BeanMyScope());//@1
    context.register(MainConfig3.class);
    context.refresh();

    System.out.println("從容器中擷取User對象");
    User user = context.getBean(User.class); //@2
    System.out.println("user對象的class為:" + user.getClass()); //@3

    System.out.println("多次調用user的getUsername感受一下效果\n");
    for (int i = 1; i <= 3; i++) {
        System.out.println(String.format("********\n第%d次開始調用getUsername", i));
        System.out.println(user.getUsername());
        System.out.println(String.format("第%d次調用getUsername結束\n********\n", i));
    }
}
           

@1:将自定義作用域注冊到spring容器中

@2:從容器中擷取User對應的bean

@3:輸出這個bean對應的class,一會認真看一下,這個類型是不是User類型的

代碼後面又搞了3次循環,調用user的getUsername方法,并且方法前後分别輸出了一行日志。

見證奇迹的時候到了,運作輸出

從容器中擷取User對象
user對象的class為:class com.javacode2018.lesson002.demo18.test3.User$EnhancerBySpringCGLIB$80233127
多次調用user的getUsername感受一下效果

********
第1次開始調用getUsername
BeanMyScope >>>>>>>>> get:scopedTarget.user
---------建立User對象com.javacode2018.lesson002.demo18.test3.User@6a370f4
7b41aa80-7569-4072-9d40-ec9bfb92f438
第1次調用getUsername結束
********

********
第2次開始調用getUsername
BeanMyScope >>>>>>>>> get:scopedTarget.user
---------建立User對象com.javacode2018.lesson002.demo18.test3.User@1613674b
01d67154-95f6-44bb-93ab-05a34abdf51f
第2次調用getUsername結束
********

********
第3次開始調用getUsername
BeanMyScope >>>>>>>>> get:scopedTarget.user
---------建立User對象com.javacode2018.lesson002.demo18.test3.User@27ff5d15
76d0e86f-8331-4303-aac7-4acce0b258b8
第3次調用getUsername結束
********
           

從輸出的前2行可以看出:

  1. 調用context.getBean(User.class)從容器中擷取bean的時候,此時并沒有調用User的構造函數去建立User對象
  2. 第二行輸出的類型可以看出,getBean傳回的user對象是一個cglib代理對象。

後面的日志輸出可以看出,每次調用user.getUsername方法的時候,内部自動調用了BeanMyScope#get 方法和 User的構造函數。

通過上面的案例可以看出,當自定義的Scope中proxyMode=ScopedProxyMode.TARGET_CLASS的時候,會給這個bean建立一個代理對象,調用代理對象的任何方法,都會調用這個自定義的作用域實作類(上面的BeanMyScope)中get方法來重新來擷取這個bean對象。

動态重新整理@Value具體實作

那麼我們可以利用上面講解的這種特性來實作@Value的動态重新整理,可以實作一個自定義的Scope,這個自定義的Scope支援@Value注解自動重新整理,需要使用@Value注解自動重新整理的類上面可以标注這個自定義的注解,當配置修改的時候,調用這些bean的任意方法的時候,就讓spring重新開機初始化一下這個bean,這個思路就可以實作了,下面我們來寫代碼。

先來自定義一個Scope:RefreshScope

package com.javacode2018.lesson002.demo18.test4;

import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;

import java.lang.annotation.*;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Scope(BeanRefreshScope.SCOPE_REFRESH)
@Documented
public @interface RefreshScope {
    ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS; //@1
}
           

要求标注@RefreshScope注解的類支援動态重新整理@Value的配置

@1:這個地方是個關鍵,使用的是ScopedProxyMode.TARGET_CLASS

這個自定義Scope對應的解析類

下面類中有幾個無關的方法去掉了,可以忽略

package com.javacode2018.lesson002.demo18.test4;


import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.config.Scope;
import org.springframework.lang.Nullable;

import java.util.concurrent.ConcurrentHashMap;

public class BeanRefreshScope implements Scope {

    public static final String SCOPE_REFRESH = "refresh";

    private static final BeanRefreshScope INSTANCE = new BeanRefreshScope();

    //來個map用來緩存bean
    private ConcurrentHashMap<String, Object> beanMap = new ConcurrentHashMap<>(); //@1

    private BeanRefreshScope() {
    }

    public static BeanRefreshScope getInstance() {
        return INSTANCE;
    }

    /**
     * 清理目前
     */
    public static void clean() {
        INSTANCE.beanMap.clear();
    }

    @Override
    public Object get(String name, ObjectFactory<?> objectFactory) {
        Object bean = beanMap.get(name);
        if (bean == null) {
            bean = objectFactory.getObject();
            beanMap.put(name, bean);
        }
        return bean;
    }

}
           

上面的get方法會先從beanMap中擷取,擷取不到會調用objectFactory的getObject讓spring建立bean的執行個體,然後丢到beanMap中

上面的clean方法用來清理beanMap中目前已緩存的所有bean

來個郵件配置類,使用@Value注解注入配置,這個bean作用域為自定義的@RefreshScope

package com.javacode2018.lesson002.demo18.test4;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

/**
 * 郵件配置資訊
 */
@Component
@RefreshScope //@1
public class MailConfig {

    @Value("${mail.username}") //@2
    private String username;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    @Override
    public String toString() {
        return "MailConfig{" +
                "username='" + username + '\'' +
                '}';
    }
}
           

@1:使用了自定義的作用域@RefreshScope

@2:通過@Value注入mail.username對一個的值

重寫了toString方法,一會測試時候可以看效果。

再來個普通的bean,内部會注入MailConfig

package com.javacode2018.lesson002.demo18.test4;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class MailService {
    @Autowired
    private MailConfig mailConfig;

    @Override
    public String toString() {
        return "MailService{" +
                "mailConfig=" + mailConfig +
                '}';
    }
}
           

代碼比較簡單,重寫了toString方法,一會測試時候可以看效果。

來個類,用來從db中擷取郵件配置資訊

package com.javacode2018.lesson002.demo18.test4;

import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

public class DbUtil {
    /**
     * 模拟從db中擷取郵件配置資訊
     *
     * @return
     */
    public static Map<String, Object> getMailInfoFromDb() {
        Map<String, Object> result = new HashMap<>();
        result.put("mail.username", UUID.randomUUID().toString());
        return result;
    }
}
           

來個spring配置類,掃描加載上面的元件

package com.javacode2018.lesson002.demo18.test4;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan
public class MainConfig4 {
}
           

來個工具類

内部有2個方法,如下:

package com.javacode2018.lesson002.demo18.test4;

import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.core.env.MapPropertySource;

import java.util.Map;

public class RefreshConfigUtil {
    /**
     * 模拟改變資料庫中都配置資訊
     */
    public static void updateDbConfig(AbstractApplicationContext context) {
        //更新context中的mailPropertySource配置資訊
        refreshMailPropertySource(context);

        //清空BeanRefreshScope中所有bean的緩存
        BeanRefreshScope.getInstance().clean();
    }

    public static void refreshMailPropertySource(AbstractApplicationContext context) {
        Map<String, Object> mailInfoFromDb = DbUtil.getMailInfoFromDb();
        //将其丢在MapPropertySource中(MapPropertySource類是spring提供的一個類,是PropertySource的子類)
        MapPropertySource mailPropertySource = new MapPropertySource("mail", mailInfoFromDb);
        context.getEnvironment().getPropertySources().addFirst(mailPropertySource);
    }

}
           

updateDbConfig方法模拟修改db中配置的時候需要調用的方法,方法中2行代碼,第一行代碼調用refreshMailPropertySource方法修改容器中郵件的配置資訊

BeanRefreshScope.getInstance().clean()用來清除BeanRefreshScope中所有已經緩存的bean,那麼調用bean的任意方法的時候,會重新出發spring容器來建立bean,spring容器重新建立bean的時候,會重新解析@Value的資訊,此時容器中的郵件配置資訊是新的,是以@Value注入的資訊也是新的。

來個測試用例

@Test
public void test4() throws InterruptedException {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
    context.getBeanFactory().registerScope(BeanRefreshScope.SCOPE_REFRESH, BeanRefreshScope.getInstance());
    context.register(MainConfig4.class);
    //重新整理mail的配置到Environment
    RefreshConfigUtil.refreshMailPropertySource(context);
    context.refresh();

    MailService mailService = context.getBean(MailService.class);
    System.out.println("配置未更新的情況下,輸出3次");
    for (int i = 0; i < 3; i++) { //@1
        System.out.println(mailService);
        TimeUnit.MILLISECONDS.sleep(200);
    }

    System.out.println("模拟3次更新配置效果");
    for (int i = 0; i < 3; i++) { //@2
        RefreshConfigUtil.updateDbConfig(context); //@3
        System.out.println(mailService);
        TimeUnit.MILLISECONDS.sleep(200);
    }
}
           

@1:循環3次,輸出mailService的資訊

@2:循環3次,内部先通過@3來模拟更新db中配置資訊,然後在輸出mailService資訊

見證奇迹的時刻,來看效果

配置未更新的情況下,輸出3次
MailService{mailConfig=MailConfig{username='df321543-8ca7-4563-993a-bd64cbf50d53'}}
MailService{mailConfig=MailConfig{username='df321543-8ca7-4563-993a-bd64cbf50d53'}}
MailService{mailConfig=MailConfig{username='df321543-8ca7-4563-993a-bd64cbf50d53'}}
模拟3次更新配置效果
MailService{mailConfig=MailConfig{username='6bab8cea-9f4f-497d-a23a-92f15d0d6e34'}}
MailService{mailConfig=MailConfig{username='581bf395-f6b8-4b87-84e6-83d3c7342ca2'}}
MailService{mailConfig=MailConfig{username='db337f54-20b0-4726-9e55-328530af6999'}}
           
上面MailService輸出了6次,前3次username的值都是一樣的,後面3次username的值不一樣了,說明修改配置起效了。

小結

動态@Value實作的關鍵是@Scope中proxyMode參數,值為ScopedProxyMode.DEFAULT,會生成一個代理,通過這個代理來實作@Value動态重新整理的效果,這個地方是關鍵。

有興趣的可以去看一下springboot中的@RefreshScope注解源碼,和我們上面自定義的@RefreshScope類似,實作原理類似的。