2025-05-17
实战设计
0

目录

Nacos 配置中 Bean 的定义
动态的注册Bean对象
Nacos配置修改Bean信息
Nacos 配置的变更
Bean 的增删改
门面调用
测试示例
插件设计的场景

对于 Nacos 的动态配置大家都很熟悉,在此我提出个疑问❓ 如何在 Nacos 配置中定义 Bean 信息且实现动态的添加删除 Bean 呢? 本文是结合工作中的实践,来讲下在 动态的可插拔式插件设计 的实现思路,可结合业务场景动态切换告警信息、短信服务商、动态线程池等等

Nacos 配置中 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; }

声明学生所做的事情,本质上来说,这就是我们真实要处理业务场景的逻辑,可自行调整

java
public 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

yaml
class: className: 十四班 gradeName: 初一 students: 2025140001: name: 张三 age: 18 sex: 2025140002: name: 紫馨 age: 20 sex: 2025140003: name: 狗五松溪 age: 13 sex: 2025140004: name: 构思 age: 10 sex: 未知

动态的注册Bean对象

定义好属性后,我们需要将配置的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 环境属性中转换对象的工具类

java
public 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; } }

Nacos配置修改Bean信息

实现了项目启动时,配置中 Bean 对象的初始化以后,我们接下来实现对配置中 Bean 对象的 增删改,来保证Spring容器中构建的对象为最新调整的属性

Nacos 配置的变更

首先,当Nacos 平台上对配置进行变更时,我们需要做出扩展,触发Bean的动态刷新操作。从源码中可以得知,当配置变更时,客户端的 RefreshEventListener 监听器,会监听到 RefreshEvent 事件,执行 ContextRefresher #refresh() 方法,此方法中调用了内部方法 refreshEnvironment(),而这个内部方法将会变更本地的配置,同时将变更的属性提取出来,并且发布了 EnvironmentChangeEvent 事件,那我们只需要监听这个事件即可

image.png

上述逻辑中提取的变更属性有3种操作情况,分别是

  1. 修改属性:会记录到变更的属性中,修改哪个属性,则记录哪个,例如修改了某个学生的年龄,则会存在一条变更的属性 class.students[2025140001].age
  2. 删除属性:同修改属性一样,删除了哪些属性,也会记录哪些属性是变更的属性,例如删除了某个key以及对应的学生,则会将学生的对象都会记录上 class.students[2025140001].nameclass.students[2025140001].ageclass.students[2025140001].sex
  3. 新增属性,同修改删除一样,但凡新增的都会记录到变更的属性中

Bean 的增删改

监听到Nacos配置的变更后,我们需要从变更的属性中,只需要找出 ClassProperties #students 的属性变更,其中配置变更的操作也是3种,分别对应上述的配置变更的3种情况

  1. 修改属性:确认对象是否在Spring容器中存在,存在则可能为更新或删除属性,接下来确认变更的属性是否还在 Environment 中,如果还在,则为更新
  2. 删除属性:如果变更的属性不在Environment 中,则视为删除属性
  3. 新增属性:Spring容器中不存在此变更的属性对象,则为新增属性

结合上述思路,代码实现如下

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)来执行一些事情的方法

java
public 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 接口查看,在更新配置后,刷新接口看内容是否发生变更

image.png

访问 /getStudent 接口,可让不同学生执行某些事情

image.png

插件设计的场景

在工作中,我是以此思路实现了 告警组件,结合el表达式,可动态调整告警信息。通过某次技术分享后,同事借鉴此思路实现了动态切换短信服务商,可结合实际的业务场景,自行实现动态的插件

本文作者:柳始恭

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!