2024年11月

本文分享自
《华为云DTSE》第五期开源专刊
,作者:聂子雄 华为云高级工程师、李来 华为云高级工程师。

微服务是一种用于构建应用的架构方案,可使应用的各个部分既能独立工作,又能协同配合,微服务的治理模式在适应云原生的方向也逐步在演进中。本文以汽车行业DMS系统在微服务应用发布时面临的挑战为切入点,介绍了基于微服务SDK框架与JavaAgent技术的全链路灰度发布,整体方案能够有效提升微服务应用发布的效率。

1、微服务应用在发布时面临的挑战

微服务架构因其小而独立的特点受到广大开发者欢迎,我们生活中很多常见的应用也是基于微服务的架构。微服务架构强调应用拆分为一系列责任单一的小型服务单元,各个服务单元可进行独立部署,相互协作配合,这种架构模式极大的提高了IT团队的开发效率。

微服务应用在发布的时候一般会采用一种叫灰度发布的策略,它将新版本的软件逐步地推送给一小部分用户,以便在全面发布之前测试和验证新版本的稳定性和可靠性。这种发布策略可以减少潜在的风险和影响,因为只有一小部分用户受到影响,而其他用户仍然可以使用旧版本的软件。

常见的灰度发布一般只针对某个单点的服务进行实施,在很多情况下,这种方法可以大大提高应用发布的效率及稳定性。当然,单点灰度发布实际上也存在一些缺点,下面以汽车行业中的DMS系统为例进行分析:

DMS全称为汽车经销商管理系统(Dealer Management System),专门为汽车经销商和售后服务提供商设计的软件系统,帮助汽车经销商实现业务数字化、自动化和智能化,提高业务效率、降低成本、提升服务质量。很多厂商的DMS系统都做过微服务化改造,在提高了团队开发效率的同时,也遇到了一些业务发布场景的挑战:

  • 经销商想在某一个门店A上线自己的新业务,作为业务试点门店,比如新品汽车销售,或者打折促销活动等。和新业务相关的流量只会流入试点门店B。

  • 为了节约成本以及降低部署服务工作量,希望能够实现逻辑上的环境隔离。例如,测试环境有部分服务复用生产环境上的模块,开发测试人员只需要聚焦于需要测试的服务模块。

  • 经销商的交易、商品服务有新的业务要上线,新上线的功能间有依赖和交互,要在上线前做一些测试工作。

  1. 计划让测试人员专门账号来进行现网测试。

  2. 引入少量比例的生产流量进行验证。

针对上述问题,一般的思路是通过灰度发布去解决,通过灰度发布,可以引入部分的测试流量到新业务模块,也能控制带有具体特征的流量只流入到对应的测试模块,其余流量保持原有方式不动。

但是经过仔细考虑,就会发现如果只做单点灰度发布,其实是无法完善地解决以上场景的痛点问题,主要体现在:

业务特征时常只在第一跳,也就是特征只在入口,传递过程中会丢失。

除了第一跳入口,后续微服务之间进行调用的时候也会把特征给丢失。

因此,仅仅依靠单点灰度发布的能力是不够的,还需要能够做到整条微服务调用链的可灰度,也就是全链路灰度的能力,这样就可以灵活解决类似DMS系统遇到的问题。

后续我们将以全链路灰度发布的场景来展示微服务SDK框架和JavaAgent如何相互结合,解决真实场景中的服务发布问题。

2、微服务治理方案的选择

在提出具体解决方案之前,我们可以先了解基于微服务SDK框架以及基于JavaAgent技术的治理模式。微服务SDK进行治理是常见的一种形态,我们常见的Spring Cloud、Dubbo都属于微服务SDK架构,这种方式通常可以较为方便的通过外部依赖的方式集成各种服务治理功能。JavaAgent则可以通过非侵入的方式引入微服务治理功能,下面将对这两种模式进行解析。

2.1 基于微服务SDK框架的微服务治理

2.1.1 原理和优势

服务治理是一个宽泛的概念,通常来说,保证微服务可靠运行的策略,都可以称为服务治理。这些策略涵盖开发态、运行态和运维态等微服务生命周期。可以从两个不同的角度进行描述服务治理:

  • 从管理流程上,可以分为进行业务定义和设置治理规则两个步骤。系统架构师将请求流量根据特征打上标记,用于区分一个或者一组代表具体含义的业务,然后对这些业务设置治理规则。

  • 从处理过程上,可以分为下发配置和应用治理规则两个步骤。可以通过配置文件、配置中心、环境变量等常见的配置管理手段下发配置。微服务SDK框架负责读取配置,解析治理规则,实现治理效果。

  • 由于微服务应用是基于微服务SDK框架开发的,开发者可以明显可以感知微服务SDK框架的存在,此外SDK和应用实例是运行在同一个进程中,这些特点也给微服务的治理带来了一些好处,包括但不限于以下:

  • 更轻量级:微服务SDK框架在运行时不需要起单独的进程,因此资源开销较小。

  • 治理粒度更精细:微服务SDK可以直接对应用实例的某个方法直接进行管理,因此能够满足各种治理场景要求。

  • 性能高,时延低:微服务实例之间的链路不存在代理,访问的时候是直接点对点的调用,因此时延低,并且微服务SDK框架可以提供高性能的RPC,保障数据的高效传输。

