0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

配置中心的核心功能和实现

jf_ro2CN3Fa 来源:三友的java日记 2023-09-22 11:12 次阅读

来源:三友的java日记

最近突然心血来潮(就是闲的)就想着撸一个简单的配置中心,顺便也照葫芦画瓢给整合到SpringCloud。

本文大纲

362ca4a4-58e8-11ee-939d-92fbcf53809c.png

配置中心的概述

随着历史的车轮不断的前进,技术不断的进步,单体架构的系统都逐渐转向微服务架构。虽然微服务架构有诸多优点,但是随着越来越多的服务实例的数量,配置的不断增多,传统的配置文件方式不能再继续适用业务的发展,所以急需一种可以统一管理配置文件应用,在此之下配置中心就诞生了。

所以配置中心就是用来统一管理各种服务配置的一个组件,本质上就是一个web应用。

配置中心的核心功能

一个配置中心的核心功能其实主要包括两个:

配置的存取

配置变更的通知

配置的存取是配置中心不可缺失的功能,配置中心需要能够将配置进行保存,存在磁盘文件也好,又或是数据库也罢,总之需要持久化,同时配置中心也得提供配置查询的功能。

配置变化的通知也是一个很重要的功能,一旦配置中心的配置有变动的话,那么使用到这个配置的客户端就需要知道这个配置有变动,从而可以做到相应的变动的操作。

手撸一个简易的配置中心

上文分析了一个配置中心的核心功能,接下来就实现这两个核心的功能。

一、文件工程整体分析

36442250-58e8-11ee-939d-92fbcf53809c.png

文件工程整体分为客户端与服务端

服务端:单独部署的一个web应用,端口是8888,提供了对于配置增删改查的http接口

客户端(SDK):业务系统需要引用对应的依赖,封装了跟服务端交互的代码

二、服务端实现详解

1、配置文件的数据存储模型ConfigFile

在配置中心存储配置的时候,需要指明以下信息

publicclassConfigFile{

privateStringfileId;

privateStringname;

privateStringextension;

privateStringcontent;

privateLonglastUpdateTimestamp;

}

fileId: 文件的唯一id,由配置中心服务端在新增配置文件存储的时候自动生成,全局唯一

name: 就是文件的名字,没有什么要求,见名知意就行

extension: 文件后缀名,指的是该配置是什么类型的文件,比如是properties、yml等

content: 就是配置文件的内容,不同的后缀名有不同的格式要求

lastUpdateTimestamp: 上一次文件更新的时间戳。当文件存储或者更新的时候,需要更新时间戳,这个字段是用来判断文件是否有改动

2、文件存储层ConfigFileStorage

对于文件存储层,我提供了一个ConfigFileStorage接口,

publicinterfaceConfigFileStorage{

voidsave(ConfigFileconfigFile);

voidupdate(ConfigFileconfigFile);

voiddelete(StringfileId);

ConfigFileselectByFileId(StringfileId);

ListselectAll();

}

这个接口提供了对于配置存储的crud操作,目前我已经实现了基于内存和磁盘文件的存储的代码

3663bdb8-58e8-11ee-939d-92fbcf53809c.png

可以在项目启动的时候,在配置文件指定是基于磁盘文件存储还是基于内存存储,默认是基于磁盘文件存储。

368c86b2-58e8-11ee-939d-92fbcf53809c.png

当然,如果想把配置信息存储到数据库,只要新增一个存储到数据的实现就行。

3、ConfigController

ConfigController提供了对于配置文件的crud的http接口

36a69d7c-58e8-11ee-939d-92fbcf53809c.png

ConfigController是通过调用ConfigManager来完成配置文件的crud

4、ConfigManager

36d485d4-58e8-11ee-939d-92fbcf53809c.png

其实就是一个service层,就是简单的参数封装,最终是调用ConfigFileStorage存储层的实现来完成配置的存储功能。

这样配置中心的配的存取的功能就实现了。

所以,服务端还是比较简单的。其实就是跟平时写的业务系统的crud没什么区别,就是将数据库存储替换成了磁盘文件的存储。

至于前面说的配置文件变更通知的功能,我是基于客户端来实现的。

三、客户端的实现

客户端工程代码如下

