SpringBoot应用的配置文件默认是application.properties,而且必须在启动前就已经配置好,在运行过程中不允许修改。如果确实想让应用在运行过程中修改配置呢,我们可以将配置记录在Zookeeper上,借助Zookeeper的watcher机制来实现配置变更通知。
一般的属性获取示例: application.properties的配置如下:
name=ljh
ConfigController如下:
@RestController
public class ConfigController {
@Resource
private Environment environment;
@Value("${name}")
private String name;
@GetMapping("/")
public String index() {
String fromEnvironment = environment.getProperty("name");
return String.format("fromEnvironment: %s, fromValue: %s", fromEnvironment, name);
}
}
获取的两个属性值都是配置文件中配置的。
了解下这两个值是怎么获取的。Environment对象的属性如下: 我们自己配置的属性都被集中保存在了最后一个OriginTrackedMapPropertySource 的source中,默认的实现用了不允许修改的Map。我们@Value 注入的属性就是通过查找propertySource中的属性值设置的,因此我们只需要把加载的配置写入自定义的PropertySource中就可以在项目运行中从Environment获取。
ZK方式: 配置Zookeeper,先导入依赖,
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>2.12.0</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>2.12.0</version>
</dependency>
将ZK的服务地址配置在application.properties中
zookeeper.address=127.0.0.1:2181
配置ZK的客户端:
@Configuration
public class ZKConfig {
@Value("${zookeeper.address}")
private String zkAddr;
@Bean
public CuratorFramework curatorFramework() {
CuratorFramework curatorFramework = CuratorFrameworkFactory.builder()
.connectString(zkAddr)
.sessionTimeoutMs(5000)
.retryPolicy(new ExponentialBackoffRetry(1000, 3))
.build();
curatorFramework.start();
return curatorFramework;
}
}
编写从ZK获取属性,并设置到Environment中:
@Component
public class PropertySetting {
private static final String PATH = "/config";
private static final String ZKPROPERTY = "config resource zookeeper";
private static final String SCOPE = "ljhScope";
private static ConcurrentHashMap<String, String> sourceMap = new ConcurrentHashMap();
@Resource
private ConfigurableApplicationContext applicationContext;
@Resource
private CuratorFramework curatorFramework;
@PostConstruct
public void configProperty() {
try {
Stat stat = curatorFramework.checkExists().forPath(PATH);
if (stat == null) {
throw new RuntimeException("zookeeper上没有配置参数");
}
addPropertiesToSpringEnvironment();
} catch (Exception e) {
e.printStackTrace();
}
}
private void addPropertiesToSpringEnvironment() {
ConfigurableEnvironment environment = applicationContext.getEnvironment();
MutablePropertySources mutablePropertySources = environment.getPropertySources();
OriginTrackedMapPropertySource originTrackedMapPropertySource = new OriginTrackedMapPropertySource(ZKPROPERTY, sourceMap);
try {
List<String> paths = curatorFramework.getChildren().forPath(PATH);
for (String path : paths) {
sourceMap.put(path, new String(curatorFramework.getData().forPath(PATH + "/" + path)));
}
} catch (Exception e) {
e.printStackTrace();
}
mutablePropertySources.addLast(originTrackedMapPropertySource);
}
}
现在已经把属性注入到了环境变量中,但是ConfigController实例可能已经初始化了,所以为了验证,我把PropertySetting实例在ConfigController之前初始化,使用了Spring的org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor 接口,示例如下:
@Component
public class ConfigInstantiationAwareBeanPostProcessor implements SmartInstantiationAwareBeanPostProcessor {
@Resource
private ConfigurableApplicationContext applicationContext;
@Override
public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException {
if (beanClass == ConfigController.class) {
applicationContext.getBean(PropertySetting.class);
}
return null;
}
}
我把application.properties中的name属性注释掉,在ZK的/config/name下写入baba,现在运行结果两个属性输出都是baba。
这个时候我们在ZK上修改属性,程序还没有任何反应,所以需要在PropertySetting中加入配置变更通知,新加入代码如下:
private void childNodeListener() {
try {
PathChildrenCache cache = new PathChildrenCache(curatorFramework, PATH, false);
cache.start(PathChildrenCache.StartMode.POST_INITIALIZED_EVENT);
cache.getListenable().addListener(new PathChildrenCacheListener() {
@Override
public void childEvent(CuratorFramework curatorFramework, PathChildrenCacheEvent pathChildrenCacheEvent) {
switch (pathChildrenCacheEvent.getType()) {
case CHILD_ADDED:
System.out.println("增加子节点");
addPropertyToSpringEnvironment(pathChildrenCacheEvent.getData());
break;
case CHILD_REMOVED:
System.out.println("删除子节点");
deletePropertyFromSpringEnvironment(pathChildrenCacheEvent.getData());
break;
case CHILD_UPDATED:
System.out.println("更新子节点");
addPropertyToSpringEnvironment(pathChildrenCacheEvent.getData());
break;
default:
break;
}
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
private void deletePropertyFromSpringEnvironment(ChildData data) {
sourceMap.remove(data.getPath().substring(PATH.length() + 1));
}
private void addPropertyToSpringEnvironment(ChildData data) {
try {
byte[] value = curatorFramework.getData().forPath(data.getPath());
sourceMap.put(data.getPath().substring(PATH.length() + 1), new String(value));
} catch (Exception e) {
e.printStackTrace();
}
}
现在重新运行程序,在ZK中修改name的值后会发现environment.getProperty("name") 输出新设置的值,但是通过@Value 引入的值还是启动时的值。为什么呢?因为@Value 的值在Bean初始化的时候就已经设置完成了,尽管环境变量中的值已被修改,但是不会重新读取,所以还是原来的值。那么为了解决这个问题,我们就需要在配置修改的时候,去动态的刷新这些Bean,让他们重新从环境中读取一次配置。这里我自定义了一个scope,将需要动态刷新的Bean都使用自定义的Scope来维持。代码如下:
public class RefreshScope implements Scope {
private ConcurrentHashMap map = new ConcurrentHashMap();
@Override
public Object get(String name, ObjectFactory<?> objectFactory) {
if (map.containsKey(name)) {
return map.get(name);
} else {
Object obj = objectFactory.getObject();
map.put(name, obj);
return obj;
}
}
@Override
public Object remove(String name) {
return map.remove(name);
}
@Override
public void registerDestructionCallback(String name, Runnable callback) {
}
@Override
public Object resolveContextualObject(String key) {
return null;
}
@Override
public String getConversationId() {
return null;
}
}
对应的Spring源码部分为: 只是定义没用,需要注册到Spring容器中,代码如下:
@Getter
@Component
public class RefreshScopeRegistry implements BeanDefinitionRegistryPostProcessor {
private BeanDefinitionRegistry registry;
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
this.registry = registry;
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
beanFactory.registerScope("ljhScope", new RefreshScope());
}
}
现在在ConfigController上配置自定义的Scope,代码如下: 到这里已经将需要动态修改的Bean都保存到了RefreshScope的Map中,现在只需要在watcher监听到的事件中加入刷新Bean的动作就可以了。如下:
@Resource
private RefreshScopeRegistry registry;
private void refreshBeans() {
BeanDefinitionRegistry beanDefinitionRegistry = registry.getRegistry();
Arrays.stream(applicationContext.getBeanDefinitionNames()).forEach(beanDefinitionName -> {
BeanDefinition beanDefinition = beanDefinitionRegistry.getBeanDefinition(beanDefinitionName);
if (beanDefinition.getScope().equals(SCOPE)) {
applicationContext.getBeanFactory().destroyScopedBean(beanDefinitionName);
applicationContext.getBean(beanDefinitionName);
}
});
}
现在运行程序,在ZK中修改属性值,可以发现不管通过Environment还是通过@Value都可以获取到最新的值。 大功告成~
本文示例的源代码下载
|