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

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

作者: linyb极客之路 | 来源:发表于2024-07-15 09:29 被阅读0次

    前言

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

    如何实现属性的热更新

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

    org.springframework.cloud.context.environment.EnvironmentManager
    

    来实现如上效果

    如何实现

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

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

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

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

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

    示例

    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格式的数据如下

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

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

    488463a3df3805182036e8e0ff43850c_ad43806134b6fd5077c8f8d2660e722c.png

    如何浅浅封装

    1、封装属性绑定接口

    @FunctionalInterface
    public interface PropertyRebinder {
    
        void binder(RefreshProperty refreshProperty);
    }
    

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

    public interface PropertyRefreshedSync {
    
        void execute(String name,Object value);
    }
    

    3、监听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)));
    
        }
    
    

    示例应用

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

    1、编写授权属性配置类

    @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、编写授权拦截器

    @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、授权拦截器装配

    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、编写需授权访问的控制器

    @RestController
    @RequestMapping("config")
    @RequiredArgsConstructor
    public class ConfigController {
    
        private final AuthProperty authProperty;
    
    
        @GetMapping("get")
        public AuthProperty get(){
            return authProperty;
        }
    
    
    }
    
    

    5、测试

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

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

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

    3aa875451947bc210d9522b8b4b6c750_174060d16f24f2d1b61238a267a67469.png

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

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

    控制台输出


    86ad2bb50e5be7fd163f73610aaff76b_2a7f4b2853b4fb9bc6b2c95314b44917.png

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

    0e7dd3ff1f05523eb3b4520894919d83_89eb6ed580f307226c067716ca30648d.png

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

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

       @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",观察控制台结果


    7d6198e70d9be8e7364242491e0cf5b8_376484c0315e33772ade3dccc558fa98.png

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

    总结

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

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

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

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

    思路如下

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

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

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

    demo链接

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

    相关文章

      网友评论

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

          本文链接:https://www.haomeiwen.com/subject/etchhjtx.html