前言
如果大家有開發(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)變更的流程思路.如下
如何淺淺封裝
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",正常是可以訪問
b挽荡、 場景二:打開授權(quán)攔截器
@Test
public void testRefreshPropertyEnabled(){
String name = AuthProperty.PREFIX + ".enabled";
String value = "true";
refreshProperty(name, value);
}
控制臺輸出
此時再訪問"/config/get"藐石,觀察控制臺結(jié)果
因?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);
}
控制臺輸出
此時在訪問"/config/get",觀察控制臺結(jié)果
可以正常拿到結(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