对于 Nacos 的动态配置大家都很熟悉,在此我提出个疑问❓ 如何在 Nacos 配置中定义 Bean 信息且实现动态的添加删除 Bean 呢? 本文是结合工作中的实践,来讲下在 动态的可插拔式插件设计 的实现思路,可结合业务场景动态切换告警信息、短信服务商、动态线程池等等
既然要动态的配置 Bean,那么首先需要在配置中定义好 Bean 的结构,需要哪些属性。接下来我将以班级、学生与学生做的事来讲述代码思路
首先定义班级的属性,其中班级下有一群学生,对应的属性为 students
,每个学生都会做同一件事,接下来我们会将学生做一些事转换为动态的,可通过Nacos配置进行 增删改
java@Data
@ConfigurationProperties(prefix = ClassProperties.PREFIX)
public class ClassProperties {
/**
* 配置属性的前缀
*/
public static final String PREFIX = "class";
/**
* 属性名
*/
public static final String STU_PROPERTY_NAME = "class.students";
/**
* BeanName Template Name
*/
public static final String BEANNAME_TEMPLATE = "class.students[%s]";
/**
* 班级名称
*/
private String className;
/**
* 年级
*/
private String gradeName;
/**
* 班级学生
* <p>
* key:学号,value:学生信息
* </p>
*/
private Map<String, Student> students;
}
学生对象的属性
java@Data
@NoArgsConstructor
@AllArgsConstructor
public class Student {
private String name;
private Integer age;
private String sex;
}
声明学生所做的事情,本质上来说,这就是我们真实要处理业务场景的逻辑,可自行调整
javapublic interface StudentHandler {
String doSomething();
}
@Data
public class StudentDoSomethingHandler implements StudentHandler {
private String key;
private Student student;
public StudentDoSomethingHandler(String key, Student student) {
this.key = key;
this.student = student;
}
@Override
public String doSomething() {
// do something
return "【" + key + "】学生【" + student.getName() + "】,今年【" + student.getAge() + "岁】,性别【" + student.getSex() + "】,开始做点什么了...";
}
}
对象定义完了,来描述下Nacos中的属性, 2025140001
即为 Map<String, Student> students
的key,此为 唯一标识,用来进行学生的查找,同时也是 StudentDoSomethingHandler
中的属性 key
,而其下的学生信息对应着对象 student
。
yamlclass:
className: 十四班
gradeName: 初一
students:
2025140001:
name: 张三
age: 18
sex: 男
2025140002:
name: 紫馨
age: 20
sex: 女
2025140003:
name: 狗五松溪
age: 13
sex: 男
2025140004:
name: 构思
age: 10
sex: 未知
定义好属性后,我们需要将配置的Bean信息在项目启动时,注入到Spring容器中,交由Spring容器管理。
核心逻辑为:将定义的 Map<String, Student> students
转换成 StudentDoSomethingHandler
对象,注入到Spring容器中,其中 BeanName 使用模板 class.students[%s]
声明,将 key
替换到占位符中,最终定义为 class.students[2025140001]
,此方式便于随时查找Bean对象,整体实现代码如下
java@Component
public class StudentBeanDefinitionRegister implements ImportBeanDefinitionRegistrar, EnvironmentAware {
/**
* 班级的属性配置
*/
private ClassProperties classProperties;
/**
* Register bean definitions as necessary based on the given annotation metadata of
* the importing {@code @Configuration} class.
* <p>Note that {@link BeanDefinitionRegistryPostProcessor} types may <em>not</em> be
* registered here, due to lifecycle constraints related to {@code @Configuration}
* class processing.
* <p>The default implementation is empty.
*
* @param importingClassMetadata annotation metadata of the importing class
* @param registry current bean definition registry
*/
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
// 配置的学生信息
Map<String, Student> students = classProperties.getStudents();
if (!CollectionUtils.isEmpty(students)) {
// 将配置对象转换成Bean,注册到Spring容器中
students.forEach((key, student) -> {
// key:beanName, value:beanDefinition
registry.registerBeanDefinition(buildBeanName(key), buildBeanDefinition(StudentDoSomethingHandler.class, key, student));
});
}
}
/**
* 根据 Environment 中的属性名进行构建
*
* <p>
* 示例:class.students[...]
* </p>
*/
public String buildBeanName(String key) {
return String.format(ClassProperties.BEANNAME_TEMPLATE, key);
}
/**
* 构建 BeanDefinition
*
* @param clazz 构建 Bean 对象的类型
* @param constructorArguments Bean 的构造参数
* @param <T> -
* @return BeanDefinition
*/
public static <T> BeanDefinition buildBeanDefinition(Class<T> clazz, Object... constructorArguments) {
// 定义 BeanDefinition
BeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(clazz).getBeanDefinition();
ConstructorArgumentValues constructorArgumentValues = beanDefinition.getConstructorArgumentValues();
// 构建参数
for (int i = 0; i < constructorArguments.length; i++) {
constructorArgumentValues.addIndexedArgumentValue(i, constructorArguments[i]);
}
return beanDefinition;
}
/**
* Set the {@code Environment} that this component runs in.
*
* @param environment
*/
@Override
public void setEnvironment(Environment environment) {
// 绑定属性
classProperties = PropertiesBinder.bindProperties(ClassProperties.PREFIX, environment, ClassProperties.class);
}
}
BeanName 的生成
BeanName 的生成各种各样,并没有什么规则,但是有一点需要注意,那就是 保证唯一性,上述规则中就是根据 key 进行的生成,即保证了唯一性,同时在后续 删改 的过程中,可根据调整的 Nacos 配置属性随时定位到修改前的 Bean 对象进行处理。
依赖关系破坏
若实现过程中,其他的 Bean 引用了被移除的 Bean(简称 引用 Bean
),可能导致 NoSuchBeanDefinitionException
或空指针,我们应该避免此类情况。
如果无法避免的情况下,如果是 属性变更 导致的移除,只需要将引用过 被移除 Bean 的对象进行缓存,在移除 Bean 的同时,构建新的 Bean,在将引用 Bean 中的属性进行覆盖,或者移除引用 Bean,进行重新添加
如果是纯移除 Bean,其他引用到此移除 Bean 导致产生异常的,那只能通过调整代码来解决了
提供一个 Environment
环境属性中转换对象的工具类
javapublic class PropertiesBinder {
@SneakyThrows
public static <T> T bindProperties(String prefix, Environment environment, Class<T> clazz) {
// 构建对象
T t = clazz.getDeclaredConstructor().newInstance();
Binder binder = Binder.get(environment);
Bindable<?> target = Bindable.of(ResolvableType.forClass(clazz)).withExistingValue(t);
binder.bind(prefix, target);
return t;
}
}
实现了项目启动时,配置中 Bean 对象的初始化以后,我们接下来实现对配置中 Bean 对象的 增删改,来保证Spring容器中构建的对象为最新调整的属性
首先,当Nacos 平台上对配置进行变更时,我们需要做出扩展,触发Bean的动态刷新操作。从源码中可以得知,当配置变更时,客户端的 RefreshEventListener
监听器,会监听到 RefreshEvent
事件,执行 ContextRefresher #refresh()
方法,此方法中调用了内部方法 refreshEnvironment()
,而这个内部方法将会变更本地的配置,同时将变更的属性提取出来,并且发布了 EnvironmentChangeEvent
事件,那我们只需要监听这个事件即可
上述逻辑中提取的变更属性有3种操作情况,分别是
class.students[2025140001].age
class.students[2025140001].name
、class.students[2025140001].age
、class.students[2025140001].sex
监听到Nacos配置的变更后,我们需要从变更的属性中,只需要找出 ClassProperties #students
的属性变更,其中配置变更的操作也是3种,分别对应上述的配置变更的3种情况
Environment
中,如果还在,则为更新Environment
中,则视为删除属性结合上述思路,代码实现如下
java@Component
public class StudentConfigChangeListener implements EnvironmentAware, BeanFactoryAware, ApplicationListener<EnvironmentChangeEvent> {
private DefaultListableBeanFactory beanFactory;
private Environment environment;
/**
* Handle an application event.
*
* @param event the event to respond to
*/
@Override
public void onApplicationEvent(EnvironmentChangeEvent event) {
// 将当前符合的信息进行变更,其他变更过滤
parseBeanName(event.getKeys()).forEach(beanName -> {
// Environment 中是否还存在 Bean 的属性信息
boolean isExist = false;
Field[] fields = Student.class.getDeclaredFields();
for (int i = 0; i < fields.length && !isExist; i++) {
if (environment.containsProperty(beanName + "." + fields[i].getName())) {
isExist = true;
}
}
// 添加 Bean
if (isExist) {
registerBean(beanName);
}
// 删除 Bean
else {
removeBean(beanName);
}
});
}
/**
* 从变更的属性中解析出自定义的 BeanName
*/
public Set<String> parseBeanName(Set<String> keys) {
return keys.stream().filter(key -> key.startsWith(ClassProperties.STU_PROPERTY_NAME)).map(key -> key.substring(0, key.indexOf("]") + 1)).collect(Collectors.toSet());
}
/**
* 删除 Bean
*/
public void removeBean(String beanName) {
try {
StudentFactory.LOCK.writeLock().lock();
// 删除 BeanDefinition
if (beanFactory.containsBeanDefinition(beanName)) {
beanFactory.removeBeanDefinition(beanName);
}
// 销毁单例实例
if (beanFactory.containsSingleton(beanName)) {
beanFactory.destroySingleton(beanName);
}
// 删除缓存
StudentFactory.STUDETN_HANDLER.remove(beanName);
} finally {
StudentFactory.LOCK.writeLock().unlock();
}
}
/**
* 添加 Bean
*/
public void registerBean(String beanName) {
try {
StudentFactory.LOCK.writeLock().lock();
// 当前 Bean存在,则先删除 Bean
if (beanFactory.containsSingleton(beanName)) {
removeBean(beanName);
}
// 注册 BeanDefinition
String key = beanName.substring(beanName.indexOf("[") + 1, beanName.indexOf("]"));
Student student = PropertiesBinder.bindProperties(beanName, environment, Student.class);
beanFactory.registerBeanDefinition(beanName, StudentBeanDefinitionRegister.buildBeanDefinition(StudentDoSomethingHandler.class, key, student));
// 添加缓存
StudentFactory.STUDETN_HANDLER.put(beanName, beanFactory.getBean(beanName, StudentHandler.class));
} finally {
StudentFactory.LOCK.writeLock().unlock();
}
}
/**
* Set the {@code Environment} that this component runs in.
*
* @param environment
*/
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = (DefaultListableBeanFactory) beanFactory;
}
}
插件设计
需要注意的一点,以上的代码为插件的设计,尤其是Spring项目启动时自动注册Bean信息实现的 ImportBeanDefinitionRegistrar
接口,只会在自动装配中生效,2.7版本以前需要添加到 META-INF/spring.factories
, 2.7版本以后添加到 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
,具体实现可以查看 SpringBoot 自动装配原理的满分解读
单例 Bean 的生命周期
单例 Bean 的实例会被容器缓存,移除定义后需手动销毁实例,可查看 StudentConfigChangeListener #removeBean
方法中的代码实现
构建一个门面方法 doPlan
,根据指定的学生key(也就是nacos配置中的学号 2025140001
)来执行一些事情的方法
javapublic class StudentFactory {
/**
* 读写锁,保证动态Bean信息的线程安全
*/
public static final ReadWriteLock LOCK = new ReentrantReadWriteLock();
/**
* 每个学生对应的处理器缓存,可用来做一些事情
*
* <p>
* key -> beanName, 对应的格式为: class.students[...]
* value -> StudentHandler, 学生对应的处理器对象
* </p>
*/
public static Map<String, StudentHandler> STUDETN_HANDLER = new ConcurrentHashMap<>();
/**
* 执行学生的计划
*
* @param key 学生的key
* @return 学生的做一些事的结果
*/
public static String doPlan(String key) {
try {
LOCK.readLock().lock();
StudentHandler studentHandler = STUDETN_HANDLER.get(String.format(ClassProperties.BEANNAME_TEMPLATE, key));
Assert.notNull(studentHandler, "StudentHandler is null!");
return studentHandler.doSomething();
} finally {
LOCK.readLock().unlock();
}
}
}
并发问题
动态修改容器可能引发安全问题,通过可重入读写锁 ReentrantReadWriteLock
来确保操作原子性,在添加或删除Bean时使用写锁,在读取动态Bean时使用读锁,依赖读写互斥的特性,来解决并发问题。
在项目中添加接口进行测试
java@RestController
public class TestController {
/**
* 获取所有学生
*/
@GetMapping("/get")
public Object get() {
Map<String, StudentHandler> beansOfType = SpringUtil.getBeansOfType(StudentHandler.class);
return JSON.toJSON(beansOfType);
}
/**
* 让某个学生做一些事,例如让百度服务商发短信,或其他服务商发短信
*/
@GetMapping("/getStudent")
public String getStudent(String key) {
return StudentFactory.doPlan(key);
}
}
调整 Nacos 平台上的配置后,以上的Bean对象中也会跟着动态调整,访问 /get
接口查看,在更新配置后,刷新接口看内容是否发生变更
访问 /getStudent
接口,可让不同学生执行某些事情
在工作中,我是以此思路实现了 告警组件,结合el表达式,可动态调整告警信息。通过某次技术分享后,同事借鉴此思路实现了动态切换短信服务商,可结合实际的业务场景,自行实现动态的插件
本文作者:柳始恭
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!