华为云也致力于为开发者提供全面开放,方便高效的微服务SDK框架,目前对外开源的稳定成熟框架主要有Spring Cloud Huawei和Java Chassis。

2.1.2 Spring Cloud Huawei框架

自从Netflix开源出最早的Spring Cloud微服务SDK框架,Spring Cloud在这个领域的发展非常迅速,目前Spring Cloud已经是业界广泛使用的微服务SDK框架之一。为了让开发者能够更加方便、高效地使用Spring Cloud开发可靠的微服务应用,基于Spring Cloud和华为云服务生态体系,华为云提供了Spring Cloud Huawei微服务SDK框架,为开发者提供了一站式的开发、部署、运维、监控、治理等全生命周期的服务。

使用Spring Cloud Huawei,开发者可以不用熟悉和了解Spring Cloud,只需要熟悉Spring和Spring Boot,就能够按照微服务架构模式开发应用。相对于Spring Cloud,Spring Cloud Huawei能够更好的支持快速微服务开发,提供开箱即用的微服务治理能力。

2.1.3 Java Chassis框架

Java Chassis框架是Apache ServiceComb项目下面向Java语言的微服务框架。ServiceComb项目最早源于华为微服务引擎(CSE),在2017年12月捐赠给Apache基金会,目前已经形成了庞大的微服务生态,而Java Chassis是其中重要一环,着重解决微服务面临的如下问题:

  • 微服务通信性能

  • 微服务运维和治理

  • 遗留系统改造

  • 配套DevOps

Java Chassis框架包含服务契约、编程模型、运行模型与通信模型四个部分,具备负载均衡、容错熔断、限流降级、调用链追踪等全面微服务治理能力。下面是总体的架构设计图:

为了支持软件工程实践, Java Chassis 的运行时架构是一个哑铃结构, 两端分别是“编程模型”和“通信模型”,中间是“运行模型”。“编程模型”面向开发者写服务接口的习惯,“通信模型”面向微服务之间的高效编码和通信,“运行模型”基于“契约”,提供一种服务无关的插拔机制,能够让开发者独立于业务实现开发治理功能,并且灵活的移除和增加功能,以及调整这些治理功能的处理顺序。“运行模型”的核心抽象接口是 Handler ,这个接口是一个异步的定义,Java Chassis 运行时模型采用纯异步的实现,让整个系统运行非常高效。

Java Chassis和Spring Cloud都实现了微服务架构模式,相比而言,Java Chassis 是一个更加紧凑的实现,开箱即用,而 Spring Cloud则是相对松散的实现,整合了大量的Netflix组件。

2.2 基于JavaAgent的非侵入微服务治理

JavaAgent是如何实现能服务治理能力的?其技术核心在于Java进程启动时,可以挂载JavaAgent来执行字节码的增强逻辑。JVM启动后,JavaAgent运行于Java应用之前,可以修改原应用的目标类和方法的字节码,做到非侵入地增强,原应用中被增强的类在JVM中实例化的对象都是已经被JavaAgent处理过的,因此在业务应用的代码执行时,我们的服务治理逻辑就能悄无声息地通过这种方法实现注入。

例如,我们可以增强服务发现过程,对这一过程中的某个关键函数进行拦截增强,插入一段路由筛选服务提供者实例的逻辑,根据服务实例的元数据和下发的路由规则选定对应的服务提供者,以此来完成灰度发布的功能。下图简化地介绍了字节码JavaAgent字节码增强的核心原理:

类似地,其他的服务治理能力的开发都可以通过这种方式,用户的Java业务应用无需修改,业务应用的开发者也无需理解其中的深层原理,只要把实现了服务治理功能的JavaAgent挂载上即可一键非侵入接入服务治理。

基于上述原理,华为云开源团队开发了无代理服务网格Sermant项目,目前已经在Github开源。Sermant专注于利用Java字节码增强技术为宿主应用程序提供服务治理功能,以解决大规模微服务场景中的服务治理问题。Sermant构建了非侵入、高性能、插件化核心框架,并在框架中提供了对接动态配置中心、事件上报、链路标记、心跳等服务治理通用能力。

当前开源仓库中提供的插件涵盖了服务注册、标签路由、流量控制、服务监控等常见的服务治理场景。Sermant的后端观测、动态配置中心、Agent等组件提供的一套完整的解决方案如下:

利用Sermant来进行微服务治理能够集合上文提到的各种服务架构的优势:

  • 高性能:对比传统的服务治理边车,由于没有边车进程,因此有更高的性能和更低的资源损耗。

  • 非侵入:对比微服务 SDK 架构架构,无需代码修改,升级云原生架构无需代码改造。

  • 插件化架构:服务治理功能作为插件嵌入到JavaAgent内部,可动态部署,且内部充分类隔离,和业务代码无类冲突。

3、方案详解

