项目中,有时候会遇到这样的需求:更新配置后,需要重新处理相关的业务,但是不想重启应用。例如 elasticsearch 证书过期后,需要更换 http_ca.crt ,但是又不想重启应用。
本人对 spring IOC 的源码不算深入,只知道可以实现,捣鼓了大半天,终于实现了,特意记录下过程。写个 demo ,希望有需要的人可以参考一下。
使用 @Bean 来创建注册业务逻辑 bean。当配置更新后,在监听事件里,创建新的业务逻辑 bean,处理业务逻辑,然后使用 beanFactory 销毁旧的业务逻辑 bean 后再重新注册新的业务逻辑bean
本人使用的是 nacos 配置中心,这里没有给出 helloWorld.yaml 文件,请自行创建
ConfigCenterConfigRefreshed
import com.alibaba.fastjson2.JSONObject;
import com.alibaba.nacos.api.config.listener.AbstractSharedListener;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import org.springframework.cloud.endpoint.event.RefreshEvent;
import org.springframework.context.ApplicationListener;
import java.lang.reflect.Field;
/**
* nacos 配置中心的配置文件修改的事件监听。
* 注意:只有启用自动刷新的配置才会收到配置中心文件修改从而才会触发事件。
* @Author: wuYaFang
* @Date: 2022/5/5
*/
public interface ConfigCenterConfigRefreshed extends ApplicationListener<RefreshEvent> {
String getDataId();
String getGroup();
@Override
default void onApplicationEvent(RefreshEvent event){
Object source1 = event.getSource();
if (source1 == null || !(source1 instanceof AbstractSharedListener)) {
return;
}
AbstractSharedListener source = (AbstractSharedListener) event.getSource();
Field[] declaredFields = AbstractSharedListener.class.getDeclaredFields();
JSONObject obj = new JSONObject();
for (int i = 0; i < declaredFields.length; i++) {
try {
Field field = declaredFields[i];
field.setAccessible(true);
Object o = field.get(source);
obj.put(field.getName(), o);
field.setAccessible(false);
} catch (IllegalAccessException ex) {
ex.printStackTrace();
return;
}
}
Source source2 = obj.toJavaObject(Source.class);
if (!StringUtils.equals(source2.getDataId(), getDataId()) || !StringUtils.equals(source2.getGroup(), getGroup())) {
return;
}
refreshed(source2);
}
/**
* 配置更新之后的处理。
* @param source
*/
default void refreshed(Source source) {
}
@Data
class Source {
private String dataId;
private String group;
}
}
HelloWorldConfig
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;
@Data
@Component
@RefreshScope
@ConfigurationProperties(prefix = "hello", ignoreInvalidFields = true)
public class HelloWorldConfig {
private String name;
}
HelloWorldLogic
package com.demo;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
@Data
@Slf4j
public class HelloWorldLogic {
private HelloWorldConfig helloWorldConfig;
public void say() {
log.info("hello-------------{}", helloWorldConfig);
}
}
HelloWorldConfiguration
package com.demo;
import com.hmc.cloud.util.api.ConfigCenterConfigRefreshed;
import com.hmc.common.util.HmcSpringBeanUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class HelloWorldConfiguration implements ConfigCenterConfigRefreshed {
@Autowired
private DefaultListableBeanFactory beanFactory;
@Autowired
private HelloWorldConfig helloWorldConfig;
@Bean
public HelloWorldLogic helloWorldLogic() {
return createHelloWorldLogic();
}
public HelloWorldLogic createHelloWorldLogic() {
HelloWorldLogic logic = new HelloWorldLogic();
return logic;
}
@Override
public String getDataId() {
return "helloWorld.yaml";
}
@Override
public String getGroup() {
return "DEFAULT";
}
@Override
public void refreshed(Source source) {
// 在 nacos 中修改发布 helloWorld.yaml 之后,更新了 HelloWorldConfig 之后,会调用 此方法 refreshed(Source source)
// 重新创建 HelloWorldLogic 实例
HelloWorldLogic logic = createHelloWorldLogic();
// 处理业务逻辑
logic.setHelloWorldConfig(helloWorldConfig);
logic.say();
// 重新注册到 IOC 容器中, 然后会将新的 helloWorld 实例更新到 HelloWorldController 中。
// 需要注意,需要在 HelloWorldController 类上声明 @RefreshScope
HmcSpringBeanUtil.registerSingleton("helloWorldLogic", logic, beanFactory);
}
}
HelloWorldController
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RefreshScope
@RestController
public class HelloWorldController {
@Autowired
private HelloWorldLogic helloWorldLogic;
@GetMapping("/test/helloWorld")
public HelloWorldConfig helloWorld() {
helloWorldLogic.say();
return helloWorldLogic.getHelloWorldConfig();
}
}
HmcSpringBeanUtil
import org.springframework.beans.factory.config.NamedBeanHolder;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.RootBeanDefinition;
/**
* spring bean 的辅助工具
* @author wuYaFang
* @date 2024/7/17 21:23
*/
public class HmcSpringBeanUtil {
/**
* <pre>
* 注册单例Bean
* 注意:重新注册 bean 之后,如果依赖者需要声明有 {@link org.springframework.cloud.context.config.annotation.RefreshScope} 才会更新注入
*
* 参考来源:https://cloud.tencent.com/developer/article/2393796
* </pre>
* @param beanName 名称
* @param singletonObject 实例对象
*/
public static void registerSingleton(String beanName, Object singletonObject, DefaultListableBeanFactory beanFactory) {
// 如果已经存在,则先销毁
if (beanFactory.containsSingleton(beanName)) {
unregisterSingleton(beanName,beanFactory);
}
beanFactory.registerSingleton(beanName, singletonObject);
}
/**
* <pre>
* 注册单例Bean
* 注意:重新注册 bean 之后,如果依赖者需要声明有 {@link org.springframework.cloud.context.config.annotation.RefreshScope} 才会更新注入
*
* 参考来源:https://cloud.tencent.com/developer/article/2393796
* </pre>
* @param beanClass 类
* @param singletonObject 实例对象
*/
public static void registerSingleton(Class<?> beanClass, Object singletonObject, DefaultListableBeanFactory beanFactory) {
String beanName = beanClass.getName();
// 如果已经存在,则先销毁
if (beanFactory.containsSingleton(beanName)) {
unregisterSingleton(beanClass, beanFactory);
}
RootBeanDefinition rootBeanDefinition = new RootBeanDefinition(beanClass);
NamedBeanHolder<?> namedBeanHolder = beanFactory.resolveNamedBean(beanClass);
beanFactory.registerBeanDefinition(namedBeanHolder.getBeanName(), rootBeanDefinition);
beanFactory.registerSingleton(beanName, singletonObject);
}
/**
* <pre>
* 注销Bean
*
* 参考来源:https://cloud.tencent.com/developer/article/2393796
* </pre>
* @param beanName
* @param beanFactory
*/
public static void unregisterSingleton(String beanName, DefaultListableBeanFactory beanFactory) {
if (beanFactory instanceof DefaultListableBeanFactory) {
// 首先确保销毁该bean的实例(如果该bean实例是一个单例的话)
if (beanFactory.containsSingleton(beanName)) {
beanFactory.destroySingleton(beanName);
}
// 然后从容器的bean定义注册表中移除该bean定义
if (beanFactory.containsBeanDefinition(beanName)) {
beanFactory.removeBeanDefinition(beanName);
}
}
}
/**
* <pre>
* 注销Bean
* 参考来源:https://cloud.tencent.com/developer/article/2393796
* @param beanClass 类
*/
public static void unregisterSingleton(Class<?> beanClass, DefaultListableBeanFactory beanFactory) {
String beanName = beanClass.getName();
if (beanFactory instanceof DefaultListableBeanFactory) {
// 首先确保销毁该bean的实例(如果该bean实例是一个单例的话)
if (beanFactory.containsSingleton(beanName)) {
beanFactory.destroySingleton(beanName);
}
// 然后从容器的bean定义注册表中移除该bean定义
if (beanFactory.containsBeanDefinition(beanName)) {
beanFactory.removeBeanDefinition(beanName);
}
}
}
}
bootstrap yaml 配置
spring:
cloud:
nacos:
config:
server-addr: ${nacos.config.server-addr}
username: ${nacos.config.username}
password: ${nacos.config.password}
namespace: ${nacos.config.namespace}
enable-remote-sync-config: on
group: ${model}
file-extension: yaml
prefix: ${model}
extension-configs:
- dataId: helloWorld.yaml
group: hmc-global
refresh: true