赞
踩
众所周知由Spring提供的@ConfigurationProperties
注解属性,是一种简单易用且方便维护的自定义配置读取映射方式。
然而使用了Nacos作为配置中心的项目,如果也想这么方便的读取到自定义的配置文件并做映射,就没有那么容易了,可能会遇到如下问题:
@ConfigurationProperties
那样方便、生效简单、轻量级,还需要依赖nacos-spring
,而nacos-spring
这个包的最后一次更新也已经比较早了,依赖的spring版本比较陈旧。于是就想自己利用现有依赖和API,编写一个简单易用的版本,在项目里作为基础组件提供。不需要再有新的依赖,就可以基本接近@ConfigurationProperties
的使用体验。
以下为自定义BeanPostProcessor:EasyNacosConfigurationPropertiesProcessor
,及自定义注解:EasyNacosConfigurationProperties
的完整代码:
package com.kamjin.common.nacos import com.alibaba.cloud.nacos.* import com.alibaba.nacos.api.config.listener.* import com.kamjin.common.ext.* import com.kamjin.common.utils.* import org.springframework.beans.factory.config.* import org.yaml.snakeyaml.* /** * @author kamjin * @date 2023年6月7日 * @description * <p> * 简易版Nacos的配置映射处理器 * 通过注解 [EasyNacosConfigurationProperties] 标注在config类上,以将属性从 Nacos 端的配置文件读取,并注入到目标类中 * 使用当前功能需要将当前类注入到 Spring IOC 中 * </p> */ class EasyNacosConfigurationPropertiesProcessor(private val nacosConfigManager: NacosConfigManager) : BeanPostProcessor { private val logger = getLogger<EasyNacosConfigurationPropertiesProcessor>() override fun postProcessAfterInitialization(bean: Any, beanName: String): Any { if (bean.javaClass.isAnnotationPresent(EasyNacosConfigurationProperties::class.java)) { // 解析注解属性 val easyNacosConfigurationProperties = bean.javaClass.getAnnotation(EasyNacosConfigurationProperties::class.java) val dataId = easyNacosConfigurationProperties.dataId val group = easyNacosConfigurationProperties.group.takeIf { it.isNotBlank() } ?: nacosConfigManager.nacosConfigProperties.group logger.info("【Nacos配置映射-初始化】bean: $beanName 配置dataId: $dataId 前缀: ${easyNacosConfigurationProperties.prefix} 开始>>>>>>>>>>>>>>>>>>>>") // 从Nacos获取配置内容 val configInfo = nacosConfigManager.configService.getConfig(dataId, group, easyNacosConfigurationProperties.timeoutMs) // 刷新配置到configBean refreshConfiguration(configInfo, easyNacosConfigurationProperties.prefix, bean) // 如果开启了自动刷新,则注册一个配置刷新监听器 if (easyNacosConfigurationProperties.autoRefresh) { nacosConfigManager.configService.addListener(dataId, group, object : Listener { override fun getExecutor() = null override fun receiveConfigInfo(configInfo: String?) { configInfo ?: return logger.info("【Nacos配置映射-配置监听】bean: $beanName dataId:$dataId 配置变更 开始>>>>>>>>>>>>>>>>>>>>>>>>>>") refreshConfiguration(configInfo, easyNacosConfigurationProperties.prefix, bean) logger.info("【Nacos配置映射-配置监听】bean: $beanName 映射完成<<<<<<<<<<<<<<<<<<<<<<<<<<") if (logger.isDebugEnabled) { logger.debug( "【Nacos配置映射-配置监听】bean: $beanName 完成 configInfo:${configInfo} bean:${ JSONUtil.toJSONString( bean ) }" ) } } }) logger.info("【Nacos配置映射-初始化】bean: $beanName 注册监听器完成") } logger.info("【Nacos配置映射-初始化】bean: $beanName 映射完成<<<<<<<<<<<<<<<<<<<<<<<<") if (logger.isDebugEnabled) { logger.debug( "【Nacos配置映射-初始化】bean: $beanName 完成 configInfo:${configInfo} bean:${ JSONUtil.toJSONString( bean ) }" ) } } return bean } /** * 刷新配置文件内容到配置类对象 * * @param configStr 配置文件内容 * @param prefix 配置所在前缀 * @param configurationBean 配置类对象 * @return */ private fun refreshConfiguration(configStr: String, prefix: String, configurationBean: Any) { /** * Public YAML interface. This class is not thread-safe. Which means that all the methods of the * same instance can be called only by one thread. It is better to create an instance for every YAML * stream. */ // 以上注释引自Yaml类注释,yaml对象不是线程安全的,api作者建议每次使用时都单独创建该对象 val yamlConfigMap = Yaml().load<Map<String, Any>>(configStr) var beanConfigMap = yamlConfigMap.toCamelCaseKeys() if (prefix.isNotBlank()) { val prefixKeys = prefix.split(".") prefixKeys.forEach { try { val c = beanConfigMap[it.toCamelCase()] ?: throw RuntimeException("配置:${prefix} 在配置文件中不存在") @Suppress("UNCHECKED_CAST") beanConfigMap = c as Map<String, Any> } catch (e: ClassCastException) { logger.error("类型转换失败", e) throw RuntimeException("配置:$prefix 无效,对应配置无法转换到配置类") } } } val convertedConfig = JSONUtil.convertValue(beanConfigMap, configurationBean::class) BeanMapper.copy(convertedConfig, configurationBean) } } /** * * 将 map 中所有的 key 的中划线转为驼峰 * @return this */ private fun Map<String, Any>.toCamelCaseKeys(): Map<String, Any> { val result = mutableMapOf<String, Any>() for ((key, value) in this) { val newKey = key.toCamelCase() when (value) { is Map<*, *> -> { @Suppress("UNCHECKED_CAST") val nestedMap = value as Map<String, Any> result[newKey] = nestedMap.toCamelCaseKeys() } else -> result[newKey] = value } } return result } /** * 将字符串中的中划线转为驼峰 * @return this */ private fun String.toCamelCase(): String { val words = this.split("-") val result = StringBuilder(words[0]) for (i in 1 until words.size) { result.append(words[i].replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() }) } return result.toString() } /** * @author kamjin * @date 2023年6月7日 * @description * <p> * 简易版Nacos的配置类处理器 * 注解需要映射nacos配置的config类 * </p> */ @Retention(AnnotationRetention.RUNTIME) @Target(AnnotationTarget.CLASS) annotation class EasyNacosConfigurationProperties( /** * 对应Nacos 配置中心配置文件的 dataId */ val dataId: String, /** * 对应Nacos 配置中心配置文件的group */ val group: String = "", /** * 配置前缀,规则参考 [@ConfigurationProperties] */ val prefix: String = "", /** * 读取 Nacos 配置的超时时间 */ val timeoutMs: Long = 5000, /** * 是否需要自动刷新配置,为 true 会注册一个配置监听器来监听 Nacos 上配置的变化,以刷新到配置类 */ val autoRefresh: Boolean = false )
注:
代码中的JSONUtil(对json字符串的操作工具)、BeanMapper(对bean的操作工具)、getLogger(logger的获取封装)都是自行封装的,可以替换为你项目中的实际封装。
1.在任意配置类中注册EasyNacosConfigurationPropertiesProcessor
到springIOC
@Bean
fun easyNacosConfigProcessor(nacosConfigManager: NacosConfigManager): EasyNacosConfigurationPropertiesProcessor {
return EasyNacosConfigurationPropertiesProcessor(nacosConfigManager)
}
2.在目标类注解 @EasyNacosConfigurationProperties
,并配置相关注解属性
@EasyNacosConfigurationProperties(dataId = "app-config.yaml", group = "example-group", autoRefresh = true) @Component class AppConfig { // 配置属性 var appName: String? = null var rules: Map<String,RuleProperties> = = mutableMapOf() var appVersion: String? = null class RuleProperties { var id:Long? = null var name:String? = null } // ... }
接下来启动项目时,就可以依靠日志看到配置类加载和映射情况。
多配置文件读取:当我们需要自定义多个配置文件时,只需要指定datgId,就可以将配置映射到具体的config bean上,在其他地方注入使用,该实现可以提供更灵活的配置管理方式。省去了多个地方写重复的配置内容读取和映射的问题。额外的配置文件多的情况下很有用。
动态配置更新:在需要实时更新配置的场景下,该实现的自动刷新功能非常有用。无需手动重启应用,即可实现配置的实时更新,提高应用的灵活性和可维护性。这与@RefreshScope实现的效果是一致的。
本文使用较少的代码和依赖实现了nacos上自定义配置到项目中config bean自动映射和自动刷新配置监听功能。
解决了一些使用Nacos作为配置中心时,在读取自定义配置文件的不便之处,可以在实际项目应用中得到良好的效果。
希望本文对各位看官能有所帮助: ]
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。