结合以上两种治理模式的各自特点,便可设计出理想的全链路灰度发布方案。目前要实现全链路灰度,一般要考虑这些问题的处理:

  • 在第一跳的地方(一般是网关),我们需要能选中各种类型的流量,把这部分流量染色,再路由到正确的目标。

  • 除了第一跳,剩下调用链路中的各个微服务能够识别染色标,透传染色标,并路由到正确的目标。

  • 能对异常情况进行妥善处理。

3.1 全链路灰度发布方案的具体流程

针对以上问题,我们有一套相对完善的全链路灰度发布方案,整体方案如下:

  • 在前端部分,请求会统一携带流量标签参数发到网关上面。

  • 网关会选中各种类型的流量,将这些流量根据需求分别染色,比如通过请求header进行标记染色。

  • 网关会将染色后的流量转发到带有不同tag的后端微服务实例,tag可以由应用发布流水线注入到相应发布的微服务实例当中。

  • 借助微服务实例上运行的SDK/JavaAgent,接收到应用网关流量的微服务实例会通过SDK/JavaAgent提供的标签路由能力将流量特征保留并转发到合适的下一跳微服务实例。

  • 对于后续链路上的微服务实例,都可以通过微服务实例上面的SDK/Agent进行特征的传递。

3.2 SDK/JavaAgent如何助力全链路灰度发布?

由于在每个微服务实例都运行着SDK/JavaAgent,因此SDK/JavaAgent可以对每个实例进行细粒度的服务治理,包括限流,熔断,降级,标签路由等功能。在全链路灰度发布过程中,对于每条链路上的微服务实例,可以借助SDK/JavaAgent的标签路由能力实现流量特征的保留以及传递到下一跳微服务实例。进行标签路由的全流程如下:

3.3 SDK/JavaAgent如何搭配使用?

从上面的方案介绍可以知道无论是SDK还是JavaAgent的方式,其实都可以非常有效地助力全链路灰度发布方案的落地,那遇到具体业务场景对全链路灰度发布有诉求的时候,我们该如何去选择呢?

对于用户规划的新业务,用户一般会统一技术栈,因此直接使用微服务SDK框架自带的能力去实现全链路灰度发布会更加方便高效。

对于用户已有的业务,并且业务内部技术栈也不统一,这时候直接采用非侵入式的JavaAgent去做全链路灰度发布会极大程度地降低改造成本,因为业务代码本身不需要做变动,只需要在运行实例上挂载一个JavaAgent即可。当然目前的JavaAgent其实是基于Java 语言的,因此对于别的编程语言,还是得依靠微服务SDK框架来实现全链路灰度发布,但是考虑到目前Java属于使用量第一的编程语言,因此JavaAgent这种方式基本上还是能够覆盖绝大多数的场景。

总的来看,两种方式其实适用于不同的业务场景,它们之间可以相互补充,形成一整套完善的全链路灰度发布解决方案。

3.4 全链路灰度发布方案带来的优势

微服务应用通过全链路灰度发布的方式可以显著提高发布的效率以及稳定性,关键优势如下:

  • 在开发测试过程中,客户可以根据需求在逻辑上划分出一套属于自己的服务链路,只需要关注自己设定的特征流量即可,这种模式可以为客户省去搭建系统中一些共用的模块时间以及节约环境资源。

  • 在发布过程中,方便地将带有试点特征的流量引入到含有自己试点应用的链路环境当中。客户还可以根据需要把一部分生产流量引入到自己的新版本业务链路环境当中,完成新版本的验证。

4、总结

微服务治理架构的形态一直在演进,各种形态有其适用的场景和优缺点。对于企业用户和开发者来说,如何尽可能以较低的成本、较好的效率来解决微服务治理过程中的各个问题是永恒的目标。

以上我们主要对主流的微服务SDK架构和JavaAgent非侵入治理架构作了解析,对于Java应用场景来说,这两种治理模式可以在不影响性能的前提解决绝大多数场景的治理问题。

华为云在微服务治理方向的持续探索也孵化出了Spring Cloud Huawei、Java Chassis、Sermant等优秀的开源项目,并且将持续演进,丰富微服务治理领域的开源生态。


华为开发者空间,汇聚鸿蒙、昇腾、鲲鹏、GaussDB、欧拉等各项根技术的开发资源及工具,致力于为每位开发者提供一台云主机、一套开发工具及云上存储空间,让开发者基于华为根生态创新。
点击链接
,免费领取您的专属云主机

点击关注,第一时间了解华为云新鲜技术~

当集群中需要升级 Mount Pod 时,目前推荐的方式是更新配置后重新挂载应用 Pod 进行滚动升级,但这种升级方式的问题在于需要业务重启。

如果对业务的使用模式很清楚时,比如没有数据写入等,也可以选择手动重建 Mount Pod 的方式。在更新配置后,手动删除已有的 Mount Pod,并等待其重建,同时依赖 CSI 对挂载点的自动恢复功能,等待应用 Pod 中挂载点的恢复。这种升级的过程有几个问题:

  1. 操作繁琐
    :需要用户自己使用 kubectl 命令,通过应用 Pod 找到对应的 Mount Pod;
  2. 升级时业务中断
    :CSI 需要先等待旧的 Mount Pod 完全被删除,才会去创建新的 Mount Pod。这就导致了在旧的 Mount Pod 删除后、新的 Mount Pod 起来前,业务处于不可用状态;
  3. 挂载点自动恢复对原 I/O 操作的影响
    :由于 CSI 使用了重新 bind 应用挂载点的方式实现了自动恢复,所以即使在恢复后,原先的 I/O 操作都会报错。

