聊聊springboot項(xiàng)目脫離配置中心拴驮,如何實(shí)現(xiàn)屬性動態(tài)刷新

前言

如果大家有開發(fā)過微服務(wù)項(xiàng)目,那對配置中心應(yīng)該是耳熟能詳了柴信,配置中心有個很有用的能力套啤,就是熱更新屬性,即不重啟服務(wù)随常,就能做到屬性的動態(tài)變更潜沦。而我們今天講的話題是,怎么樣不使用配置中心绪氛,也能達(dá)到如上的效果

如何實(shí)現(xiàn)屬性的熱更新

如果我們屬性是配置在配置文件中唆鸡,我們可以通過監(jiān)聽文件的變化,然后進(jìn)行屬性重新綁定枣察。那我們?nèi)绾螌?shí)現(xiàn)這種效果呢争占,我們可以利用hutool提供的cn.hutool.core.io.watch.WatchMonitor或者是apache提供的commons-io下的org.apache.commons.io.monitor.FileAlterationObserver實(shí)現(xiàn)文件監(jiān)聽變化燃逻,然后在監(jiān)聽變化的監(jiān)聽器里面進(jìn)行屬性綁定。然而今天我們介紹不是這種燃乍,我們介紹是通過spring-cloud-context里面提供的

org.springframework.cloud.context.environment.EnvironmentManager

來實(shí)現(xiàn)如上效果

如何實(shí)現(xiàn)

1唆樊、在項(xiàng)目的pom引入spring-cloud-context gav

    <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-context</artifactId>
        </dependency>

因?yàn)橐┞秂nv端點(diǎn),所以還要引入

  <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

2刻蟹、在項(xiàng)目的yml文件開啟訪問env端點(diǎn)以及將management.endpoint.env.post.enabled設(shè)置為true

示例

management:
  endpoints:
    web:
      exposure:
        include: "*"

  endpoint:
    health:
      show-details: always
    env:
      post:
        enabled: true

注: management.endpoint.env.post.enabled不配制逗旁,默認(rèn)也生效

3、通過客戶端工具post請求訪問http://ip:端口/actuator/env舆瘪。以json格式發(fā)送

json格式的數(shù)據(jù)如下

{
"name":"需要變更的key",
"value":"變更后的value"
}

通過以上3步配置片效,就可以實(shí)現(xiàn)屬性的變更了,是不是感覺到很簡單英古。不過正常我們會淺淺封裝下淀衣,在講如何淺淺封裝的時候,我先講下召调,他大體實(shí)現(xiàn)變更的流程思路.如下

488463a3df3805182036e8e0ff43850c_ad43806134b6fd5077c8f8d2660e722c.png

如何淺淺封裝

1膨桥、封裝屬性綁定接口

@FunctionalInterface
public interface PropertyRebinder {

    void binder(RefreshProperty refreshProperty);
}

2、封裝屬性變更同步接口

public interface PropertyRefreshedSync {

    void execute(String name,Object value);
}

3唠叛、監(jiān)聽EnvironmentChangeEvent事件

核心代碼如下

  @EventListener(EnvironmentChangeEvent.class)
    public void listener(EnvironmentChangeEvent event){
        if(CollectionUtils.isEmpty(propertyRebinders)){
            return;
        }
        RefreshProperty refreshProperty = get(event.getKeys());
        propertyRebinders.forEach(propertyRebinder -> run(() -> propertyRebinder.binder(refreshProperty)));

    }

示例應(yīng)用

示例模擬演示一個授權(quán)訪問的例子

1只嚣、編寫授權(quán)屬性配置類

@Data
@AllArgsConstructor
@NoArgsConstructor
@ConfigurationProperties(prefix = AuthProperty.PREFIX)
public class AuthProperty {

    public static final String PREFIX = "lybgeek.auth";

    private boolean enabled;

    private String tokenKey = "token";

    private List<String> whitelistUrls;
}

2、編寫授權(quán)攔截器

@Slf4j
public class AuthHandlerInterceptor implements HandlerInterceptor {

    @Autowired
    private AuthProperty authProperty;

    @Autowired
    private WebEndpointProperties webEndpointProperties;
    
    private final AntPathMatcher antPathMatcher = new AntPathMatcher();