36e64dfa-58e8-11ee-939d-92fbcf53809c.png

1、ConfigFileChangedListener

36fc99fc-58e8-11ee-939d-92fbcf53809c.pngConfigFileChangedListener

配置变动的监听器,当客户端对某个配置监听的时候,如果这个配置的内容有变化的话,客户端就会回调这个监听器,传入最新的配置

2、ConfigService

3726ed6a-58e8-11ee-939d-92fbcf53809c.png

封装了客户端的核心功能,可以添加对某个文件的监听器和获取某个文件的配置内容。

使用示例:

//创建一个ConfigService,传入配置中心服务端的地址
ConfigServiceconfigService=newConfigService("localhost:8888");

//从服务端获取配置文件的内容,文件的id是新增配置文件时候自动生成
ConfigFileconfig=configService.getConfig("69af6110-31e4-4cb4-8c03-8687cf012b77");

//对某个配置文件进行监听
configService.addListener("69af6110-31e4-4cb4-8c03-8687cf012b77",newConfigFileChangedListener(){
@Override
publicvoidonFileChanged(ConfigFileconfigFile){
System.out.printf("fileId=%s配置文件有变动,最新内容为:%s%n",configFile.getFileId(),configFile.getContent());
}
});

这里说一下配置变更通知的实现原理。

首先对于客户端来说,要想知道哪个配置文件进行了改动,有两种方式

第一种是通过push的方式来实现。当配置文件发生变动的时候,服务端主动将变动的配置文件push给客户端。这种方式实现起来比较麻烦,一方面是服务端还得存储客户端的服务的信息,因为服务端得知道push到哪台服务器上;另一方面,客户端需要提供一个接口来接收服务端push的请求,所以这种方式整体实现起来比较麻烦。但是这种push方式时实性比较好,一旦配置文件有变动,第一时间客户端就能够知道配置有变动。

第二种方式就是基于pull模式来实现。客户端定时主动去服务端拉取配置文件,判断文件内容是否有变动,一旦有变动就进行监听器的回调。这种实现相比push来说简单不少,因为服务端不需要关心客户端的信息,所有的操作都由客户端来完成。但是这个定时的时间间隔不好控制,太长可能会导致时实性差,太短会导致可能无效请求过多,因为配置压根可能没有变化。

但是这里我选择了第二种方式,因为实现起来简单。。

374440ea-58e8-11ee-939d-92fbcf53809c.png变动通知代码实现

到这,一个简单的配置中心的服务端的和客户端就完成了,这里画张图来总结一下配置中心的核心原理。

376649d8-58e8-11ee-939d-92fbcf53809c.png

接下来就把这个简易的配置中心整合到SpringCloud中。

SpringCloud配置中心的原理

1、项目启动是如何从配置中心加载数据的?

在SpringCloud环境下,当项目启动的时候,在SpringBoot应用容器创建之前,会先创建一个容器,这个容器非常重要,这个容器是用来跟配置中心交互,拉取配置的。

这个容器在启动的时候会干两件事:

加载bootstrap配置文件,这就是为什么配置中心的配置信息需要写在bootstrap配置文件的重要原因

加载所有spring.factories文件中的键为org.springframework.cloud.bootstrap.BootstrapConfiguration对应的配置类,将这些配置类注入到这个容器中,注意这里是不会加载@EnbaleAutoConfiguration自动装配的类

当这两件事都做好之后,会从这个容器中获取到所有的PropertySourceLocator这个接口的实现类对象,依次调用locate方法。

37847930-58e8-11ee-939d-92fbcf53809c.pngPropertySourceLocator

这个类很重要,先来看看注释

Strategy for locating (possibly remote) property sources for the Environment. Implementations should not fail unless they intend to prevent the application from starting.

扔到有道翻译如下:

为环境定位(可能是远程)属性源的策略。实现不应该失败,除非它们打算阻止应用程序启动。

说的简单点就是用来定位到(也就是获取的意思)项目启动所需要的属性信息。同时要注意到括号内的 可能是远程 告诉我们一个很重要的信息,那就是获取的配置信息不仅仅可以存在本地,而且还可以存在远程。

远程?作者这里就差直接告诉你可以从配置中心获取了。。