为了解决现有的升级过程遇到的问题,JuiceFS CSI Driver 在 v0.25.0 版本中,实现了 Mount Pod 的平滑升级,即在应用不停服的情况下升级 Mount Pod。

相比有损升级的方式,使用平滑升级的好处在于:

  1. 操作简单:在 CSI Dashboard 中,可以轻易的在应用 Pod 的页面找到 Mount Pod,且点击按钮即可触发平滑升级;
  2. 业务不中断:升级过程中,业务不会受到影响,用户也可以使用平滑升级功能来进行 Mount Pod 的参数和配置修改。

01 CSI 如何实现 Mount Pod 的平滑升级

目前 JuiceFS CSI 支持两种平滑升级方式,即二进制升级和 Pod 重建升级。

二进制升级

二进制升级不会重建 Mount Pod,而是升级 Mount Pod 中的客户端二进制。其依赖 JuiceFS 客户端自身的守护进程,即社区版版本在 v1.2.0 以上,商业版版本在 v5.0.0 以上。

整个二进制升级的过程如下:

  1. 在触发平滑升级后,CSI Node 启动一个使用新镜像的 Job,在 Job 中将 JuiceFS 客户端二进制 copy 到 Mount Pod 中;
  2. 等 Job 完成后,CSI Node 向 Mount Pod 发送 SIGHUP 信号;
  3. Mount Pod 接收到 SIGHUP 信号后,将目前的 I/O 请求状态信息保存到临时文件中,此时服务进程退出;
  4. Mount Pod 的服务进程退出后,守护进程会用新的二进制再次启动新的服务进程;
  5. 新的服务进程启动后读取中间状态文件,并继续处理之前的请求;
  6. 新的服务进程向守护进程拿当前的 FUSE fd。

二进制升级使用于仅需要升级客户端的情况。但升级后查看 Pod 的 yaml,其镜像依然是旧的。由于没有重建 Pod,这种升级的好处在于速度快且风险小,缺点在于不能更新 Mount Pod 的其他配置。

Pod 重建升级

Pod 重建升级指的是重建 Mount Pod 进行平滑升级。这种升级方式依赖 JuiceFS 本身的平滑升级功能,即社区版版本在 v1.2.1 以上,商业版版本在 v5.1.0 以上。

整个 Pod 重建升级的过程如下:

  1. 每个 Mount Pod 在启动后,都会将自身使用的 FUSE fd 传给 CSI Node,CSI Node 维护了每个 Mount Pod 使用的 FUSE fd 的关系表;
  2. 在触发了平滑升级后,CSI Node 会先起一个与新 Mount Pod 相同配置的空 Job,这一步的作用是提前将新镜像 pull 在节点上,从而节省升级时间;
  3. 等 Job 完成后,CSI Node 向旧的 Mount Pod 发送 SIGHUP 信号;
  4. 旧的 Mount Pod 接收到 SIGHUP 信号后,将目前的 I/O 状态信息保存到临时文件中,并退出。此时 Mount Pod 变为 Complete 状态;
  5. CSI Node 根据 ConfigMap 中的配置,创建新的 Mount Pod;
  6. 新的 Mount Pod 起来后,读取临时文件中的中间状态信息,从而继续处理之前的请求;
  7. 新的 Mount Pod 向 CSI Node 拿当前的 FUSE fd。

其中 Mount Pod 和 CSI Node 之间通过
Unix domain socket
来传递文件句柄。当某个 FUSE 请求未能在升级期间完成,会被强制中断,建议在负载比较低的时候进行升级操作。

可以看到整个平滑升级的过程,与
宿主机上客户端的平滑升级
类似,唯一的区别在于由 CSI Node 向旧的服务进程发送 SIGHUP 信号以及新的服务进程启动后向 CSI Node 拿 FUSE fd。这是因为 Mount Pod 在重建后,其中的守护进程无法向旧的守护进程发送 SIGHUP 信号以及无法通过 Unix domain socket 传递文件句柄,所以在 K8s 环境中这些工作就交由 CSI Node 来完成。

这种升级方式由于重建了 Pod,缺点在于如果集群环境比较复杂,重建 Pod 的过程中出错的风险比较大;其优点在于可以更新 Mount Pod 的其他配置,且查看 Pod 的 yaml,其镜像是新的。

02 如何触发平滑升级

目前平滑升级可以在 CSI Dashboard 或者 kubectl 插件中触发。

Dashboard 中触发

首先在 CSI Dashboard 中,点击「配置」按钮,更新 Mount Pod 需要升级的新镜像版本。