    public static final String MOCK_TOKEN_VALUE = "123456";

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if(log.isDebugEnabled()){
            log.debug("url:{},queryString:{}",request.getRequestURI(),request.getQueryString());
        }
        if(!authProperty.isEnabled()){
            return true;
        }
        if(isWhiteList(request)){
            return true;
        }

        String token = request.getHeader(authProperty.getTokenKey());
        if(MOCK_TOKEN_VALUE.equals(token)){
            return true;
        }

        throw new AuthException("token is not valid:" + token, HttpStatus.UNAUTHORIZED.name());
    }

    private boolean isWhiteList(HttpServletRequest request) {
        String url = request.getRequestURI();
        if(CollectionUtil.isNotEmpty(authProperty.getWhitelistUrls())){
            for (String whitelistUrl : authProperty.getWhitelistUrls()) {
               boolean isMatch = isMatch(whitelistUrl,url);
               if(isMatch){
                   return true;
                }
            }
        }
        boolean isMatchLogger = isMatch("/"+BASE_LOG_URL + "/**",url);
        if(isMatchLogger){
            return true;
        }
        return isMatch(webEndpointProperties.getBasePath() + "/**",url);
    }

    private boolean isMatch(String pattern, String url){
        if(antPathMatcher.match(pattern,url)){
            if(log.isDebugEnabled()){
                log.debug("url: {} is in whitelist",url);
            }
            return true;
        }
        return false;
    }
}

3艺沼、授權(quán)攔截器裝配

Configuration
@EnableConfigurationProperties(AuthProperty.class)
public class AuthAutoConfiguration implements WebMvcConfigurer {



    @Bean
    @ConditionalOnMissingBean
    public AuthHandlerInterceptor authHandlerInterceptor(){
        return new AuthHandlerInterceptor();
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(authHandlerInterceptor()).addPathPatterns("/**");
    }
}

4册舞、編寫需授權(quán)訪問的控制器

@RestController
@RequestMapping("config")
@RequiredArgsConstructor
public class ConfigController {

    private final AuthProperty authProperty;


    @GetMapping("get")
    public AuthProperty get(){
        return authProperty;
    }


}

5、測試

a障般、 場景一:授權(quán)攔截器關(guān)閉

  @Test
    public void testGetProperty(){
                   ForestResponse response = Forest.get(serverUrl + "/config/get").executeAsResponse();
            PrintUtils.print(response.getContent());
    }

一開始我們授權(quán)攔截器是關(guān)閉的调鲸,因此我們訪問"/config/get",正常是可以訪問

3aa875451947bc210d9522b8b4b6c750_174060d16f24f2d1b61238a267a67469.png

b挽荡、 場景二:打開授權(quán)攔截器

 @Test
    public void testRefreshPropertyEnabled(){
        String name = AuthProperty.PREFIX + ".enabled";
        String value = "true";
        refreshProperty(name, value);
    }

控制臺輸出


86ad2bb50e5be7fd163f73610aaff76b_2a7f4b2853b4fb9bc6b2c95314b44917.png

此時再訪問"/config/get"藐石,觀察控制臺結(jié)果

0e7dd3ff1f05523eb3b4520894919d83_89eb6ed580f307226c067716ca30648d.png

因?yàn)闆]授權(quán),因此無法訪問

c徐伐、 場景三:打開授權(quán)攔截器贯钩,新增白名單

   @Test
    public void testRefreshPropertyWhitelistUrls(){
        String name = AuthProperty.PREFIX + ".whitelistUrls";
        List<String> whitelistUrls = new ArrayList<>();
        whitelistUrls.add("/config/refresh");
        whitelistUrls.add("/config/get");
        String value = String.join(",", whitelistUrls);
        refreshProperty(name, value);
    }

控制臺輸出


32d6e7ce9bde7c42d23080d3984a52a5_a850112088323c67f2418070e20e8742.png

此時在訪問"/config/get",觀察控制臺結(jié)果


7d6198e70d9be8e7364242491e0cf5b8_376484c0315e33772ade3dccc558fa98.png

可以正常拿到結(jié)果办素,而且結(jié)果還是屬性熱更新后的結(jié)果角雷,說明整個動態(tài)刷新的效果是有效的