所以从这个注释就可以发现,原来PropertySourceLocator就是起到在SpringCloud环境下从配置中心获取配置的作用。

PropertySourceLocator是一个接口,所以只要不同的配置中心实现这个接口,那么不同的配置中心就可以整合到了SpringCloud,从而实现从配置中心加载配置属性到Spring环境中了。

2、如何实现注入到Bean中的属性动态刷新?

上面讲了在项目启动的时候SpringCloud是如何从配置中心加载数据的,主要是通过新建一个容器,加载bootstrap配置文件和一些配置类,最后会调用PropertySourceLocator来从配置中心获取到配置信息。

那么在SpringCloud环境下,是如何实现注入到Bean中的属性动态刷新的呢?

举个例子

37ac721e-58e8-11ee-939d-92fbcf53809c.pngUserService

当在类上加一个@RefreshScope注解之后,那么当配置中心sanyou.username的属性有变化的时候,那么此时注入的username也会跟着变化。

这种变化是如何实现的呢?

SpringCloud中规定,当配置中心客户端一旦感知到服务端的某个配置有变化的时候,需要发布一个RefreshEvent事件来告诉SpringCloud配置有变动。

37ca3c2c-58e8-11ee-939d-92fbcf53809c.png

在SpringCloud中RefreshEventListener类会去监听这个事件,一旦监听到这个事件,就会进行两步操作来刷新注入到对象的属性。

37e33b32-58e8-11ee-939d-92fbcf53809c.pngRefreshEventListener

从配置中心再次拉取属性值,而这个拉取的代码逻辑跟项目启动时拉取的属性值核心逻辑几乎是一样的,也是创建一个新的spring容器,加载配置文件和配置类,最后通过PropertySourceLocator获取属性,这一部分核心的代码逻辑是复用的。

有了最新的属性之后,就开始刷新对象的属性。

刷新的逻辑实现的非常的巧妙,可不是你以为的简单地将新的属性重新注入对象中,而是通过动态代理的方式来实现的。

对于在类上加了@RefreshScope注解的Bean,Spring在生成这个Bean的时候,会进行动态代理。

这里我们就上面举个UserService例子来分析,在生成UserService有两步操作

生成一个UserService对象,将从配置中心拉到的配置sanyou.username注入给UserService对象

由于加了@RefreshScope,会给上一步骤生成的UserService对象进行代理,生成一个代理对象

最后真正暴露出去供我们使用的其实是就是这个代理对象,如图所示

37fbe376-58e8-11ee-939d-92fbcf53809c.png

由于暴露出去的是一个代理对象,所以当调用getUsername方法的时候,其实是调用UserService的代理对象的getUsername方法,从而就会找到UserService,调用UserService的getUsername获取到username的属性值。

当配置中心的配置有变动刷新属性的时候,Spring会把UserService这个对象(非代理对象)给销毁,重新创建一个UserService对象,注入最新的属性值。

当再次通过UserService代理对象获取username属性的时候,就会找最新创建的那个UserService对象,此时就能获取到最新的属性值。

38147922-58e8-11ee-939d-92fbcf53809c.png

配置每刷新一次,UserService对象就会先销毁再重新创建,但是暴露出去的UserService代理对象一直不会变。

这样,对于使用者来说,好像是UserService对象的属性自动刷新了,其实本质上是UserService代理对象最终找的UserService对象发生了变化。

到这应该就知道为什么加了@RefreshScope的对象能够实现配置的自动刷新了,其实依靠的是动态代理完成的。

3、源码执行流程图

由于上面并没有涉及整体执行流程的源码分析,所以我特地结合源码画了两张源码的执行流程图,有兴趣的小伙伴可以对照着图翻一翻具体的源码。

3.1启动时加载配置流程

38366726-58e8-11ee-939d-92fbcf53809c.png

最终从配置中心获取到的属性会放在项目启动时创建的 Environment 对象里面。

3.2配置刷新源码流程

3856f6e4-58e8-11ee-939d-92fbcf53809c.png

这个图新增了对于加了@ConfigurationProperties数据绑定的对象原理的分析。

整合SpringCloud和测试

一、整合SpringCloud

1、ConfigCenterProperties

38774d54-58e8-11ee-939d-92fbcf53809c.png