其次,在 Mount Pod 的详情页,有两个升级按钮,分别是「Pod 重建升级」和「二进制升级」。

  • Pod 重建升级:Mount Pod 会重建,可用于更新镜像、调整挂载参数、调整 pod 资源等。Mount Pod 的最低版本要求为 1.2.1(社区版)或 5.1.0(企业版);
  • 二进制升级:Mount Pod 不重建,只升级其中的二进制,不可变更其他配置,且升级完成后 Pod yaml 中所看到的依然是原来的镜像。Mount Pod 的最低版本要求为 1.2.0(社区版)或 5.0.0(企业版)。

点击升级按钮,即可触发 Mount Pod 的平滑升级。

触发后可以看到整个过程,完成后页面会自动跳转到新的 Mount Pod 的详情页。

kubectl 插件中触发

使用 kubectl 在 CSI ConfigMap 配置中更新 Mount Pod 所需要升级的镜像版本。

apiVersion: v1
data:
   config.yaml: |
      mountPodPatch:
         - ceMountImage: juicedata/mount:ce-v1.2.0
           eeMountImage: juicedata/mount:ee-5.1.1-ca439c2
kind: ConfigMap

使用 JuiceFS kubectl plugin 触发 Mount Pod 的平滑升级。

# Pod 重建升级
kubectl jfs upgrade juicefs-kube-node-1-pvc-52382ebb-f22a-4b7d-a2c6-1aa5ac3b26af-ebngyg --recreate
# 二进制升级
kubectl jfs upgrade juicefs-kube-node-1-pvc-52382ebb-f22a-4b7d-a2c6-1aa5ac3b26af-ebngyg

03 总结

鉴于目前的有损升级方案存在诸多缺陷,JuiceFS CSI Driver 在 v0.25.0 版本中,支持了 Mount Pod 的平滑升级。CSI 提供了两种平滑升级方案,包括二进制升级和 Pod 重建升级。二进制升级风险小,但不支持更新 Mount Pod 的其他配置;Pod 重建升级在集群环境较复杂的情况下有失败的风险,但支持更新 Mount Pod 的其他配置,比如可以根据 Mount Pod 的实际资源使用情况,动态调整资源配比等。用户可以根据需要,选择更适合的升级方式,同时建议在负载比较低的时候进行升级操作。

本人之前对C#开发非常喜欢,也从事开发C#开发桌面开发、Web后端、Vue前端应用开发多年,最近一直在研究使用Python,希望能够把C#的一些好的设计模式、开发便利经验引入到Python开发中,很多时候类似的开发方式,可以极大提高我们开发的效率,本篇随笔对wxpython控件实现类似C#扩展函数处理的探究总结。

1、C#扩展函数特点及便利性回顾

C# 的扩展方法具有以下几个特点和便利性:

  1. 语法简洁
    :扩展方法允许在不修改原始类型的情况下,向现有类型添加新功能。调用时看起来像是实例方法。

  2. 易于使用
    :可以在调用扩展方法时使用点语法,这使得代码更易读且更自然。例如,对于字符串类型,可以直接调用扩展方法而不是传递实例。

  3. 静态类和静态方法
    :扩展方法必须定义在静态类中,并且本身也是静态方法。第一个参数指定了要扩展的类型,并使用
    this
    关键字修饰。

  4. 提升代码组织性
    :将相关功能组织到扩展方法中,可以减少主类中的代码量,提高可维护性。

  5. 与 LINQ 的结合
    :扩展方法在 LINQ 中的应用非常广泛,使得集合操作更加直观和简洁。

  6. 支持多种数据类型
    :可以为基本类型、集合类型甚至自定义类型添加扩展方法,从而提供更广泛的功能。

总的来说,扩展方法在提高代码可读性和可维护性方面具有明显的优势,是C#语言设计中的一项重要特性。

我在开发C#Winform应用前端的时候,在自己的公用类库上实现了很多扩展方法,特别是对于一些控件,增加了很多如绑定数据列表、绑定字典大类名称后直接加载数据列表等,以及一些对数据类型的通用处理,如字符串的格式判断或者裁剪等等。

如在随笔《
使用扩展函数方式,在Winform界面中快捷的绑定树形列表TreeList控件和TreeListLookUpEdit控件
》中介绍过,对于一些常规控件的数据绑定处理。

对于常规的列表绑定,我们可以用简单的一个扩展函数实现,如下所示。

    //常规类别绑定
    this.txtProjectList4.BindDictItems(list, "Text", "Value", true, columns.ToArray());

定义了扩展方法,就很容易实现数据的绑定,减少涉及控件处理的细节。

那么对于Python如何使用类似的方式实现呢,我们需要对Python 的语言特性进行了解和实际测试下。

2、基于Python实现数据的快速绑定

我们让ChatGPT来进行解答,它给出的代码答案是如下所示。

1)属性绑定方式

importwx#定义扩展方法
defBindDictItems(self, items_dict):
self.Clear()
#清空现有项 for key, value initems_dict.items():
self.Append(value, key)
#添加项,key 为用户数据,value 为显示内容 #将扩展方法绑定到 wx.ComboBox 类 setattr(wx.ComboBox, "BindDictItems", BindDictItems)#测试应用 classMyApp(wx.App):defOnInit(self):
frame
= wx.Frame(None, title="ComboBox BindDictItems Example", size=(300, 200))
panel
=wx.Panel(frame)

