基于Apollo对配置类的热更新优化

背景

关于配置的热更新,apollo 通过`com.ctrip.framework.apollo.spring.annotation.SpringValueProcessor` 处理带@Value的方法或字段,通过监听变更事件,采用反射去更新对应的值

但这个功能仅仅用于单个属性,当我有一组有关联关系的配置时,需要对每一个属性都进行@Value注解标记,这样不利于统一管理配置,比如像下面定义xxljob任务的一些属性

 @Datapublic static class FlushConfig {//sleep-interval-mills@Value("${xxl-sharding-job.common.sleep-interval-mills:-1}")private Integer sleepIntervalMills;//batch-size@Value("${xxl-sharding-job.common.batch-size:1000}")private Integer batchSize;//max-retry@Value("${xxl-sharding-job.common.max-retry:1000}")private Integer maxRetry;}

如果有新加的字段,Apollo和本地代码都需要改动,而且需要检查新加的字段是否有漏加注解的情况

另外如果对某个任务需要自定义属性,比如对某个任务自定义定义batchSize,还要在构造配置类,配置名称不同的key,不利于复用和管理

配置绑定

针对这个问题,springboot提供了@ConfigurationProperties注解把一些配置与java对象的属性进行绑定,我们可以定义这样一个配置类

@ConfigurationProperties(prefix = "xxl-sharding-job")
@Component
public class XxlShardingConfig {private FlushConfig common;private Map<String, FlushConfig> custom;@Datapublic static class FlushConfig {private Integer sleepIntervalMills;private Integer batchSize;private Integer maxRetry;}public int getBatchSize(String xxlJobName) {FlushConfig flushConfig = getFlushConfig(xxlJobName);return Optional.ofNullable(flushConfig.batchSize).orElse(common.batchSize);}private FlushConfig getFlushConfig(String xxlJobName) {if(custom==null){return common;}return custom.getOrDefault(xxlJobName, common);}public int getMaxRetry(String xxlJobName) {FlushConfig flushConfig = getFlushConfig(xxlJobName);return Optional.ofNullable(flushConfig.maxRetry).orElse(common.maxRetry);}public int getSleepIntervalMills(String xxlJobName) {FlushConfig flushConfig = getFlushConfig(xxlJobName);return Optional.ofNullable(flushConfig.sleepIntervalMills).orElse(common.sleepIntervalMills);}
}

同时配置文件定义如下属性,即可实现配置绑定到属性

xxl-sharding-job.common.sleep-interval-mills=-1
xxl-sharding-job.common.batch-size=1000
xxl-sharding-job.common.max-retry=3
#特殊任务自定义
xxl-sharding-job.custom.yourTask.max-retry=5

基于这种配置类的方式,在特殊任务自定义属性的场景,我们无需修改配置类代码,配置类能提供相关的功能方法,更内聚,但缺陷是无法实现热更新

配置类热更新Demo

查阅apollo相关代码,官网给了个demo

有如下配置类

@ConditionalOnProperty("redis.cache.enabled")
@ConfigurationProperties(prefix = "redis.cache")
@Component("sampleRedisConfig")
@RefreshScope
public class SampleRedisConfig {private static final Logger logger = LoggerFactory.getLogger(SampleRedisConfig.class);private int expireSeconds;private String clusterNodes;private int commandTimeout;private Map<String, String> someMap = Maps.newLinkedHashMap();private List<String> someList = Lists.newLinkedList();@PostConstructprivate void initialize() {logger.info("SampleRedisConfig initialized - expireSeconds: {}, clusterNodes: {}, commandTimeout: {}, someMap: {}, someList: {}",expireSeconds, clusterNodes, commandTimeout, someMap, someList);}public void setExpireSeconds(int expireSeconds) {this.expireSeconds = expireSeconds;}public void setClusterNodes(String clusterNodes) {this.clusterNodes = clusterNodes;}public void setCommandTimeout(int commandTimeout) {this.commandTimeout = commandTimeout;}public Map<String, String> getSomeMap() {return someMap;}public List<String> getSomeList() {return someList;}@Overridepublic String toString() {return String.format("[SampleRedisConfig] expireSeconds: %d, clusterNodes: %s, commandTimeout: %d, someMap: %s, someList: %s",expireSeconds, clusterNodes, commandTimeout, someMap, someList);}
}

配置刷新类

@ConditionalOnProperty("redis.cache.enabled")
@Component
public class SpringBootApolloRefreshConfig {private static final Logger logger = LoggerFactory.getLogger(SpringBootApolloRefreshConfig.class);private final SampleRedisConfig sampleRedisConfig;private final RefreshScope refreshScope;public SpringBootApolloRefreshConfig(final SampleRedisConfig sampleRedisConfig,final RefreshScope refreshScope) {this.sampleRedisConfig = sampleRedisConfig;this.refreshScope = refreshScope;}@ApolloConfigChangeListener(value = {ConfigConsts.NAMESPACE_APPLICATION, "TEST1.apollo", "application.yaml"},interestedKeyPrefixes = {"redis.cache."})public void onChange(ConfigChangeEvent changeEvent) {logger.info("before refresh {}", sampleRedisConfig.toString());refreshScope.refresh("sampleRedisConfig");logger.info("after refresh {}", sampleRedisConfig.toString());}
}

可以看到通过RefreshScope实现配置的刷新

这个类允许bean进行动态刷新(使用refresh/refreshAll方法),再下一次访问时会新建bean实例

所有的生命周期方法都会应用到这个bean上,并支持序列化,本文主要对RefresScope的逻辑进行分析

RefreshScope浅析

Scope 作用域用于管理bean的生命周期和可见范围,比如默认的常见单例,原型,session级

GenericScope是springcloud包下的扩展作用域,为 Bean 提供一个通用的作用域管理机制,支持动态注册和销毁 Bean 实例,同时提供了对作用域生命周期的细粒度控制。

RefreshScope 定义,scope定义为refresh

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Scope("refresh")
@Documented
public @interface RefreshScope {/*** @see Scope#proxyMode()*/ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;}

初始化流程

spring会扫描对应的路径,构造bean定义—org.springframework.context.annotation.ClassPathBeanDefinitionScanner#doScan

对于RefershScope标记的类,解析后ScopedProxyMode=TARGET_CLASS

org.springframework.context.annotation.AnnotationConfigUtils#applyScopedProxyMode

ScopedProxyMode scopedProxyMode = metadata.getScopedProxyMode();if (scopedProxyMode.equals(ScopedProxyMode.NO)) {return definition;}boolean proxyTargetClass = scopedProxyMode.equals(ScopedProxyMode.TARGET_CLASS);return ScopedProxyCreator.createScopedProxy(definition, registry, proxyTargetClass);

会生成两个bean定义

一个为factoryBean的bean定义,会在refersh流程中进行初始化,名称为beanName

一个为实际配置类的bean定义,名称为scopedTarget.beanName

容器级扩展接口处理

容器启动流程里会调用 org.springframework.context.support.AbstractApplicationContext#invokeBeanFactoryPostProcessors ,这是一个容器级的扩展接口

会调用org.springframework.cloud.context.scope.GenericScope 里的后置处理方法

当对应bean的scope为refresh时,会更改其targetClass为LockedScopedProxyFactoryBean,用于创建 AOP代理对象,并为代理对象添加切面——执行相关方法加读写锁,防止与scope的refresh逻辑发生冲突

CreateBean

在初始化bean时,由于 上面设置targetClass为LockedScopedProxyFactoryBean,父类逻辑(org.springframework.aop.scope.ScopedProxyFactoryBean#setBeanFactory)生成scopedTarget.beanName代理对象并进行初始化操作

生成代理对象的同时也会对 refreshScope的 cache进行缓存初始化

ContextRefreshed

RefreshScope会监听contextRefreshed事件,做一个兜底初始化的动作,保证某个scope下的 bean都已初始化,纳入管理

//org.springframework.beans.factory.support.AbstractBeanFactory#doGetBean
Object scopedInstance = scope.get(beanName, () -> {beforePrototypeCreation(beanName);try {return createBean(beanName, mbd, args);}finally {afterPrototypeCreation(beanName);}});bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);

scope get方法如下,其中org.springframework.cloud.context.scope.ScopeCache#put方法,相当于putIfAbsent方法,

BeanLifecycleWrapper value = this.cache.put(name,new BeanLifecycleWrapper(name, objectFactory));locks.putIfAbsent(name, new ReentrantReadWriteLock());try {return value.getBean();}catch (RuntimeException e) {this.errors.put(name, e);throw e;}

其中wrapper的getBean方法如下,做bean的初始化

if (this.bean == null) {synchronized (this.name) {if (this.bean == null) {this.bean = this.objectFactory.getObject();}}}return this.bean;

刷新处理

当调用org.springframework.cloud.context.scope.refresh.RefreshScope#refresh 方法时,会把该缓存清理掉

使用该对象时,由于持有的是代理对象,会走到 scope的处理逻辑,如果 scope中没有缓存该对象,同样会走一遍doGetBean的逻辑,重新加载 bean,从而拿到最新的bean

RefreshScope总结

RefreshScope的核心逻辑是通过自定义Scope去管理bean的生命周期,而factoryBean在初始化阶段生成该Scope对应的代理bean,当访问beanName对象时,走了代理逻辑,实际访问的是 beanName为scopedTarget.beanName的代理对象

在获取bean就走到scope逻辑,里面缓存了代理对象,当 refresh 时,会把代理对象删除,下次访问时会 createBean

优化使用方式

了解原理后,可以发现对每个配置类如果要实现更新,都需要写一个监听器并调用refresh方法,可以看到这个逻辑是公共的,并且@Configuration的prefix和apollo监听的前缀也是一致的,可以设计一个注解来减少这些重复工作

定义热更新注解

@ConfigurationProperties
@Component
@RefreshScope
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApolloRefreshConfig {/*** 配置类前缀* @return*/@AliasFor(annotation = ConfigurationProperties.class, value = "prefix")String prefix();/*** 默认监听的 namesapce* @return*/String[] namespaces() default {"application"};
}

对于该注解标记的类,会注册一个内置的监听器

@Configuration(proxyBeanMethods = false)
public class ApolloConfigListenerRegister implements BeanDefinitionRegistryPostProcessor, EnvironmentAware {private static Environment environment;@Overridepublic void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {for (String beanDefinitionName : registry.getBeanDefinitionNames()) {BeanDefinition beanDefinition = registry.getBeanDefinition(beanDefinitionName);if (beanDefinition instanceof AnnotatedBeanDefinition) {AnnotationMetadata metadata = ((AnnotatedBeanDefinition) beanDefinition).getMetadata();MergedAnnotation<ApolloRefreshConfig> mergedAnnotation = metadata.getAnnotations().get(ApolloRefreshConfig.class);if (mergedAnnotation.isPresent()) {AnnotationAttributes annotationAttributes = mergedAnnotation.asAnnotationAttributes();String listenName = beanDefinitionName + "-apollo-listener";RootBeanDefinition listenerBean = new RootBeanDefinition(DefaultConfigListener.class);listenerBean.setPropertyValues(new MutablePropertyValues().add("annotationAttributes", annotationAttributes).add("beanName", beanDefinitionName));registry.registerBeanDefinition(listenName, listenerBean);}}}}@Overridepublic void postProcessBeanFactory(@NotNull ConfigurableListableBeanFactory beanFactory) throws BeansException {//do nothing}@Overridepublic void setEnvironment(@NotNull Environment environment) {ApolloConfigListenerRegister.environment = environment;}@Setter@Slf4jstatic class DefaultConfigListener implements ConfigChangeListener {@Resourceprivate RefreshScope refreshScope;private String beanName;private AnnotationAttributes annotationAttributes;public void onChange(ConfigChangeEvent changeEvent) {LOGGER.info("bean:{},namespace:{} change", beanName, changeEvent.getNamespace());for (String changedKey : changeEvent.changedKeys()) {ConfigChange changedConfig = changeEvent.getChange(changedKey);LOGGER.info("key:{}, oldValue:{}, newValue:{}", changedConfig.getPropertyName(), changedConfig.getOldValue(), changedConfig.getNewValue());}refreshScope.refresh(beanName);}@PostConstructpublic void init() {String[] namespaces = annotationAttributes.getStringArray("namespaces");String[] annotatedInterestedKeyPrefixes = new String[]{annotationAttributes.getString("prefix")};Set<String> interestedKeyPrefixes = Sets.newHashSet(annotatedInterestedKeyPrefixes);for (String namespace : namespaces) {final String resolvedNamespace = environment.resolveRequiredPlaceholders(namespace);Config config = ConfigService.getConfig(resolvedNamespace);config.addChangeListener(this, null, interestedKeyPrefixes);}}}}

当我们定义配置类时候,可以用这个注解实现配置类热更新

/*** #公共设置,本地文件预设* xxl-sharding-job.common.sleep-interval-mills=-1* xxl-sharding-job.common.batch-size=1000* xxl-sharding-job.common.max-retry=3* #特殊任务自定义* xxl-sharding-job.custom.yourTask.max-retry=3*/
@Data
@Slf4j
@ApolloRefreshConfig(prefix = "xxl-sharding-job")
public class XxlShardingConfig {private FlushConfig common;private Map<String, FlushConfig> custom;@Datapublic static class FlushConfig {//sleep-interval-millsprivate Integer sleepIntervalMills;//batch-sizeprivate Integer batchSize;//max-retryprivate Integer maxRetry;}public int getBatchSize(String xxlJobName) {FlushConfig flushConfig = getFlushConfig(xxlJobName);return Optional.ofNullable(flushConfig.batchSize).orElse(common.batchSize);}private FlushConfig getFlushConfig(String xxlJobName) {if(custom==null){return common;}return custom.getOrDefault(xxlJobName, common);}public int getMaxRetry(String xxlJobName) {FlushConfig flushConfig = getFlushConfig(xxlJobName);return Optional.ofNullable(flushConfig.maxRetry).orElse(common.maxRetry);}public int getSleepIntervalMills(String xxlJobName) {FlushConfig flushConfig = getFlushConfig(xxlJobName);return Optional.ofNullable(flushConfig.sleepIntervalMills).orElse(common.sleepIntervalMills);}
}

实现效果

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/71105.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Highcharts 配置语法详解

Highcharts 配置语法详解 引言 Highcharts 是一个功能强大的图表库&#xff0c;广泛应用于数据可视化领域。本文将详细介绍 Highcharts 的配置语法&#xff0c;帮助您快速上手并制作出精美、实用的图表。 高级配置结构 Highcharts 的配置对象通常包含以下几部分&#xff1a…

【AD】3-5 元件在原理图中的基本操作2

1.导线连接 选择放置->线&#xff08;CtrlW&#xff09;&#xff0c;或者直接点击横条处&#xff0c;建议使用直角走线 注意&#xff1a;下图中的线不具有电气连接属性&#xff0c;只是单纯的线 双击导线&#xff0c;进行设置导线粗细及颜色 2.网络标签 放置->网络标…

vim:基础配置

Vim 配置清单 设置行号显示 set number 设置相对行号&#xff08;可选&#xff09; set relativenumber设置制表符为4个空格 set tabstop4 设置自动缩进 set autoindent " 启用语法高亮 syntax on" 设置背景颜色&#xff08;可选&#xff0c;根据终端或GUI Vi…

【Springboot】解决问题 o.s.web.servlet.PageNotFound : No mapping for *

使用 cursor 进行老项目更新为 springboot 的 web 项目&#xff0c;发生了奇怪的问题&#xff0c;就是 html 文件访问正常&#xff0c;但是静态文件就是 404 检查了各种配置&#xff0c;各种比较&#xff0c;各种调试&#xff0c;最后放弃时候&#xff0c;清理没用的配置文件&…

day02_Java基础

文章目录 day02_Java基础一、今日课程内容二、数组&#xff08;熟悉&#xff09;1、定义格式2、基本使用3、了解数组的内存图介绍4、数组的两个小问题5、数组的常见操作 三、方法&#xff08;熟悉&#xff09;1、定义格式2、方法重载overload 四、面向对象&#xff08;掌握&…

Linux服务器防火墙白名单访问策略的配置示例

最近在做Linux系统应用部署配置过程中&#xff0c;为了确保应用的安全&#xff0c;简单学习了解了一些Linux中的动态防火墙管理工具的使用方法。本文测试实验主要采用Linux服务器的动态防火墙管理工具(即firewalld)&#xff0c;来实现服务或端口的访问控制&#xff0c;firewall…

【UCB CS 61B SP24】Lecture 17 - Data Structures 3: B-Trees学习笔记

本文以 2-3-4 树详细讲解了 B 树的概念&#xff0c;逐步分析其操作&#xff0c;并用 Java 实现了标准的 B 树。 1. 2-3 & 2-3-4 Trees 上一节课中讲到的二叉搜索树当数据是随机顺序插入的时候能够使得树变得比较茂密&#xff0c;如下图右侧所示&#xff0c;时间复杂度也就…

【手撕算法】支持向量机(SVM)从入门到实战:数学推导与核技巧揭秘

摘要 支持向量机&#xff08;SVM&#xff09;是机器学习中的经典算法&#xff01;本文将深入解析最大间隔分类原理&#xff0c;手撕对偶问题推导过程&#xff0c;并实战实现非线性分类与图像识别。文中附《统计学习公式手册》及SVM调参指南&#xff0c;助力你掌握这一核心算法…

西门子S7-1200比较指令

西门子S7-1200 PLC比较指令学习笔记 一、比较指令的作用 核心功能&#xff1a;用于比较两个数值的大小或相等性&#xff0c;结果为布尔值&#xff08;True/False&#xff09;。典型应用&#xff1a; 触发条件控制&#xff08;如温度超过阈值启动报警&#xff09;数据筛选&…

SDF,占用场,辐射场简要笔记

符号距离函数&#xff08;Signed Distance Function&#xff0c;SDF&#xff09;的数学公式用于描述空间中任意点到某个几何形状边界的最短距离&#xff0c;并通过符号区分点在边界内外。具体定义如下&#xff1a; 假设 Ω \Omega Ω 是一个几何形状的边界&#xff0c;对于空…

solidwork智能尺寸怎么对称尺寸

以构造轴为中心线就能画智能尺寸的对称尺寸。先点击边再点击构造线

如何从零开始理解LLM训练理论?预训练范式、模型推理与扩容技巧全解析

Part 1&#xff1a;预训练——AI的九年义务教育 &#x1f4da; 想象你往峨眉山猴子面前扔了1000本《五年高考三年模拟》-我那时候还在做的题&#xff08;海量互联网数据&#xff09;&#xff0c;突然有一天它开口唱起《我在东北玩泥巴》&#xff0c;这有意思的过程就是LLM的预…

工程化与框架系列(13)--虚拟DOM实现

虚拟DOM实现 &#x1f333; 虚拟DOM&#xff08;Virtual DOM&#xff09;是现代前端框架的核心技术之一&#xff0c;它通过在内存中维护UI的虚拟表示来提高渲染性能。本文将深入探讨虚拟DOM的实现原理和关键技术。 虚拟DOM概述 &#x1f31f; &#x1f4a1; 小知识&#xff1…

设计模式--spring中用到的设计模式

一、单例模式&#xff08;Singleton Pattern&#xff09; 定义&#xff1a;确保一个类只有一个实例&#xff0c;并提供全局访问点 Spring中的应用&#xff1a;Spring默认将Bean配置为单例模式 案例&#xff1a; Component public class MySingletonBean {// Spring 默认将其…

深入浅出:Spring AI 集成 DeepSeek 构建智能应用

Spring AI 作为 Java 生态中备受瞩目的 AI 应用开发框架&#xff0c;凭借其简洁的 API 设计和强大的功能&#xff0c;为开发者提供了构建智能应用的强大工具。与此同时&#xff0c;DeepSeek 作为领先的 AI 模型服务提供商&#xff0c;在自然语言处理、计算机视觉等领域展现了卓…

CSS浮动详解

1. 浮动的简介 浮动是用来实现文字环绕图片效果的 2.元素浮动后会有哪些影响 对兄弟元素的影响&#xff1a; 后面的兄弟元素&#xff0c;会占据浮动元素之前的位置&#xff0c;在浮动元素的下面&#xff1b;对前面的兄弟 无影响。 对父元素的影响&#xff1a; 不能撑起父元…

python数据类型等基础语法

目录 字面量 注释 变量 查数据类型 类型转换 算数运算符 字符串定义的三种方式 字符串占位 数据输入 字面量 被写在代码中固定的值 六种数据类型: 1 字符串 String 如"egg" 2 数字 Number: 整数int 浮点数float 复数complex :如43j 布尔…

Android 图片压缩详解

在 Android 开发中,图片压缩是一个重要的优化手段,旨在提升用户体验、减少网络传输量以及降低存储空间占用。以下是几种主流的图片压缩方法,结合原理、使用场景和优缺点进行详细解析。 效果演示 直接先给大家对比几种图片压缩的效果 质量压缩 质量压缩:根据传递进去的质…

Flutter状态管理框架GetX最新版详解与实践指南

一、GetX框架概述 GetX是Flutter生态中轻量级、高性能的全能开发框架&#xff0c;集成了状态管理、路由导航、依赖注入等核心功能&#xff0c;同时提供国际化、主题切换等实用工具。其优势在于代码简洁性&#xff08;减少模板代码约70%&#xff09;和高性能&#xff08;基于观…

【linux】详谈 环境变量

目录 一、基本概念 二、常见的环境变量 取消环境变量 三、获取环境变量 通过代码获取环境变量 环境变量的特性 1. getenv函数:获取指定的环境变量 2. environ获取环境变量 四、本地变量 五、定义环境变量的方法 临时定义&#xff08;仅对当前会话有效&#xff09; 永…