配置中心的配置信息,这里需要配置配置中心服务端的地址和使用的配置文件的id。当然这部分信息需要写在bootstrap配置文件中,前面也说过具体的原因。

2、ConfigCenterPropertySourceLocator

上面分析知道,项目启动和刷新的时候,SpringCloud是通过PropertySourceLocator的实现从配置中心加载配置信息,所以这里就得实现一下

388f67ae-58e8-11ee-939d-92fbcf53809c.png

核心的逻辑就是根据所配置的文件的id,从配置中心拉取配置信息,然后解析配置。

3、ConfigContextRefresher

这个是用来注册文件变动的监听器,来刷新文件的信息的。

因为上面提到,当配置发生变化的时候,需要发布一个RefreshEvent事件来触发刷新配置的功能。

38aa54ba-58e8-11ee-939d-92fbcf53809c.png

核心的逻辑就是当项目启动的时候,对所使用的配置文件进行注册一个监听器,监听器的实现就是当发生配置改动的时候,就发布一个RefreshEvent事件。

4、两个配置类

4.1 ConfigCenterBootstrapConfiguration

38d0eb7a-58e8-11ee-939d-92fbcf53809c.png

配置了ConfigCenterPropertySourceLocator、ConfigCenterProperties、ConfigService

4.2 ConfigCenterAutoConfiguration

38fdb52e-58e8-11ee-939d-92fbcf53809c.png

配置了ConfigContextRefresher、ConfigCenterProperties、ConfigService

最后需要将两个配置类在spring.factories配置一下。

这里有个需要注意,前面说过,SpringCloud会创建新的容器来加载配置,而这个容器只会加载spring.factories文件中键为@BootstrapConfiguration注解的配置类,所以需要将ConfigCenterBootstrapConfiguration跟BootstrapConfiguration配对,因为ConfigCenterBootstrapConfiguration配置了ConfigCenterPropertySourceLocator。

3920f066-58e8-11ee-939d-92fbcf53809c.png

好了,到这里真的就完成了对SpringCloud整合了。

二、测试

1、新增一个配置文件

启动配置中心的server端,然后打开ApiPost,新增一个配置文件

393e046c-58e8-11ee-939d-92fbcf53809c.png

新增文件类型为properties一个配置,内容为sanyou.username=sanyou键值对,当然可以写很多键值对,我这里就写了一个,新增成功之后,返回了文件的id:79765c73-c1ef-4ea2-ba77-5d27a64c4685

2、测试客户端

这里我为了方便,就把测试代码跟客户端写在同一个服务了,正常情况肯定是把跟SpringCloud代码打成一个依赖引到项目中。

在bootstrap.yml文件中配置配置中心的相关信息

39635e38-58e8-11ee-939d-92fbcf53809c.png

配置中心服务端的地址是:localhost:8888

使用的配置文件的id是刚才创建的:79765c73-c1ef-4ea2-ba77-5d27a64c4685

测试Controller

39853db4-58e8-11ee-939d-92fbcf53809c.png

提供一个接口,注入上面提到的UserService

启动项目,调用接口

3998e9b8-58e8-11ee-939d-92fbcf53809c.png

从断这里可以看出,实际注入的是一个UserService代理对象,并且最终找的是com.sanyou.configcenter.test.UserService@3a1e4fd3这个UserService对象

此时这次调用的返回值就是:sanyou

39af0888-58e8-11ee-939d-92fbcf53809c.png

接下来测试一下自动刷新属性的功能

现在修改一下配置中心的sanyou.username为sanyou666

39ce42f2-58e8-11ee-939d-92fbcf53809c.png

静静等待5秒钟。。

此时控制台打印出 Refresh keys changed: [sanyou.username] ,也就是sanyou.username属性变了

39f8afce-58e8-11ee-939d-92fbcf53809c.png

此时再次获取username

3a1517fe-58e8-11ee-939d-92fbcf53809c.png

可以看出,UserService代理对象没变,但是UserService对象已经变成了com.sanyou.configcenter.test.UserService@4237b3cd

此时获取到的username就已经变成了sanyou666

3a30cdb4-58e8-11ee-939d-92fbcf53809c.png