combo_box
= wx.ComboBox(panel, style=wx.CB_DROPDOWN, pos=(10, 10))#使用扩展方法绑定字典项 items ={"item1": "Item One","item2": "Item Two","item3": "Item Three"}
combo_box.BindDictItems(items)

frame.Show()
returnTrueif __name__ == "__main__":
app
=MyApp()
app.MainLoop()

解释

  1. 定义
    BindDictItems
    方法

    :这个方法接受一个字典,将字典中的项绑定到
    ComboBox
    中。
    • 使用
      self.Clear()
      清空现有项。
    • 遍历字典,并使用
      Append
      方法将项添加到
      ComboBox
  2. 使用
    setattr
    绑定方法

    :通过
    setattr
    将定义的方法添加到
    wx.ComboBox
    类中。
  3. 测试应用
    :在
    MyApp
    中创建一个
    wx.ComboBox
    并调用
    BindDictItems
    方法。

这样,你就可以为
wx.ComboBox
提供一个
BindDictItems
方法。

测试发现,对于功能实现没有问题,但是对于开发的时候,combo_box 没有函数提示,也就是没有强类型的函数提示,这样只是进行了动态绑定,无法在开发的时候,获得IDE的智能提示,很容易写错。

另外也可以使用下面代码实现类似setattr的处理,相当于直接附加方法,同样在使用的时候也是没有函数的智能提示,但是能够正常工作,俗称
猴子补丁

#使用猴子补丁将方法添加到 wx.ComboBox
wx.ComboBox.BindDictItems = bind_dict_items

2)自定义子类方式

对该结果不满意,继续追问实现该功能的其他途径,给出的另一个解决方案是继承父类,在子类实现更多的自定义函数。

为了实现更好的智能提示和类型检查,可以通过使用类型注解和创建一个自定义子类来扩展
wx.ComboBox
。这样可以确保 IDE 提供正确的代码补全和提示。

给出的代码如下所示。

importwxfrom typing importDictclassMyComboBox(wx.ComboBox):def BindDictItems(self, items_dict: Dict[str, str]) ->None:"""Bind a dictionary of items to the ComboBox."""self.Clear()#清空现有项
        for key, value initems_dict.items():
self.Append(value, key)
#添加项,key 为用户数据,value 为显示内容 classMyApp(wx.App):defOnInit(self):
frame
= wx.Frame(None, title="ComboBox BindDictItems Example", size=(300, 200))
panel
=wx.Panel(frame)

combo_box
= MyComboBox(panel, style=wx.CB_DROPDOWN, pos=(10, 10))#使用扩展方法绑定字典项 items ={"item1": "Item One","item2": "Item Two","item3": "Item Three"}
combo_box.BindDictItems(items)

frame.Show()
returnTrueif __name__ == "__main__":
app
=MyApp()
app.MainLoop()

通过这种方式,可以确保在使用
BindDictItems
时能够获得智能提示,从而减少错误。

这样的方式,确实有强类型的函数提示了。虽然可以获得智能提示,但是也就是破坏了界面类的代码,也就是需要修改相关的使用代码,而非C#扩展方法那样,隐式的扩展了。

3)通过中介辅助类的方式实现

有些情况下,不适合继承关系,不可能为每个类都提供一个子类来封装,有时候提供一些辅助类可能更具有性价比。

要在不继承父类的情况下实现类似 C# 的扩展方法,并确保获得智能提示,推荐使用类型注解和一个中介类来包装扩展方法。通过这种方式,IDE 可以识别这些扩展方法并提供智能提示。

创建一个名为
ComboBoxExtensions.py
的文件,其中定义扩展方法。

#combo_box_extensions.py
from typing importDictimportwxclassComboBoxExtensions:
@staticmethod
def bind_dict_items(combo_box: wx.ComboBox, items_dict: Dict[str, str]) ->None:"""Bind a dictionary of items to the ComboBox."""combo_box.Clear()#清空现有项 for key, value initems_dict.items():
combo_box.Append(value, key)
#添加项,key 为用户数据,value 为显示内容

在主应用程序中,导入扩展类并使用其方法。

importwxfrom combo_box_extensions import ComboBoxExtensions  #导入扩展类

classMyApp(wx.App):defOnInit(self):
frame
= wx.Frame(None, title="ComboBox Extensions Example", size=(300, 200))
panel
=wx.Panel(frame)

combo_box
= wx.ComboBox(panel, style=wx.CB_DROPDOWN, pos=(10, 10))#使用扩展方法绑定字典项 items ={"item1": "Item One","item2": "Item Two","item3": "Item Three"}
ComboBoxExtensions.bind_dict_items(combo_box, items)
#这里应该有智能提示 frame.Show()returnTrueif __name__ == "__main__":
app
=MyApp()
app.MainLoop()

ComboBoxExtensions
类包含一个静态方法
bind_dict_items
,该方法接受
wx.ComboBox
实例和字典作为参数。

在主应用程序中,调用
ComboBoxExtensions.bind_dict_items(combo_box, items)
,这将获得智能提示。

4)使用协议类型的方式处理,并在使用的时候转换为协议类

