Nacos作为一款优秀的开源注册中心框架,同时提供了强大的配置管理功能。然而,在使用原生的Nacos注解读取配置,并映射到Bean时,我们可能会遇到一些问题:
@ConfigurationProperties
那样方便、生效简单、轻量级,还需要依赖nacos-spring
,而nacos-spring
这个包的最后一次更新也已经比较早了,依赖的spring版本比较老。于是就想自己利用现有依赖,编写一个简单易用的版本,在项目里作为基础组件提供,提供给项目其他成员使用。
本文介绍了一种基于自定义注解加Spring的BeanPostProcessor实现的Nacos配置自动读取和映射到配置类的处理器,它能够轻松地将属性从Nacos配置文件注入到目标类中,解决了一些原生注解的限制和不便,提供了更便捷、高效的配置读取和映射方式。
以下为自定义注解及自定义BeanPostProcessor的完整代码:
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.*
import java.util.concurrent.*
/**
* @author kamjin
* @date 2023年6月7日
* @description
*
* 简易版Nacos的配置类处理器
* 通过注解 [EasyNacosConfigurationProperties] 标注在类上,以将属性从 Nacos 端的配置文件读取,并注入到目标类中
* 使用当前功能需要将当前类注入到 Spring IOC 中
*
*/
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
//从nacos获取配置内容
val configInfo =
nacosConfigManager.configService.getConfig(dataId, group, easyNacosConfigurationProperties.timeoutMs)
//刷新配置到bean对象
refreshConfiguration(configInfo, easyNacosConfigurationProperties.prefix, bean)
//如果开启了自动刷新,则注册一个配置监听器
if (easyNacosConfigurationProperties.autoRefresh) {
nacosConfigManager.configService.addListener(dataId, group, object : Listener {
override fun getExecutor(): Executor? {
return null
}
override fun receiveConfigInfo(configInfo: String?) {
configInfo ?: return
refreshConfiguration(configInfo, easyNacosConfigurationProperties.prefix, bean)
}
})
}
}
return bean
}
/**
* 刷新配置文件内容到配置类对象
*
* @param configStr 配置文件内容
* @param prefix 配置所在前缀
* @param configurationBean 配置类对象
* @return
*/
private fun refreshConfiguration(configStr: String, prefix: String, configurationBean: Any) {
val yamlConfigMap = Yaml().load<Map<String, Any>>(configStr)
var beanConfigMap = yamlConfigMap.toCamelCaseKeys()
if (prefix.isNotBlank()) {
val prefixKeys = prefix.split(".")
prefixKeys.forEach {
try {
beanConfigMap = beanConfigMap[it.toCamelCase()] as Map<String, Any>
} catch (e: ClassCastException) {
logger.error("配置的 prefix:$prefix 无效,对应配置无法转换到配置类", e)
}
}
}
val convertedConfig = JSONUtil.convertValue(beanConfigMap, configurationBean::class)
BeanMapper.copy(convertedConfig, configurationBean)
}
/**
* 将 map 中所有的 key 的中划线转为驼峰
* @return this
*/
private fun Map<String, Any>.toCamelCaseKeys(): Map<String, Any> {
return mapValues { (key, value) ->
if (value is Map<*, *>) {
(value as Map<String, Any>).toCamelCaseKeys()
} else {
key.toCamelCase()
}
}
}
/**
* 将字符串中的中划线转为驼峰
* @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()
}
}
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
annotation class EasyNacosConfigurationProperties(
/**
* Nacos 配置中心配置文件的 dataId
*/
val dataId: String,
/**
* Nacos 配置分组
*/
val group: String = "",
/**
* 配置前缀,参考 [@ConfigurationProperties]
*/
val prefix: String = "",
/**
* 读取 Nacos 配置的超时时间
*/
val timeoutMs: Long = 5000,
/**
* 是否需要自动刷新配置,为 true 会注册一个配置监听器来监听 Nacos 上配置的变化,以刷新到配置类
*/
val autoRefresh: Boolean = false
)
注:
代码中的JSONUtil(对json字符串的操作工具)、BeanMapper(对bean的操作工具)、getLogger(log的获取封装)都是自行封装的,可以替换为你项目中的实际封装。
描述:
1.需要将EasyNacosConfigurationPropertiesProcessor
类实例注册到Spring IOC容器中。这样的话,在Spring IOC初始化完成后会调用该处理器处理每个bean。
2.在目标配置类上使用@EasyNacosConfigurationProperties
注解进行标注,指定相关的配置信息,如dataId、group等(用法类似@ConfigurationProperties和@NacosConfigurationProperties)。
这样,当Spring IOC容器初始化时,自定义的BeanPostProcessor会处理每个标注了EasyNacosConfigurationProperties注解的类,并将属性从Nacos配置中心的配置文件中读取,注入到目标类中。实现自动属性配置。
参考代码示例(java也可以使用,代码改成java版即可):
配置方式与@ConfigurationProperties类似,默认不配置prefix,会读取整个配置文件内容并映射到该对象。
// 配置类示例代码
@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
}
// ...
}
// 在Spring IOC容器中注入EasyNacosConfigurationPropertiesProcessor
@Bean
fun easyNacosConfigProcessor(nacosConfigManager: NacosConfigManager): EasyNacosConfigurationPropertiesProcessor {
return EasyNacosConfigurationPropertiesProcessor(nacosConfigManager)
}
简单易用:通过使用自定义注解,我们可以轻松地将属性从Nacos配置文件中注入到目标类中,而无需繁琐的配置和额外的代码。在使用了nacos的项目中,无需再有额外的如nacos-spring这样的依赖引入。
提供了自动刷新功能:通过nacosConfigManager这个api实现了配置文件的监听和自动刷新,即当Nacos上的配置文件发生变化时,listener接收到最新的配置,会自动将最新的配置信息注入到目标类中,无需手动重启应用或手动刷新配置。
支持配置前缀:通过指定配置前缀,我们可以灵活地控制属性的注入范围,只将符合指定前缀的配置注入到目标类中,避免不必要的属性注入和冲突。支持与spring定义的配置一样的处理方式,会自动映射中线为驼峰,并映射到类属性上。
简易版Nacos配置处理器适用于各种实际应用场景,特别是在以下情况下可以发挥优势:
多配置文件管理:当我们需要自定义多个配置文件时,只需要指定datgId,就可以将配置映射到项目中的某个pojo类上,在其他地方注入使用,该实现可以提供更灵活的配置管理方式。省去了多个地方写重复的配置内容读取和映射的问题。额外的配置文件多的情况下很有用。
动态配置更新:在需要实时更新配置的场景下,该实现的自动刷新功能非常有用。无需手动重启应用,即可实现配置的实时更新,提高应用的灵活性和可维护性。这与@RefreshScope实现的效果是一致的。
尽管该实现已经提供了一种方便和灵活的配置管理方式,但我们仍可以进一步改进和扩展功能。以下是一些可能的改进和扩展方向:
支持更多的配置源:除了Nacos配置中心,我们可以考虑支持其他配置源,如Spring Cloud Config、Apollo等,以满足不同项目的需求。
支持属性校验和类型转换:在属性注入过程中,我们可以添加属性校验和类型转换的功能,确保注入的属性满足一定的规则和要求。
支持不同的配置文件内容类型:例如JSON、Properties、甚至自定义配置类型的文件的配置、读取和映射到bean。只需要在注解中,添加自定义的配置解析器就可以解决。
提供更方便的启用和使用方式:要让现在的EasyNacosConfigurationProperties
注解生效,启用需要注册这个EasyNacosConfigurationPropertiesProcessor
,可以考虑做成SpringBootStater依赖的方式省去这个步骤。
该实现是一个方便、灵活且易于使用的配置映射实现,通过采用自定义注解和Spring的BeanPostProcessor接口,提供了更灵活的配置读取和映射方式,并支持自动刷新配置。
它解决了原生Nacos注解实际使用时的不生效,部分配置不映射的问题,以及某些依赖太旧的问题,可以在实际项目应用中得到良好的效果。
希望本文能够帮助读者更好地理解和应用自定义注解以及spring的BeanPostProcessor用途,并在实际项目中发挥其作用。