所以,到这里就成功将我们自己写的那个简易版的配置中心整合到了SpringCloud中了。

不足和改进

虽然我们这里的配置中心有了配置中心基本的功能,但是其实还有很多的不足和可以改进的地方。

1、配置变更推送问题

问题前面也说过,在判断配置是否变更的时候,这里是每隔5s从服务端获取一次,这里就会可能5s之后才能感知到配置有变化,达不到真正时实的效果,并且由于这里是由客户端根据来判断,会导致无效的请求过多,因为可能配置压根没有变化,但是还是每隔5s获取一次配置信息,白白浪费资源

解决这个问题可以换成上面提到的push方式来做,或者将轮询方式改成长轮询的方式实现也是可以的。

2、高可用问题

这里服务端的实例只有一个,不支持集群的方式,就会有单点故障的问题,不支持高可用。在实际项目中,肯定要支持集群的方式,保证即使有服务实例挂了,整个集群仍然可以继续对外提供服务,比如nacos就支持集群的方式,并且可以自由选择是使用AP模式还是CP模式。

3、通信协议和序列化协议

对于通信协议,这里为了方便,我选择了客户端和服务端的通信方式是基于http协议的,当然也可以自定义协议,或者使用其它的协议,比如gRPC协议。其实在nacos2.x的版本中,nacos开始全面拥抱gRPC协议了。

至于序列化协议,这里选择了json协议,因为很简单、常见、使用范围广、跨语言,当然也可以选择其它的,比如hessian序列化协议等等。

4、多租户隔离

一个合格的配置中心需要能支持不同应用的隔离,还有同一个应用不同环境的隔离,这里就图省事,直接就是有一个文件id来表示,虽然也可以做到隔离(不同系统用不同的文件id),但是这种方式比较low。像nacos会自动根据配置的名称和后缀名之类的,生成文件id(dataId),同时还有分组的概念,其实就是为了做到隔离的效果。

5、鉴权

鉴权是一个系统比较常见的东西,这里就不做过多赘述

6、控制页面

上面所有对于配置的crud都是基于ApiPost来的,但是实际怎么也得通过一个页面来操作吧,至于这里我为啥不自己写个页面,给你个眼神自己体会~~

最后,本文代码地址:

https://github.com/sanyou3/sanyou-config-center


审核编辑:汤梓红

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • Web
    Web
    +关注

    关注

    2

    文章

    1238

    浏览量

    68473
  • 数据库
    +关注

    关注

    7

    文章

    3591

    浏览量

    63375
  • 客户端
    +关注

    关注

    1

    文章

    282

    浏览量

    16345
  • springcloud
    +关注

    关注

    0

    文章

    17

    浏览量

    1492

原文标题:撸了一个简易的配置中心,顺带还给整合到了SpringCloud