为了确保在不继承的情况下实现扩展方法并获得智能提示,最佳方案是结合类型注解和一个特定的函数注册过程。以下是一个经过验证的方式,确保能够在实例上调用扩展方法,同时获得 IDE 的智能提示。


combo_box_extensions.py
中定义扩展函数,并使用
cast
来确保类型正确。

#combo_box_extensions.py
from typing importDict, Protocolimportwxfrom typing importcastclassComboBoxWithBindDictItems(Protocol):def BindDictItems(self, items_dict: Dict[str, str]) ->None:
...
def bind_dict_items(self: wx.ComboBox, items_dict: Dict[str, str]) ->None:"""Bind a dictionary of items to the ComboBox."""self.Clear()#清空现有项 for key, value initems_dict.items():
self.Append(value, key)
#添加项,key 为用户数据,value 为显示内容 #将扩展方法绑定到 wx.ComboBox wx.ComboBox.BindDictItems = bind_dict_items

在主应用程序中调用扩展方法,并确保正确使用类型注解。

importwxfrom combo_box_extensions import ComboBoxWithBindDictItems  #导入协议类型

classMyApp(wx.App):defOnInit(self):
frame
= wx.Frame(None, title="ComboBox Extensions Example", size=(300, 200))
panel
=wx.Panel(frame)

combo_box
= wx.ComboBox(panel, style=wx.CB_DROPDOWN, pos=(10, 10))#确保类型为 ComboBoxWithBindDictItems,以获得智能提示 cast(ComboBoxWithBindDictItems, combo_box).BindDictItems({"item1": "Item One","item2": "Item Two","item3": "Item Three"})#这里应该有智能提示 frame.Show()returnTrueif __name__ == "__main__":
app
=MyApp()
app.MainLoop()

可以看到,通过cast的方式转换后,具有函数代码的智能提示了。

协议类型
:定义
ComboBoxWithBindDictItems
,它确保
BindDictItems
方法存在。

使用
cast

:在调用
BindDictItems
方法时,使用
cast
来明确指定
combo_box
的类型为
ComboBoxWithBindDictItems
,这样 IDE 能够识别并提供智能提示。

智能提示
:通过类型注解和
cast
,IDE 能够识别扩展方法并提供智能提示。

无继承
:避免了复杂的继承结构,同时实现了功能扩展。

以上几种在Python开发中,对于实现C#扩展函数方法的实现,不过总体来说,虽然能够实现类似的方式,却没有C#那种简洁明了,不知道以后Python发展后解决或者是我没有研究透彻的原因,很多时候如果要实现自定义函数的处理方式,估计我只能结合子类继承和辅助类的方式一起解决这个问题。

前言

对于大多数.NET后端开发者而言,ABP框架已经相当熟悉,可以轻松进行二次开发,无需重复实现用户角色管理、权限控制、组织管理和多租户等功能。

然而,ABP框架主要专注于Web应用,对于桌面端和移动设备的支持较为有限。因此,对于有桌面或移动开发需求的开发者来说,可能需要寻找其他解决方案。

给大家推荐一款基于ABP商业版全面开发的WPF框架,它实现了ABP商业版的所有功能,并提供了WPF及Xamarin.Forms版本,支持完整的跨平台应用开发。

框架介绍

WPF ABP框架不仅仅是简单地将ABP技术应用于WPF项目,而是全面还原了ABP框架所提供的业务功能,同时在WPF项目中移除了ABP的启动配置、模块系统、依赖注入以及反射加载和自动实体映射等功能。

本项目采用Prism MVVM框架进行重构,UI部分则使用了Syncfusion的WPF组件。

另外,该框架兼容多种UI框架,包括Syncfusion、HandyControl和MaterialDesign。

框架功能

该套框架包含用户和角色管理、组织机构管理、权限管理、多租户支持、本地化多语言、身份认证及授权、审计日志记录、UI主题定制、异常处理机制、数据字典管理和系统设置功能。

框架说明

由于ABP官方尚未提供完整的WPF框架,因此本套框架应运而生,填补了市场空白。如果你是使用ABP框架的开发者,可以毫不费力地将本套系统集成到你们的业务中。

另外,ABP商业版提供的Xamarin.Forms框架较为简陋,因此本套框架也包含了完整的Xamarin.Forms版本。

项目效果

下面通过一些实际运行的截图来展示效果, 包含桌面端(WPF)以及移动端(Xamarin.Forms)效果图。

1、登录页面

包含切换租户、语言切换、修改密码、邮箱激活。

2、系统首页

包含系统菜单、主题切换(深色/浅色主题)、首页数据统计面板。

3、组织机构

维护组织信息, 添加不同的角色和用户。

4、角色管理

维护角色信息, 设定角色权限,根据权限筛选不同的角色。

5、用户管理

管理用户信息, 需改用户权限, 锁定/解锁/删除用户。

6、审计日志

系统的请求日志、错误日志、异常数据、更改日志信息记录。

7、动态属性

设置动态数据, 下拉列表、选择性、多选项等。

8、多租户

维护租户信息

9、语言列表

维护多语言的数据, 修改/设定/维护相关信息

10、设置

包含系统的核心功能的设置, 包含租户、用户、系统安全、邮箱、发票、其它设置。

