前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >聊聊springboot项目脱离配置中心,如何实现属性动态刷新

聊聊springboot项目脱离配置中心,如何实现属性动态刷新

原创
作者头像
lyb-geek
发布2024-07-16 09:43:56
1750
发布2024-07-16 09:43:56
举报
文章被收录于专栏:Linyb极客之路

前言

如果大家有开发过微服务项目,那对配置中心应该是耳熟能详了,配置中心有个很有用的能力,就是热更新属性,即不重启服务,就能做到属性的动态变更。而我们今天讲的话题是,怎么样不使用配置中心,也能达到如上的效果

如何实现属性的热更新

如果我们属性是配置在配置文件中,我们可以通过监听文件的变化,然后进行属性重新绑定。那我们如何实现这种效果呢,我们可以利用hutool提供的cn.hutool.core.io.watch.WatchMonitor或者是apache提供的commons-io下的org.apache.commons.io.monitor.FileAlterationObserver实现文件监听变化,然后在监听变化的监听器里面进行属性绑定。然而今天我们介绍不是这种,我们介绍是通过spring-cloud-context里面提供的

代码语言:java
复制
org.springframework.cloud.context.environment.EnvironmentManager

来实现如上效果

如何实现

1、在项目的pom引入spring-cloud-context gav

代码语言:java
复制
    <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-context</artifactId>
        </dependency>

因为要暴露env端点,所以还要引入

代码语言:java
复制
  <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

2、在项目的yml文件开启访问env端点以及将management.endpoint.env.post.enabled设置为true

示例

代码语言:java
复制
management:
  endpoints:
    web:
      exposure:
        include: "*"

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

注: management.endpoint.env.post.enabled不配制,默认也生效

3、通过客户端工具post请求访问http://ip:端口/actuator/env。以json格式发送

json格式的数据如下

代码语言:yaml
复制
{
"name":"需要变更的key",
"value":"变更后的value"
}

通过以上3步配置,就可以实现属性的变更了,是不是感觉到很简单。不过正常我们会浅浅封装下,在讲如何浅浅封装的时候,我先讲下,他大体实现变更的流程思路.如下

如何浅浅封装

1、封装属性绑定接口

代码语言:java
复制
@FunctionalInterface
public interface PropertyRebinder {

    void binder(RefreshProperty refreshProperty);
}

2、封装属性变更同步接口

代码语言:java
复制
public interface PropertyRefreshedSync {

    void execute(String name,Object value);
}

3、监听EnvironmentChangeEvent事件

核心代码如下

代码语言:java
复制
  @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)));

    }

示例应用

示例模拟演示一个授权访问的例子

1、编写授权属性配置类

代码语言:java
复制
@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、编写授权拦截器

代码语言:java
复制
@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、授权拦截器装配

代码语言:java
复制
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、编写需授权访问的控制器

代码语言:java
复制
@RestController
@RequestMapping("config")
@RequiredArgsConstructor
public class ConfigController {

    private final AuthProperty authProperty;


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


}

5、测试

a、 场景一:授权拦截器关闭

代码语言:java
复制
  @Test
    public void testGetProperty(){
                   ForestResponse response = Forest.get(serverUrl + "/config/get").executeAsResponse();
            PrintUtils.print(response.getContent());
    }

一开始我们授权拦截器是关闭的,因此我们访问"/config/get",正常是可以访问

b、 场景二:打开授权拦截器

代码语言:java
复制
 @Test
    public void testRefreshPropertyEnabled(){
        String name = AuthProperty.PREFIX + ".enabled";
        String value = "true";
        refreshProperty(name, value);
    }

控制台输出

此时再访问"/config/get",观察控制台结果

因为没授权,因此无法访问

c、 场景三:打开授权拦截器,新增白名单

代码语言:java
复制
   @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",观察控制台结果

可以正常拿到结果,而且结果还是属性热更新后的结果,说明整个动态刷新的效果是有效的

总结

利用spring-cloud-context提供的API来实现一个属性热更新,还是挺容易的。但这种方式是有局限性的,比如集群环境,就涉及到属性的更新同步,其次因为变更,本质是刷新bean的内存值,这就意味着服务一旦重启,刷新的值就会恢复成初始值。

可能大家会感觉spring-cloud-context提供的这个功能有点鸡肋,还不如直接用配置中心,但如果大家springcloud用得多,就会发现springcloud它可能更多提供是API抽象能力,而非具体实现。因此我们其实可以根据springcloud 提供的API扩展出一个简易版的配置中心出来

其次上述的方式有一种感觉挺实用的功能是结合业务场景,做业务属性的热替换,比如示例中的授权属性,动态添加白名单,当然使用的前提是项目中没有使用配置中心

最后再补充说明一下,上述的方式是针对加了@ConfigurationProperties注解属性的动态刷新。还有一种是加了@Value注解的属性,该属性刷新本文没介绍,不过这边提供一下@Value的实现刷新的思路。

思路如下

在引用@Value属性的bean,通常是一个controller,在这个controller加上@RefreshScope注解。当监听器监听到EnvironmentChangeEvent事件后,触发调用下

代码语言:java
复制
org.springframework.cloud.context.refresh.ContextRefresher#refresh

方法。就可实现@Value值变化的动态刷新。感兴趣的朋友,可以查看下方demo链接

demo链接

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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 如何实现属性的热更新
  • 如何实现
  • 如何浅浅封装
  • 示例应用
  • 总结
  • demo链接
相关产品与服务
微服务引擎 TSE
微服务引擎(Tencent Cloud Service Engine)提供开箱即用的云上全场景微服务解决方案。支持开源增强的云原生注册配置中心(Zookeeper、Nacos 和 Apollo),北极星网格(腾讯自研并开源的 PolarisMesh)、云原生 API 网关(Kong)以及微服务应用托管的弹性微服务平台。微服务引擎完全兼容开源版本的使用方式,在功能、可用性和可运维性等多个方面进行增强。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档