Apisix-Ingress服务发现详解
apisix
Apache APISIX 是一个基于微服务 API 网关,其不仅可以处理南北向的流量,也可以处理东西向的流量即服务之间的流量。Apache APISIX 集成了控制面板和数据面,与其他 API 网关相比,Apache APISIX 的上游、路由、插件全是动态的,修改这些东西时都不用重启。并且 Apache APISIX 的插件也是热加载,可以随时插拔、修改插件。
Apache APISIX 其设计理念是基于API 网关的数据平面和控制平面分离。控制平面不仅仅能够控制 Apache APISIX ,同时其还能够控制其他组件;数据平面不仅仅能够被自身的控制平面控制,还能被其他组件所控制。由于其基于ETCD 来存储和分发路由数据,默认具备高可用,无单点故障风险。除此之外,其能够友好地支持 Prometheus、SkyWalking 动态追踪、流量复制、故障注入等相关功能。
apisix ingress
在 K8s 生态中,Ingress 作为表示 K8s 流量入口的一种资源,想要让其生效,就需要有一个 Ingress Controller 去监听 K8s 中的 Ingress 资源,并对这些资源进行相应规则的解析和实际承载流量。在当下趋势中,像 Kubernetes Ingress Nginx 就是使用最广泛的 Ingress Controller 实现。
而 APISIX Ingress 则是另一种 Ingress Controller 的实现。跟 Kubernetes Ingress Nginx 的区别主要在于 APISIX Ingress 是以 Apache APISIX 作为实际承载业务流量的数据面。
Apache APISIX Ingress Controller 除了覆盖 NGINX Ingress Controller 已有的能力外,还解决了一些 Nginx Ingress Controller 的痛点。具体如下:
1、配置的动态化加载
通常情况下,作为接入层的 Ingress Controller ,其承载着服务的入口流量引入,在生产环境中,我们的业务对系统的可靠性有着更高的要求,然而,基于 Apache APISIX Ingress Controller 其能够支持动态配置,即时生效,降低生产事故的意外及风险,有助于提高运维可维护性。2、较强的灰度能力
在实际的业务场景中,有的时候,往往会依据某些特定的需求进行权重调整,结合业务需求按比例进行流量控制,Apache APISIX Ingress Controller 可以支持 Service和 Pod 级别的权重调整,配置清晰而且可读性更强。
除此,相对于NGINX Ingress Controller 中通过 Annotation 的方式提供 Canary 灰度方案,Apache APISIX Ingress Controller 能够解决其缺陷,从而能够更好的提供灰度策略。3、较好的扩展能力
基于 Apache APISIX 强大的插件能力,Apache APISIX Ingress Controller 通过动态绑定插件来增强功能。Apache APISIX 通过插件封装逻辑,易于管理;完善的文档,易于使用和理解。Apache APISIX Ingress Controller 通过配置即可绑定和解绑插件,无需操作脚本。
APISIX Ingress 目前已经支持的自定义资源主要是以下 5 类,涉及到路由、上游、消费者、证书相关和集群公共配置的相关类别。
内置服务发现
APISIX 内置了下面这些服务发现机制:
基于 Eureka 的服务发现
基于 Nacos 的服务发现
基于 Consul 的服务发现
基于 Consul KV 的服务发现
基于 DNS 的服务发现
基于 APISIX-Seed 架构的控制面服务发现
基于 Kubernetes 的服务发现
上面介绍的这些都是基于数据面apisix配置信息手动变更操作的方案集成。其实,在基于k8s的云原生场景下,apisix还提供了一个控制面组件来对apisix的服务发现进行自动管理,那就是apisix ingress
。
下面就从源码的角度来看看apisix ingress是怎么做到自动的服务发现的
apisix ingress启动
从main.go
启动入口一路跟踪,进入providers/controller.go
的run
方法:
func (c *Controller) run(ctx context.Context) {log.Infow("controller tries to leading ...",zap.String("namespace", c.namespace),zap.String("pod", c.name),)var cancelFunc context.CancelFuncctx, cancelFunc = context.WithCancel(ctx)defer cancelFunc()// give up leaderdefer c.leaderContextCancelFunc()clusterOpts := &apisix.ClusterOptions{AdminAPIVersion: c.cfg.APISIX.AdminAPIVersion,Name: c.cfg.APISIX.DefaultClusterName,AdminKey: c.cfg.APISIX.DefaultClusterAdminKey,BaseURL: c.cfg.APISIX.DefaultClusterBaseURL,MetricsCollector: c.MetricsCollector,}err := c.apisix.AddCluster(ctx, clusterOpts)if err != nil && err != apisix.ErrDuplicatedCluster {// TODO give up the leader rolelog.Errorf("failed to add default cluster: %s", err)return}if err := c.apisix.Cluster(c.cfg.APISIX.DefaultClusterName).HasSynced(ctx); err != nil {// TODO give up the leader rolelog.Errorf("failed to wait the default cluster to be ready: %s", err)// re-create apisix cluster, used in next c.runif err = c.apisix.UpdateCluster(ctx, clusterOpts); err != nil {log.Errorf("failed to update default cluster: %s", err)return}return}// Creation Phasec.informers = c.initSharedInformers()common := &providertypes.Common{ControllerNamespace: c.namespace,ListerInformer: c.informers,Config: c.cfg,APISIX: c.apisix,KubeClient: c.kubeClient,MetricsCollector: c.MetricsCollector,Recorder: c.recorder,}c.namespaceProvider, err = namespace.NewWatchingNamespaceProvider(ctx, c.kubeClient, c.cfg)if err != nil {ctx.Done()return}c.podProvider, err = pod.NewProvider(common, c.namespaceProvider)if err != nil {ctx.Done()return}c.translator = translation.NewTranslator(&translation.TranslatorOptions{APIVersion: c.cfg.Kubernetes.APIVersion,EndpointLister: c.informers.EpLister,ServiceLister: c.informers.SvcLister,SecretLister: c.informers.SecretLister,PodLister: c.informers.PodLister,ApisixUpstreamLister: c.informers.ApisixUpstreamLister,PodProvider: c.podProvider,})c.apisixProvider, c.apisixTranslator, err = apisixprovider.NewProvider(common, c.namespaceProvider, c.translator)if err != nil {ctx.Done()return}c.ingressProvider, err = ingressprovider.NewProvider(common, c.namespaceProvider, c.translator, c.apisixTranslator)if err != nil {ctx.Done()return}c.kubeProvider, err = k8s.NewProvider(common, c.translator, c.namespaceProvider, c.apisixProvider, c.ingressProvider)if err != nil {ctx.Done()return}if c.cfg.Kubernetes.EnableGatewayAPI {c.gatewayProvider, err = gateway.NewGatewayProvider(&gateway.ProviderOptions{Cfg: c.cfg,APISIX: c.apisix,APISIXClusterName: c.cfg.APISIX.DefaultClusterName,KubeTranslator: c.translator,RestConfig: nil,KubeClient: c.kubeClient.Client,MetricsCollector: c.MetricsCollector,NamespaceProvider: c.namespaceProvider,})if err != nil {ctx.Done()return}}// Init Phaseif err = c.namespaceProvider.Init(ctx); err != nil {ctx.Done()return}if err = c.apisixProvider.Init(ctx); err != nil {ctx.Done()return}// Run Phasee := utils.ParallelExecutor{}e.Add(func() {c.checkClusterHealth(ctx, cancelFunc)})e.Add(func() {c.informers.Run(ctx)})e.Add(func() {c.namespaceProvider.Run(ctx)})e.Add(func() {c.kubeProvider.Run(ctx)})e.Add(func() {c.apisixProvider.Run(ctx)})e.Add(func() {c.ingressProvider.Run(ctx)})if c.cfg.Kubernetes.EnableGatewayAPI {e.Add(func() {c.gatewayProvider.Run(ctx)})}e.Add(func() {c.resourceSyncLoop(ctx, c.cfg.ApisixResourceSyncInterval.Duration)})c.MetricsCollector.ResetLeader(true)log.Infow("controller now is running as leader",zap.String("namespace", c.namespace),zap.String("pod", c.name),)<-ctx.Done()e.Wait()for _, execErr := range e.Errors() {log.Error(execErr.Error())}if len(e.Errors()) > 0 {log.Error("Start failed, abort...")cancelFunc()}
}
上面代码逻辑大致如下:
初始化apisix集群配置信息:用于跟数据面服务apisix通信进行相关的配置操作
初始化k8s资源inforrmers信息:用户对k8s各个资源进行监听及获取资源信息
监听k8s集群namespace资源并处理
监听k8s集群pod资源并处理
监听k8s集群ingress资源并处理
监听k8s集群中apisix自定义资源并处理,比如:apisixRoute等
监听k8s集群endpoint资源并处理
监听k8s集群secret资源并处理
监听k8s集群configmap资源并处理
监听k8s集群gateway资源并处理
下面以处理endpoint资源为例进行说明,其他资源的监听处理类似,就不一一讲解了。
服务发现
进入到k8s/endpoint/provider.go
中,我们先来看看实例初始化方法:
func NewProvider(common *providertypes.Common, translator translation.Translator, namespaceProvider namespace.WatchingNamespaceProvider) (Provider, error) {p := &endpointProvider{cfg: common.Config,}base := &baseEndpointController{Common: common,translator: translator,svcLister: common.SvcLister,apisixUpstreamLister: common.ApisixUpstreamLister,}if common.Kubernetes.WatchEndpointSlices {p.endpointSliceController = newEndpointSliceController(base, namespaceProvider)} else {p.endpointsController = newEndpointsController(base, namespaceProvider)}return p, nil
}
func newEndpointsController(base *baseEndpointController, namespaceProvider namespace.WatchingNamespaceProvider) *endpointsController {ctl := &endpointsController{baseEndpointController: base,workqueue: workqueue.NewNamedRateLimitingQueue(workqueue.NewItemFastSlowRateLimiter(1*time.Second, 60*time.Second, 5), "endpoints"),workers: 1,namespaceProvider: namespaceProvider,epLister: base.EpLister,epInformer: base.EpInformer,}ctl.epInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{AddFunc: ctl.onAdd,UpdateFunc: ctl.onUpdate,DeleteFunc: ctl.onDelete,},)return ctl
}
注意到最后代码的AddEventHandler
,这里就是我们经常见到的informer的处理回调方法设置的地方。
我们看到针对endpoint资源的增删改,设置了对应的回调方法。这里来看看onAdd方法:
func (c *endpointsController) onAdd(obj interface{}) {key, err := cache.MetaNamespaceKeyFunc(obj)if err != nil {log.Errorf("found endpoints object with bad namespace/name: %s, ignore it", err)return}if !c.namespaceProvider.IsWatchingNamespace(key) {return}log.Debugw("endpoints add event arrived",zap.String("object-key", key))c.workqueue.Add(&types.Event{Type: types.EventAdd,// TODO pass key.Object: kube.NewEndpoint(obj.(*corev1.Endpoints)),})c.MetricsCollector.IncrEvents("endpoints", "add")
}
该方法的参数表示增加的endpoint资源对象信息。该方法主要是向endpoint的队列workqueue
中增加一个事件对象:包含事件类型、增加的endpoint对象
。
在最开始main我们介绍provider的启动方法中提到:执行了每个provider的run方法,下面我们来看下endpoint的provider的run方法:
func (c *endpointsController) run(ctx context.Context) {log.Info("endpoints controller started")defer log.Info("endpoints controller exited")defer c.workqueue.ShutDown()if ok := cache.WaitForCacheSync(ctx.Done(), c.epInformer.HasSynced); !ok {log.Error("informers sync failed")return}handler := func() {for {obj, shutdown := c.workqueue.Get()if shutdown {return}err := c.sync(ctx, obj.(*types.Event))c.workqueue.Done(obj)c.handleSyncErr(obj, err)}}for i := 0; i < c.workers; i++ {go handler()}<-ctx.Done()
}
func (c *endpointsController) sync(ctx context.Context, ev *types.Event) error {ep := ev.Object.(kube.Endpoint)ns, err := ep.Namespace()if err != nil {return err}newestEp, err := c.epLister.GetEndpoint(ns, ep.ServiceName())if err != nil {if errors.IsNotFound(err) {return c.syncEmptyEndpoint(ctx, ep)}return err}return c.syncEndpoint(ctx, newestEp)
}
func (c *baseEndpointController) syncEndpoint(ctx context.Context, ep kube.Endpoint) error {log.Debugw("endpoint controller syncing endpoint",zap.Any("endpoint", ep),)namespace, err := ep.Namespace()if err != nil {return err}svcName := ep.ServiceName()svc, err := c.svcLister.Services(namespace).Get(svcName)if err != nil {if k8serrors.IsNotFound(err) {return c.syncEmptyEndpoint(ctx, ep)}log.Errorf("failed to get service %s/%s: %s", namespace, svcName, err)return err}switch c.Kubernetes.APIVersion {case config.ApisixV2beta3:var subsets []configv2beta3.ApisixUpstreamSubsetsubsets = append(subsets, configv2beta3.ApisixUpstreamSubset{})auKube, err := c.apisixUpstreamLister.V2beta3(namespace, svcName)if err != nil {if !k8serrors.IsNotFound(err) {log.Errorf("failed to get ApisixUpstream %s/%s: %s", namespace, svcName, err)return err}} else if auKube.V2beta3().Spec != nil && len(auKube.V2beta3().Spec.Subsets) > 0 {subsets = append(subsets, auKube.V2beta3().Spec.Subsets...)}clusters := c.APISIX.ListClusters()for _, port := range svc.Spec.Ports {for _, subset := range subsets {nodes, err := c.translator.TranslateEndpoint(ep, port.Port, subset.Labels)if err != nil {log.Errorw("failed to translate upstream nodes",zap.Error(err),zap.Any("endpoints", ep),zap.Int32("port", port.Port),)}name := apisixv1.ComposeUpstreamName(namespace, svcName, subset.Name, port.Port, types.ResolveGranularity.Endpoint)for _, cluster := range clusters {if err := c.SyncUpstreamNodesChangeToCluster(ctx, cluster, nodes, name); err != nil {return err}}}}case config.ApisixV2:var subsets []configv2.ApisixUpstreamSubsetsubsets = append(subsets, configv2.ApisixUpstreamSubset{})auKube, err := c.apisixUpstreamLister.V2(namespace, svcName)if err != nil {if !k8serrors.IsNotFound(err) {log.Errorf("failed to get ApisixUpstream %s/%s: %s", namespace, svcName, err)return err}} else if auKube.V2().Spec != nil && len(auKube.V2().Spec.Subsets) > 0 {subsets = append(subsets, auKube.V2().Spec.Subsets...)}clusters := c.APISIX.ListClusters()for _, port := range svc.Spec.Ports {for _, subset := range subsets {nodes, err := c.translator.TranslateEndpoint(ep, port.Port, subset.Labels)if err != nil {log.Errorw("failed to translate upstream nodes",zap.Error(err),zap.Any("endpoints", ep),zap.Int32("port", port.Port),)}name := apisixv1.ComposeUpstreamName(namespace, svcName, subset.Name, port.Port, types.ResolveGranularity.Endpoint)for _, cluster := range clusters {if err := c.SyncUpstreamNodesChangeToCluster(ctx, cluster, nodes, name); err != nil {return err}}}}default:panic(fmt.Errorf("unsupported ApisixUpstream version %v", c.Kubernetes.APIVersion))}return nil
}
上面代码主要逻辑就是:
从endpoint的队列
workqueue
中获取事件对象根据endpoint信息从k8s集群中获取最新的namespace和service等信息
根据namespace和servicename从k8s集群中获取apisix upstream资源信息
对每一个service端口,向数据面服务apisix发送配置更新请求
https://xiaorui.cc/archives/7369
相关文章:

Apisix-Ingress服务发现详解
apisix Apache APISIX 是一个基于微服务 API 网关,其不仅可以处理南北向的流量,也可以处理东西向的流量即服务之间的流量。Apache APISIX 集成了控制面板和数据面,与其他 API 网关相比,Apache APISIX 的上游、路由、插件全是动态的…...

spring6-事务
文章目录 1、JdbcTemplate1.1、简介1.2、准备工作1.3、实现CURD①装配 JdbcTemplate②测试增删改功能③查询数据返回对象④查询数据返回list集合⑤查询返回单个的值 2、声明式事务概念2.1、事务基本概念①什么是事务②事务的特性 2.2、编程式事务2.3、声明式事务 3、基于注解的…...

JavaFx学习问题2--音频、视频播放失败情况
文章目录 一、路径注意事项:① 用相对路径的时候别忘了前面的斜杠② uri问题 二、播放不了的问题① 获取的媒体文件路径本身就是不对的② 必须是uri③ 特殊情况 额外收获: 一、路径注意事项: 完整代码如下: import javafx.application.Application; im…...

第55节—— redux-toolkit中的createReducer——了解
一、概念 当我们使用 Redux 开发应用程序时,一个非常重要的概念就是 reducer。一个 reducer 是一个纯函数,它接受先前的状态和一个动作,然后返回一个新状态。每个动作都会引起状态的变化,从而使应用程序状态管理更加清晰和可控。…...

JUC并发编程——JUC并发编程概述及Lock锁(重点)(基于狂神说的学习笔记)
基于bilibili狂神说JUC并发编程视频所做笔记 概述 什么是JUC JUC时java.util工具包中的三个包的简称 java.util.concurrent java.util.concurrent.atomic java.util.concurrent.locks 业务:普通的线程代码中,我们常使用Runnable接口 但Runnable没有返…...

深入了解 Java 中的时间信息定义、转换、比较和操作
1. 简介 在过去的传统Java日期处理中,经常面临着一些问题。比如,java.util.Date和java.util.Calendar在表示日期和时间时存在着一些奇怪的行为,如月份从0开始计数、对日期进行格式化的方式繁琐不直观等。这些问题给开发带来了一定的困扰。 …...

2023年中国智能矿山发展历程及趋势分析:智能矿山健康有序发展[图]
智能矿山系统对矿山生产提质增效的效果已经开始显现:对不合规、有风险的行动进行及时预警,减少安全事故发生概率,避免因停产整顿产生的巨额亏损;精细化管理整个生产流程,避免过往传统粗放的流程导致的浪费,…...

acwing算法基础之基础算法--整数离散化算法
目录 1 知识点2 模板 1 知识点 整个范围很大,但存在的数据点很少。比如从 − 1 0 9 -10^9 −109到 1 0 9 10^9 109,但总共只有 1 0 6 10^6 106个数。 可以采用离散化的思想来做,即将离散的大数值映射成连续的小数值(一般是 1 , …...

基于SSM框架的安全教育平台
末尾获取源码 开发语言:Java Java开发工具:JDK1.8 后端框架:SSM 前端:Vue 数据库:MySQL5.7和Navicat管理工具结合 服务器:Tomcat8.5 开发软件:IDEA / Eclipse 是否Maven项目:是 目录…...

Kafka生产者使用案例
1.生产者发送消息的过程 首先介绍一下 Kafka 生产者发送消息的过程: 1)Kafka 会将发送消息包装为 ProducerRecord 对象, ProducerRecord 对象包含了目标主题和要发送的内容,同时还可以指定键和分区。在发送 ProducerRecord 对象前,…...

EasyX图形库实现贪吃蛇游戏
⭐大家好,我是Dark Falme Masker,学习了动画制作及键盘交互之后,我们就可以开动利用图形库写一个简单的贪吃蛇小游戏,增加学习乐趣。 ⭐专栏:EasyX部分小游戏实现详细讲解 最终效果如下 首先包含头文件 #include<stdio.h> #…...

利用 Amazon CodeWhisperer 激发孩子的编程兴趣
我是一个程序员,也是一个父亲。工作之余我会经常和儿子聊他们小学信息技术课学习的 Scratch 和 Kitten 这两款图形化的少儿编程工具。 我儿子有一次指着书房里显示器上显示的 Visual Studio Code 问我,“为什么我们上课用的开发界面,和爸爸你…...

2023年中国分子筛稀土催化材料竞争格局及行业市场规模分析[图]
稀土催化材料能够起到提高催化剂热稳定性、催化剂活性、催化剂储氧能力,以及减少贵金属活性组分用量等作用,广泛应用于石油化工、汽车尾气净化、工业废气和人居环境净化、燃料电池等领域。 2015-2023年中国稀土催化材料规模及预测 资料来源:…...

vue3插件——vue-web-screen-shot——实现页面截图功能
最近在看前同事发我的vue3框架时,发现他们有个功能是要实现页面截图功能。 vue3插件——vue-web-screen-shot——实现页面截图功能 效果图如下:1.操作步骤1.1在项目中添加vvue-web-screen-shot组件1.2在项目入口文件导入组件——main.ts1.3在需要使用的页…...

简单总结Centos7安装Tomcat10.0版本
文章目录 前言JDK8安装部署Tomcat 前言 注意jdk与tomcat的兼容问题,其他的只要正确操作一般问题不大 Tomcat 是由 Apache 开发的一个 Servlet 容器,实现了对 Servlet 和 JSP 的支持,并提供了作为Web服务器的一些特有功能,如Tomca…...

ffmpeg中AVCodecContext和AVCodec的关系分析
怎么理解AVCodecContext和AVCodec的关系 AVCodecContext和AVCodec是FFmpeg库中两个相关的结构体,它们在音视频编解码中扮演着不同的角色。 AVCodecContext:是编解码器上下文结构体,用于存储音视频编解码器的参数和状态信息。它包含了进行音视…...

2023年中国门把手产量、销量及市场规模分析[图]
门把手行业是指专门从事门把手的设计、制造、销售和安装等相关业务的行业。门把手是门窗装饰硬件的一种,用于开启和关闭门窗,同时也具有装饰和美化门窗的作用。 门把手行业分类 资料来源:共研产业咨询(共研网) 随着消…...

HTML 核心技术点基础详细解析以及综合小案例
核心技术点 网页组成 排版标签 多媒体标签及属性 综合案例一 - 个人简介 综合案例二 - Vue 简介 02-标签语法 HTML 超文本标记语言——HyperText Markup Language。 超文本:链接 标记:标签,带尖括号的文本 标签结构 标签要成…...

BAT学习——批处理脚本(也称为BAT文件)常用语法元素与命令
批处理脚本(也称为BAT文件)使用Windows的批处理语言编写,它具有一些常用的语法元素和命令。以下是一些BAT编程的常用语法元素和命令: 命令行命令: 批处理脚本通常包含一系列Windows命令,例如echo࿰…...

AMD AFMF不但能用在游戏,也适用于视频
近期AMD发布了AMD Software Adrenalin Edition预览版驱动程序,增加了对平滑移动帧(AMD Fluid Motion Frames,AFMF)功能的支持,也就是AMD的“帧生成”技术,与DLSS 3类似,作为FidelityFX Super Re…...

CSS 常用样式浮动属性
一、概述 CSS 中,浮动属性的作用是让元素向左或向右浮动,使其他元素围绕它排布,常用的浮动属性有以下几种: float: left; 使元素向左浮动,其他元素从右侧包围它。 float: right; 使元素向右浮动,其他元素…...

Linux引导故障排除:从问题到解决方案的详细指南
1 BIOS初始化 通电->对硬件检测->初始化硬件时钟 2 磁盘引导及其修复 2.1 磁盘引导故障 磁盘主引导记录(MBR)是在0磁道1扇区位置,446字节。 MBR作用:记录grub2引导文件的位置 2.2 修复 步骤:1、光盘进…...

【vim 学习系列文章 6 -- vim 如何从上次退出的位置打开文件】
文章目录 1.1 vim 如何从上次退出的位置打开文件1.2 autogroup 命令学习1.2.1 augroup 基本语法 1.3 vim call 命令详细介绍 1.1 vim 如何从上次退出的位置打开文件 假设我打开了文件 test.c,然后我向下滚动到第 50 行,然后我做了一些修改并关闭了文件。…...

怎样学习C#上位机编程?
怎样学习C#上位机编程? 00001. 掌握C#编程和.NET框架基础。 00002. 学WinForm应用开发,了解控件使用和事件编程。 00003. 熟悉基本数据结构和算法,如链表、栈、队列。 00004. 理解串口通信协议和方法,用于与硬件交互。 00005…...

【算法-动态规划】两个字符串的删除操作-力扣 583
💝💝💝欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:kuan 的首页,持续学…...

【06】基础知识:typescript中的泛型
一、泛型的定义 在软件开发中,我们不仅要创建一致的定义良好的API,同时也要考虑可重用性。 组件不仅能支持当前数据类型,同时也能支持未来的数据类型,这在创建大型系统时提供了十分灵活的功能。 在像 C# 和 Java 这样的语言中&…...

flutter 绘制原理探究
文章目录 Widget1、简介2、源码分析Element1、简介2、源码分析RenderObjectWidget 渲染过程总结思考Flutter 的核心设计思想便是“一切皆 Widget”,Widget 是 Flutter 功能的抽象描述,是视图的配置信息,同样也是数据的映射,是 Flutter 开发框架中最基本的概念。 在 Flutter…...

[Java]SPI扩展功能
一、什么是SPI Java SPI(Service Provider Interface)是Java官方提供的一种服务发现机制。 它允许在运行时动态地加载实现特定接口的类,而不需要在代码中显式地指定该类,从而实现解耦和灵活性。 二、实现原理 基于 Java 类加载…...

机器人命令表设计
演算命令 CLEAR 将数据 1 上被指定的编号以后的变数的内容,以及数据 2 上仅被指定的个数都清除至 0。 INC 在被指定的变数内容上加上 1。 DEC 在被指定的变数内容上减掉 1。 SET 在数据 1 上设定数据 2。 ADD 将数据 1 和数据 2 相加,得出的结果保存在数…...

STM32--WDG看门狗
文章目录 WDG简介IWDGIWDG的超时计算WWDGWWDG超时和窗口值设定独立看门狗工程WWDG工程 WDG简介 WDG看门狗(Watchdog Timer)是一种常见的硬件设备,在STM32F10系列中,有两种看门狗,分别是独立看门狗和窗口看门狗&#x…...