当前位置:   article > 正文

【NACOS自定义配置读取和映射】一个简易的Nacos配置读取和映射处理器,自定义注解+BeanPostProcessor,简单方便高效的属性注入解决方法_nacos自定义配置文件读取

nacos自定义配置文件读取

前言

众所周知由Spring提供的@ConfigurationProperties注解属性,是一种简单易用且方便维护的自定义配置读取映射方式。
然而使用了Nacos作为配置中心的项目,如果也想这么方便的读取到自定义的配置文件并做映射,就没有那么容易了,可能会遇到如下问题:

  • 部分API不会直接生效
    或者说需要做一些额外的操作,具体就是直接使用了@NacosConfigurationProperties后也没效果,使用了@NacosPropertySource标注了也没有效果,从源码里找处理的部分,在nacos-client包里没有的。感觉这块的api设计上本身就缺乏一些考究,既然加上没效果就放在有效果的包里好了,放在nacos-client包里面,它只是个摆设。
  • 不够方便轻量:
    不能像使用spring的@ConfigurationProperties那样方便、生效简单、轻量级,还需要依赖nacos-spring,而nacos-spring这个包的最后一次更新也已经比较早了,依赖的spring版本比较陈旧。
  • 据说有api缺陷(未能实际印证):
    好像使用中还有一些限制,比如set和map不支持直接映射(看到网上有人说这个,具体没试,因为配置了@NacosConfigProperties等一系列注解后均不生效,折腾了半天也没法用,后面就没有再尝试了)

于是就想自己利用现有依赖和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
)


  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196

注:
代码中的JSONUtil(对json字符串的操作工具)、BeanMapper(对bean的操作工具)、getLogger(logger的获取封装)都是自行封装的,可以替换为你项目中的实际封装。

二、使用方式

1.在任意配置类中注册EasyNacosConfigurationPropertiesProcessorspringIOC

@Bean
fun easyNacosConfigProcessor(nacosConfigManager: NacosConfigManager): EasyNacosConfigurationPropertiesProcessor {
    return EasyNacosConfigurationPropertiesProcessor(nacosConfigManager)
}
  • 1
  • 2
  • 3
  • 4

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
	}
    // ...
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

接下来启动项目时,就可以依靠日志看到配置类加载和映射情况。

三、实际应用场景

  • 多配置文件读取:当我们需要自定义多个配置文件时,只需要指定datgId,就可以将配置映射到具体的config bean上,在其他地方注入使用,该实现可以提供更灵活的配置管理方式。省去了多个地方写重复的配置内容读取和映射的问题。额外的配置文件多的情况下很有用。

  • 动态配置更新:在需要实时更新配置的场景下,该实现的自动刷新功能非常有用。无需手动重启应用,即可实现配置的实时更新,提高应用的灵活性和可维护性。这与@RefreshScope实现的效果是一致的。

总结

本文使用较少的代码和依赖实现了nacos上自定义配置到项目中config bean自动映射和自动刷新配置监听功能。
解决了一些使用Nacos作为配置中心时,在读取自定义配置文件的不便之处,可以在实际项目应用中得到良好的效果。
希望本文对各位看官能有所帮助: ]

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/菜鸟追梦旅行/article/detail/234217
推荐阅读
相关标签
  

闽ICP备14008679号