總結(jié)

利用spring-cloud-context提供的API來實(shí)現(xiàn)一個屬性熱更新,還是挺容易的性穿。但這種方式是有局限性的勺三,比如集群環(huán)境,就涉及到屬性的更新同步需曾,其次因?yàn)樽兏鸺幔举|(zhì)是刷新bean的內(nèi)存值祈远,這就意味著服務(wù)一旦重啟,刷新的值就會恢復(fù)成初始值商源。

可能大家會感覺spring-cloud-context提供的這個功能有點(diǎn)雞肋车份,還不如直接用配置中心,但如果大家springcloud用得多牡彻,就會發(fā)現(xiàn)springcloud它可能更多提供是API抽象能力扫沼,而非具體實(shí)現(xiàn)。因此我們其實(shí)可以根據(jù)springcloud 提供的API擴(kuò)展出一個簡易版的配置中心出來

其次上述的方式有一種感覺挺實(shí)用的功能是結(jié)合業(yè)務(wù)場景庄吼,做業(yè)務(wù)屬性的熱替換缎除,比如示例中的授權(quán)屬性,動態(tài)添加白名單总寻,當(dāng)然使用的前提是項(xiàng)目中沒有使用配置中心

最后再補(bǔ)充說明一下器罐,上述的方式是針對加了@ConfigurationProperties注解屬性的動態(tài)刷新。還有一種是加了@Value注解的屬性渐行,該屬性刷新本文沒介紹轰坊,不過這邊提供一下@Value的實(shí)現(xiàn)刷新的思路。

思路如下

在引用@Value屬性的bean祟印,通常是一個controller衰倦,在這個controller加上@RefreshScope注解。當(dāng)監(jiān)聽器監(jiān)聽到EnvironmentChangeEvent事件后旁理,觸發(fā)調(diào)用下

org.springframework.cloud.context.refresh.ContextRefresher#refresh

方法。就可實(shí)現(xiàn)@Value值變化的動態(tài)刷新我磁。感興趣的朋友孽文,可以查看下方demo鏈接

demo鏈接

https://github.com/lyb-geek/springboot-learning/tree/master/springboot-config-refresh

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市夺艰,隨后出現(xiàn)的幾起案子芋哭,更是在濱河造成了極大的恐慌,老刑警劉巖郁副,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件减牺,死亡現(xiàn)場離奇詭異,居然都是意外死亡存谎,警方通過查閱死者的電腦和手機(jī)拔疚,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來既荚,“玉大人稚失,你說我怎么就攤上這事∏∑福” “怎么了句各?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵吸占,是天一觀的道長。 經(jīng)常有香客問我凿宾,道長矾屯,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任初厚,我火速辦了婚禮件蚕,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘惧所。我一直安慰自己骤坐,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布下愈。 她就那樣靜靜地躺著纽绍,像睡著了一般。 火紅的嫁衣襯著肌膚如雪势似。 梳的紋絲不亂的頭發(fā)上拌夏,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天,我揣著相機(jī)與錄音履因,去河邊找鬼障簿。 笑死,一個胖子當(dāng)著我的面吹牛栅迄,可吹牛的內(nèi)容都是我干的站故。 我是一名探鬼主播,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼毅舆,長吁一口氣:“原來是場噩夢啊……” “哼西篓!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起憋活,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤岂津,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后悦即,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體吮成,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年辜梳,在試婚紗的時候發(fā)現(xiàn)自己被綠了粱甫。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡作瞄,死狀恐怖魔种,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情粉洼,我是刑警寧澤节预,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布叶摄,位于F島的核電站,受9級特大地震影響安拟,放射性物質(zhì)發(fā)生泄漏蛤吓。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一糠赦、第九天 我趴在偏房一處隱蔽的房頂上張望会傲。 院中可真熱鬧,春花似錦拙泽、人聲如沸淌山。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽泼疑。三九已至,卻和暖如春荷荤,著一層夾襖步出監(jiān)牢的瞬間退渗,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工蕴纳, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留会油,地道東北人。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓古毛,卻偏偏與公主長得像翻翩,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子稻薇,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評論 2 345

推薦閱讀更多精彩內(nèi)容