文章出处:【微信号:芋道源码,微信公众号:芋道源码】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    5G电视或成下代5G无线通信服务核心功能

    电视以及视讯内容可能会成为下一代5G无线通信服务的核心功能。..
    发表于 02-17 08:14 1351次阅读

    阿里云数据库POLARDB核心功能物理复制技术解读

    深入解读阿里云数据库POLARDB核心功能物理复制技术
    发表于 06-02 10:16

    智能数字钟的核心功能定位

    目录前言… 2第一章 需求定义… 10产品功能定义… 10智能数字钟的核心功能定位… 14芯片选型… 15基于成本约束的设计思路… 15CPU的选型… 15音乐芯片的选型… 16天气预报语音播报芯片
    发表于 07-30 07:19

    F103的功能分类核心功能是什么

    文章目录F103的功能分类核心功能:缺一不可,缺少任何一个都不能工作。重要功能:根据每一款单片机的不同,具有不同的偏重点。多为帮助内核做一些内核不能做的事情。通信功能:单片机行业成熟,
    发表于 12-10 07:33

    PX780游戏本控制中心驱动安装和功能使用

    我们收到新机器的时候,想要设置键盘颜色、或者要设置风扇速度不知从何下手,要怎么安装驱动、控制中心功能怎么使用!!!那么接下来由我华仔为大家讲解机械师PX780游戏本控制中心驱动安装和功能使用。本篇
    发表于 01-10 07:05

    Bifrost GPU可编程核心的顶级布局、优势和着色器核心功能

    本指南介绍了典型的马里Bifrost GPU可编程核心(第三代马里GPU)的顶级布局、优势和着色器核心功能。Bifrost家族包括Mali-G30、Mali-G50和Mali-G70系列产品。 在
    发表于 08-02 17:52

    智齿云呼叫中心核心功能,一文秒懂

    等,助力企业快速提升客户满意度及营销效率。今天,我们就逐一拆解功能,为大家详细介绍智齿的云呼叫中心产品: 云客服智齿云客服(呼入)场景核心功能的设计遵循客户接入、客户服务和服务管理三层一站式满足的设计原则
    的头像 发表于 08-06 17:42 3079次阅读

    核心功能具体的执行过程-2

    这篇我们主要讲解一下 axios 中的 配置、拦截器和执行链等一些核心功能到底是怎么运行的。
    的头像 发表于 03-01 09:59 365次阅读
    <b class='flag-5'>核心功能</b>具体的执行过程-2

    健康码人脸测温门禁机、“电子哨兵”闸机有哪些核心功能

    检测、门禁与闸机联动管控、健康状态一体化可视化管理等功能,还可以进行体温检测和口罩佩戴检测。健康码人脸测温门禁机的核心功能有:1、精准核验。RK3566,4核2.0
    的头像 发表于 07-25 16:35 443次阅读
    健康码人脸测温门禁机、“电子哨兵”闸机有哪些<b class='flag-5'>核心功能</b>?

    【新品】自动中继无线数传电台,16大核心功能

    新品上市全新一代LoRa数传电台远程配置加密传输自动中继组网RSSI信号强度自动中继无线数传电台——E90-DTU(400SL33)具备十六大核心功能!采用军工级LoRa调制技术的无线数传电台,具有
    的头像 发表于 08-19 10:16 996次阅读
    【新品】自动中继无线数传电台,16大<b class='flag-5'>核心功能</b>!

    机智云新版开发者中心功能一览表

    开发者中心增加了更多免开发的个性化配置功能,新手也能快速上手,实现设备接入控制和应用上线。同时配有丰富的使用文档、工具和教程,欢迎前往“机智云开发者
    的头像 发表于 09-26 10:54 417次阅读
    机智云新版开发者<b class='flag-5'>中心功能</b>一览表

    应用笔记 | TSMaster核心功能之标定数据的管理

    概述标定模块中,标定数据的管理也是其核心功能。主要包括以下方面的内容:标定数据的载入、标定数据导出、标定数据的刷写,以及配套应用程序的刷写等。下面来详细介绍下这些功能。一、标定数据的载入标定
    的头像 发表于 01-30 09:44 507次阅读
    应用笔记 | TSMaster<b class='flag-5'>核心功能</b>之标定数据的管理

    核心功能部件企业,在人形机器人赛道下了盘大棋

    国产零部件厂商们已经跃跃欲试,其中,广州市昊志机电股份有限公司(以下简称“昊志机电”)作为从事中高端数控机床、机器人、新能源汽车核心功能部件的研发设计、生产制造、销售与维修服务的技术企业,其机器人零部件产品齐全且品质卓越 ,在人形机器人赛道角逐中优势显著。
    的头像 发表于 09-18 16:43 599次阅读

    机器视觉的四大核心功能

    机器视觉的四大核心功能  机器视觉是一种通过电子系统和计算机软件实现人类视觉功能的技术。它运用计算机视觉、模式识别、图像处理和机器学习等技术,以摄像机和图像处理技术为基础,将图像转化为数字信号
    的头像 发表于 12-25 11:15 566次阅读

    智慧灌区平台功能全面解析(智慧灌区场景和核心功能

    ​   智慧灌区是当今精准农业的重要组成部分,其核心就是建设一个涵盖了灌区运营管理全流程、实现多源信息融合、涵盖多维度水资源全要素的智慧灌区平台。那么智慧灌区平台的功能和设备构成包括哪些呢?本文
    的头像 发表于 02-22 10:27 220次阅读
    智慧灌区平台<b class='flag-5'>功能</b>全面解析(智慧灌区场景和<b class='flag-5'>核心功能</b>)