2

前言

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

如何实现属性的热更新

如果我们属性是配置在配置文件中,我们可以通过监听文件的变化,然后进行属性重新绑定。那我们如何实现这种效果呢,我们可以利用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步配置,就可以实现属性的变更了,是不是感觉到很简单。不过正常我们会浅浅封装下,在讲如何浅浅封装的时候,我先讲下,他大体实现变更的流程思路.如下

如何浅浅封装

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",正常是可以访问

b、 场景二:打开授权拦截器
 @Test
    public void testRefreshPropertyEnabled(){
        String name = AuthProperty.PREFIX + ".enabled";
        String value = "true";
        refreshProperty(name, value);
    }

控制台输出

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


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

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);
    }

控制台输出

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

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

总结

利用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


linyb极客之路
330 声望191 粉丝