11、UI组件

包含了一些常用的控件演示

12、系统主题切换

Xamarin.Forms

项目地址

GitHub:
https://github.com/HenJigg/wpf-abp

视频演示:
https://www.bilibili.com/video/BV1Av4y1w7ds?spm_id_from=333.999.0.0

总结

本文只展示部分功能和内容,如有需求访问项目地址获取详细信息。希望本文能在WPF开发方面为各位提供有益的帮助。期待大家在评论区留言交流,分享您的宝贵经验和建议。

最后

如果你觉得这篇文章对你有帮助,不妨点个赞支持一下!你的支持是我继续分享知识的动力。如果有任何疑问或需要进一步的帮助,欢迎随时留言。

也可以加入微信公众号
[DotNet技术匠]
社区,与其他热爱技术的同行一起交流心得,共同成长!
优秀是一种习惯,欢迎大家留言学习!

GitHub 仓库链接

https://github.com/mattn/anko

1.
anko
是干嘛用的?

anko
是一个可以让 Go 项目支持脚本语言的小工具。换句话说,就是我们可以给 Go 项目加点“脚本魔法”,在程序跑起来之后还能动态地改代码逻辑。比如,你在写一个应用,想让用户可以随时调整设置或控制程序的某些行为,而不需要每次都去改代码重新编译,这时候就可以用
anko

2. 为什么会用到
anko

有时候我们的项目需要
灵活一点
。比如:

  • 做一个游戏,想让用户自己定义规则。
  • 写一个自动化脚本,想让用户随时调整参数。
  • 做后台管理工具,管理员可以直接在网页上写脚本来控制一些业务流程。

如果这些逻辑写在代码里,就得不停地改代码重启服务。而用
anko
就可以把这些逻辑写成脚本,用户想怎么改就怎么改,还不用重启,轻松方便。


3. 如何开始使用
anko

anko
安装起来也很简单,只需要几行代码就能用上。首先安装:

go get github.com/mattn/anko

然后我们在代码里引入:

import "github.com/mattn/anko/vm"


4. 让
anko
跑起来

假设我们有一段小脚本,想动态地执行它。下面是一个简单的例子:

package main

import (
    "fmt"
    "github.com/mattn/anko/vm"
)

func main() {
    env := vm.NewEnv() // 创建一个新的脚本环境

    // 写一段小脚本代码
    script := `
x = 5
y = 10
z = x + y
z
`

    // 执行脚本
    result, err := env.Execute(script)
    if err != nil {
        fmt.Println("出错了:", err)
    } else {
        fmt.Println("脚本运行结果:", result) // 输出:15
    }
}


5. 用 Go 变量控制脚本

如果想在脚本里使用 Go 程序的变量,可以用
Define
方法定义变量,然后在脚本里直接用。例如:

env := vm.NewEnv()
env.Define("name", "Anko") // 在脚本里定义 name 变量

script := `
"Hello, " + name + "!"
`

result, err := env.Execute(script)
fmt.Println(result) // 输出 "Hello, Anko!"


6. 让脚本调用 Go 函数

不仅可以传变量,还可以把 Go 的函数给脚本用。举个例子,假如我们有个打招呼的函数
greet

package main

import (
    "fmt"
    "github.com/mattn/anko/vm"
)

func greet(name string) string {
    return "Hello, " + name
}

func main() {
    env := vm.NewEnv()
    env.Define("greet", greet) // 把 greet 函数传给脚本

    script := `
greet("Anko")
`

    result, err := env.Execute(script)
    fmt.Println(result) // 输出 "Hello, Anko"
}


7. 用
anko
实现简单的逻辑

anko
也支持一些基本的控制语句,比如
if

for

script := `
sum = 0
for i = 1; i <= 5; i++ {
    sum += i
}
sum
`

result, _ := env.Execute(script)
fmt.Println("Sum is:", result) // 输出 15


8.
anko
的优缺点

优点

  • 灵活
    :可以在不重启程序的情况下改代码逻辑,非常适合需要频繁调整规则或逻辑的场景。
  • 易于集成
    :可以直接把 Go 的函数和变量传递给脚本,让脚本和 Go 程序无缝结合。
  • 语法简单
    :大多数人可以快速上手,用 Go 写代码的同学用这个库没啥学习成本。

缺点

  • 性能限制
    :解释器相对慢一些,不适合执行复杂、频繁的计算任务。
  • 功能不如高级脚本语言
    :没有像 JavaScript 或 Python 那么强大的功能,主要适合轻量级的动态任务。


9.
anko
适合哪些场景?

  • 动态配置
    :比如管理系统里定义一些规则,不用每次都改代码。
  • 业务规则引擎
    :很多应用需要灵活配置规则,
    anko
    是一个轻量级的选择。
  • 自动化脚本
    :运行一些自动化任务,允许用户在界面里直接编写脚本控制任务。


总结

anko
是一个让 Go 支持脚本的好工具。它的轻量、灵活和简单特性,让我们可以在 Go 应用里嵌入脚本语言,用户可以自由定义一些规则或逻辑,非常适合后台管理、自动化任务、游戏规则等应用场景。