万字+28张图带你探秘小而美的规则引擎框架LiteFlow

图片

大家好,今天给大家介绍一款轻量、快速、稳定可编排的组件式规则引擎框架LiteFlow。

一、LiteFlow的介绍

前言

在每个公司的系统中,总有一些拥有复杂业务逻辑的系统,这些系统承载着核心业务逻辑,几乎每个需求都和这些核心业务有关,这些核心业务业务逻辑冗长,涉及内部逻辑运算,缓存操作,持久化操作,外部资源调取,内部其他系统RPC调用等等。时间一长,项目几经易手,维护的成本就会越来越高。各种硬代码判断,分支条件越来越多。代码的抽象,复用率也越来越低,各个模块之间的耦合度很高。一小段逻辑的变动,会影响到其他模块,需要进行完整回归测试来验证。如要灵活改变业务流程的顺序,则要进行代码大改动进行抽象,重新写方法。实时热变更业务流程,几乎很难实现。

LiteFlow框架的作用

LiteFlow就是为解耦复杂逻辑而生,如果你要对复杂业务逻辑进行新写或者重构,用LiteFlow最合适不过。它是一个轻量,快速的组件式流程引擎框架,组件编排,帮助解耦业务代码,让每一个业务片段都是一个组件,并支持热加载规则配置,实现即时修改。

使用LiteFlow,你需要去把复杂的业务逻辑按代码片段拆分成一个个小组件,并定义一个规则流程配置。这样,所有的组件,就能按照你的规则配置去进行复杂的流转。

LiteFlow的设计原则

LiteFlow是基于工作台模式进行设计的,何谓工作台模式?

n个工人按照一定顺序围着一张工作台,按顺序各自生产零件,生产的零件最终能组装成一个机器,每个工人只需要完成自己手中零件的生产,而无需知道其他工人生产的内容。每一个工人生产所需要的资源都从工作台上拿取,如果工作台上有生产所必须的资源,则就进行生产,若是没有,就等到有这个资源。每个工人所做好的零件,也都放在工作台上。

这个模式有几个好处:

  • 每个工人无需和其他工人进行沟通。工人只需要关心自己的工作内容和工作台上的资源。这样就做到了每个工人之间的解耦和无差异性。

  • 即便是工人之间调换位置,工人的工作内容和关心的资源没有任何变化。这样就保证了每个工人的稳定性。

  • 如果是指派某个工人去其他的工作台,工人的工作内容和需要的资源依旧没有任何变化,这样就做到了工人的可复用性。

  • 因为每个工人不需要和其他工人沟通,所以可以在生产任务进行时进行实时工位更改:替换,插入,撤掉一些工人,这样生产任务也能实时地被更改。这样就保证了整个生产任务的灵活性。

这个模式映射到LiteFlow框架里,工人就是组件,工人坐的顺序就是流程配置,工作台就是上下文,资源就是参数,最终组装的这个机器就是这个业务。正因为有这些特性,所以LiteFlow能做到统一解耦的组件和灵活的装配。

二、LiteFlow的使用

1)非Spring环境下

引入pom依赖

<dependency>   <groupId>com.yomahub</groupId>   <artifactId>liteflow-core</artifactId>   <version>2.6.13</version></dependency>

第一步构建自己的业务Node,也就是继承NodeComponent,重写process方法,业务执行的过程中,会调用process来执行节点的业务。

图片

我这里写了三个

图片

然后编写xml文件,直接放在resources底下

图片

图片

<nodes/>配置了每个业务的节点,这里配置了我们写的那几个,<chain/>标签代表了每一个业务的执行流程,配置了<when/>和<then/>标签,然后value标签设置了上面配置的<node/>的id,至于为什么这么配置,后面会解析。

然后执行这个demo

图片

构建了一个LiteflowConfig,传入xml的路径,然后构建FlowExecutor,最后调用FlowExecutor的execute2Resp,传入需要执行的业务流程名字 chain1 ,就是xml中配置的,执行业务流程。

结果

图片

如果业务变动,现在不需要执行B流程了,那么直接修改规则文件就行了,如图。

图片

运行结果

图片

这里发现B就没执行了。

2)SpringBoot环境下

引入pom依赖

<dependency>   <groupId>com.yomahub</groupId>   <artifactId>liteflow-spring-boot-starter</artifactId>   <version>2.6.13</version></dependency>

构建自己的业务Node,只不过在Spring的环境底下,可以不需要在xml配置<node/>标签,直接使用@LiteflowComponent注解即可

图片

xml中没有声明<node/>标签

图片

application.properties中配置xml文件的路径

图片

测试代码

图片

执行结果

图片

跟非spring的环境结果一致。

如果有想要获取demo的小伙伴在微信公众号后台回复 LiteFlow 即可获取。

通过上面的例子我们可以看出,其实每个业务节点之间是没有耦合的,用户只需要按照一定的业务规则配置节点的执行顺序,LiteFlow就能实现业务的执行。

三、LiteFlow核心组件讲解

讲解核心组件的时候如果有什么不是太明白的,可以继续往下看,后面会有源码解析。

下图为LiteFlow整体架构图

图片

1)Parser

这个组件的作用就是用来解析流程配置的规则,也就是将你配置的规则文件解析成Java代码来运行。支持的文件格式有xml、json、yaml,其实不论是什么格式,只是形式的不同,用户可根据自身配置的习惯来选择规则文件的格式。

同时,规则文件的存储目前官方支持基于zk或者本地文件的形式,同时也支持自定义的形式。

对于xml来说,Parser会将<node/>标签解析成Node对象,将<chain/>解析成Chain对象,将<chain/>内部的比如<when/>、<then/>等标签都会解析成Condition对象。

如下图所示。

图片

  • Node其实就是代表了你具体业务执行的节点,就是真正的业务是在Node中执行的

  • Condition可以理解为一种条件,比如前置条件,后置条件,里面一个Condition可以包含许多需要执行的Node

  • Chain可以理解成整个业务执行的流程,按照一定的顺序来执行Condition中的Node也就是业务节点

Condition和Node的关系

图片

Condition分为以下几种

  • PreCondition:在整个业务执行前执行,就是前置的作用

  • ThenCondition:内部的Node是串行执行的

  • WhenCondition:内部的Node是并行执行的

  • FinallyCondition:当前面的Condition中的Node都执行完成之后,就会执行这个Condition中的Node节点

Chain和Condition的关系

图片

Chain内部其实就是封装了一堆Condition,Chain的执行就是指从不同的Condition中拿出里面的Node来执行,首先会拿出来PreCondition中的Node节点来执行,执行完之后会执行ThenCondition和WhenCondition中的Node节点,最后执行完之后才会执行FinallyCondition中的Node节点。

2)FlowBus

这个组件主要是用来存储上一步骤解析出来的Node和Chain的

图片

3)FlowExecutor

这个其实是用来执行上面解析出来的业务流程,从FlowBus找到需要执行的业务流程Chain,然后执行Chain,也就是按照Condition的顺序来分别执行每个Condition的Node,也就是业务节点。

图片

4)Slot

Slot可以理解为业务的上下文,在一个业务流程中,这个Slot是共享的。

Slot有个默认的实现DefaultSlot,DefaultSlot虽然可以用,但是在实际业务中,用这个会存在大量的弱类型,存取数据的时候都要进行强转,颇为不方便。所以官方建议自己去实现自己的Slot,可以继承AbsSlot。

图片

5)DataBus

用来管理Slot的,从这里面可以获取当前业务流程执行的Slot。

四、LiteFlow源码探究

说完核心的组件,接下来就来剖析一下源码,来看一看LiteFlow到底是如何实现规则编排的。

1)FlowExecutor的构造流程

我们这里就以非Spring环境的例子来说,因为在SpringBoot环境底下,FlowExecutor是由Spring创建的,但是创建的过程跟非Spring的例子是一样的。

图片

这里在构建FlowExecutor,传入了一个规则的路径flow.xml,也就是ruleSource属性值。

进入loadInstance这个方法,其实就是直接new了一个FlowExecutor。

图片

进入FlowExecutor构造方法,前面就是简单的赋值操作。然后调用liteflowConfig.isParseOnStart(),这个方法默认是返回true的,接下来会调用init方法,也就是在启动时,就去解析规则文件,保证运行时的效率。

接下来进入init方法。

图片

init方法非常长,来一步一步解析

前面就是校验,不用care

List<String> sourceRulePathList = Lists.newArrayList(liteflowConfig.getRuleSource().split(",|;"));

这行代码的意思就是将我们传入的规则文件路径进行分割成多个路径,从这可以看出支持配置多个规则的文件。对我们这个demo来说其实就是只有一个,那就是flow.xml。

分割完之后,就会遍历每个路径,然后判断文件的格式,比如xml、json、yaml,然后根据文件格式找到对应的FlowParser。

随后根据liteflowConfig.isSupportMultipleType()判断是不是支持多类型的,什么叫多类型,就是指规则文件配置了多个并且文件的格式不同,如果支持的话,需要每个规则文件单独去解析,如果不支持,那就说明文件的格式一定是相同的,相同可以在最后统一解析,解析是通过调用FlowParser的parseMain来解析的。

剖析完之后整个init方法就会结束,然后继续调用DataBus的init方法,其实就是初始化DataBus。

到这其实构建FlowExecutor就完成了,从上面我们得出一个结论,那就是在构造FlowExecutor的时候会通过FlowParser的parseMain来处理对应规则文件的路径,所以接下来我们分析一下这个FlowParser是如何解析xml的,并且解析了之后干了什么。

2)FlowParser规则解析流程

接下来我们进入FlowParser来看看一个是如何解析规则的。

以本文的例子为例,因为是配置本地的xml文件,找到的FlowParser的实现是LocalXmlFlowParser。

图片

接下会调用parseMain方法,parseMain的方法的实现很简单,首先根据PathContentParserHolder拿到一个PathContentParser来解析路径,对上面案例来说,就是flow.xml路径,拿到路径对应文件的内容,其实就是拿到了flow.xml内容。然后调用父类的parse方法来解析xml的内容,所以parse方法才是解析xml的核心方法。

这里有个细节说一下,PathContentParserHolder其实内部使用了Java的SPI机制来加载PathContentParser的实现,然后解析路径,拿到内容,在Spring环境中默认基于Spring的实现的优先级高点,但是不论是怎么实现,作用都是一样的,那就是拿到路径对应的xml文件的内容,这里就不继续研究PathContentParser是如何加载文件的源码了。

其实不光是PathContentParser,LiteFlow内部使用了很多SPI机制,但是基本上整合Spring的实现的优先级都高于框架本身的实现。

接下来我们就来看一下LocalXmlFlowParser父类中的parse方法的实现。

图片

首先遍历每个文件中的内容,然后转成Document,Document其实是dom4j的包,其实就是将xml转成Java对象,这样可以通过Java中的方法来获取xml中每个标签的数据。

将文件都转换成Document之后,调用parseDocument方法。

图片


首先调用了ContextCmpInitHolder.loadContextCmpInit().initCmp() ,这行代码也是通过SPI机制来加载ContextCmpInit,调用initCmp方法。框架本身对于initCmp的实现是空实现,但是在Spring环境中,主要是用来整合Spring中的Node节点的,将Node节点添加到FlowBus中,这也是为什么在Spring环境中的那个案例中不需要在xml文件中配置<nodes/>的原因,因为LiteFlow会自动识别这些Node节点的Spring Bean。至于怎么整合Spring的,有兴趣的同学可以看一下ComponentScanner类的实现,主要在Bean初始化之后进行判断的,这里画一张图来总结一下initCmp方法的作用。

图片

至于为什么需要先将Spring中的Node节点添加到FlowBus,其实很简单,主要是因为构建Chain是需要Node,需要保证构建Chain之前,Spring中的Node节点都已经添加到了FlowBus中。

接下来就会继续遍历每个Document,也就是每个xml,然后拿到解析<nodes></nodes>中的每个<node></node>标签,拿出每个node标签中的属性,通过LiteFlowNodeBuilder构建Node,然后放入到FlowBus中,至于如何放入到FlowBus中,可以看一下LiteFlowNodeBuilder的build方法的实现。

图片

解析完Node之后,接下来就是解析<chain/>标签,拿到每一个<chain/>标签对应的Element之后,调用parseOneChain来解析<chain/>标签的内容。

图片

parseOneChain方法,先拿到<chain/>底下所有的标签,然后判断标签类型,标签的类型主要有四种类型:then、when、pre、finally,然后拿到每个标签的值,构建对应的Condition,就是上文提到的ThenCondition、WhenCondition、PreCondition、FinallyCondition,然后加入到Chain中,至于如何将Node设置到Condition中,主要是通过LiteFlowConditionBuilder的setValue方法来实现的,setValue这个方式设置的值是条件标签的value属性值,然后解析value属性值,然后从FlowBus中clone一个新的Node,加入到Condition中,至于为什么需要clone一下新的Node,因为同一个业务节点,可能在不同的执行链中,为了保证不同业务中的同一个业务节点不相互干扰,所以得重新clone一个新的Node对象。

构建好Condition之后,都设置到了对应的Chain中,最后将Chain添加到FlowBus中。

图片

到这里,其实整个xml就解析完了,FlowParser的最主要的作用就是解析xml,根据配置构建Node、Condition和Chain对象,有了这些基础的组件之后,后面才能运行业务流程。其实从这里也可以看出是如何流程编排的,其实就是根据配置,将一个个Node添加到Condition中,Condition再添加到Chain中,这样相同的业务节点,可能分布在不同的Chain中,这样就实现了业务代码的复用和流程的编排。

3)Chain的执行流程

剖析完FlowParser的作用,也就是Node和Chain的构造流程之后,接下来看一下Chain是如何执行的。

流程执行是通过FlowExecutor来执行的,FlowExecutor执行的方法很多,我们以上面demo调用的execute2Resp为例,最终会走到如下图的重载方法。

图片

execute2Resp方法就会调用doExecute方法的实现,然后拿到Slot,封装成一个LiteflowResponse返回回去,所以从这里可以看出,doExecute是核心方法。

接下来看看doExecute方法的实现。

图片

图片

doExecute方法比较长,我截了两张图

首先从DataBus中获取一个Slot,也就是当前业务执行的上下文。之后从FlowBus中获取需要执行的Chain,最后分别调用了Chain的executePre、execute、executeFinally方法,其实不用看也知道这些方法干了什么,其实就是调用不同的Condition中Node方法。

executePre和executeFinally方法

图片

这两个方法最后调用的是同一个方法,就是分别找到PreCondition和FinallyCondition,取出里面的Node节点,执行excute方法。

这里有重点说明一下,其实在Condition中存的不是直接的Node,而是Executable,Executable的有两个实现,一个就是我们所说的Node,还有一个就是我们一直说的Chain,为了方便大家理解,我一直说的是Node,其实这里的Executable是有可能为Chain的,取决于规则的配置。当是一个Chain的时候,其实就是一个嵌套的子流程,也就是在一个流程中嵌套另一个流程的意思,大家注意一下就行了,其实不论怎么嵌套,流程执行到最后一定是Node,因为如果是Chain,那么还会继续执行,不会停止,只有最后一个流程的Executable都是Node的时候流程才能执行完。

executePre和executeFinally方法说完之后,看一看execute方法的实现。

图片

execute方法主要是判断Condition的类型,然后判断是ThenCondition还是WhenCondition,ThenCondition的话其实也就是拿出Node直接执行,如果是WhenCondition的话,其实就是并行执行每个Node节点。这也是ThenCondition和WhenCondition的主要区别。

画图总结一下Chain的执行流程

图片

4)Node的执行流程

从上面我们可以看出,Chain的执行其实最终都是交给Node来执行的,只不过是不同阶段调用不同的Node而已,其实最终也就是会调用Node的execute方法,所以我们就来着重看一下Node的execute方法。

图片

instance就是NodeComponent对象,也就是我们自定义实现的节点对象,好家伙,终于要执行到业务了。有人可能好奇NodeComponent是如何设置到Node对象中的,其实就是在往FlowBus添加Node的时候设置的,不清楚的小伙伴可以翻一下那块相关的源码,在解析xml那块我有说过。

先调用NodeComponent的isAccess方法来判断业务要不要执行,默认是true,你可以重写这个方法,自己根据其它节点执行的情况来判断当前业务的节点要不要执行,因为Slot是公共的,每个业务节点的执行结果可以放在Slot中。

随后通过这个方法获取了NodeExecutor,NodeExecutor可以通过execute方法来执行NodeComponent的,也就是来执行业务的,NodeExecutor默认是使用DefaultNodeExecutor子类的,当然你也可以自定义NodeExecutor来执行NodeComponent

NodeExecutor nodeExecutor = NodeExecutorHelper.loadInstance().buildNodeExecutor(instance.getNodeExecutorClass());

DefaultNodeExecutor的execute方法也是直接调用父类NodeExecutor的execute方法,接下来我们来看一下NodeExecutor的execute方法。

图片

从这个方法的实现我们可以看出,LiteFlow对于业务的执行是支持重试功能的,但是不论怎么重试,最终一定调用的是NodeComponent的execute方法。

进入NodeComponent的execute方法

图片

红框圈出来的,就是核心代码,self是一个变量,指的是当前这个NodeComponent对象,所以就直接调用当前这个NodeComponent的process方法,也就是用来执行业务的方法。

在执行NodeComponent的process方法前后其实有回调的,也就是可以实现拦截的效果,在Spring环境中会生效。

至于这里为什么要使用self变量而不是直接使用this,其实源码也有注释,简单点说就是如果process方法被动态代理了,那么直接使用this的话,动态代理会不生效,所以为了防止动态代理不生效,就单独使用了self变量来引用自己。至于为什么不生效,这是属于Spring的范畴了,这里就不过多赘述了。

其实到这里,一个Node就执行完成了,Node的执行其实就是在执行NodeComponent,而NodeComponent其实最终是交给NodeExecutor来执行的。

图片

每个Condition中的Node执行完之后,就将Slot返回,这样就能在调用方就能通过Slot拿到整个流程的执行结果了。

到这里,其实核心流程源码剖析就完成了,总的来说就是将规则配置文件翻译成代码,生成Node和Chain,然后通过调用Chain来执行业务流程,最终其实就是执行我们实现的NodeComponent的process方法。

最终画一张图来总结整个核心源码。

图片

图中我省略了Condition的示意图,因为Condition其实最终也是执行Node的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/194666.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Java】IDEA 基本操作

0.IDEA 0.1 IDEA中的层级结构 0.1.1 结构分类 project&#xff08;项目、工程&#xff09;module&#xff08;模块&#xff09;package&#xff08;包&#xff09;class&#xff08;类&#xff09; 0.1.2 结构介绍 project&#xff08;项目、工程&#xff09; ​ 淘宝、京…

CANdelaStudio 中 Bese Variant 和 Variant区别

关于 Bese Variant &#xff0c;其在 CDDT 和 CDD 文件中都存在&#xff0c;有且只有一个 主要包含三部分&#xff0c;重点只关注 DIDs 和 Supported Diagnostic Classes 而在 CDD 文件中&#xff0c;除了 Bese Variant 外&#xff0c;还有一个 Variant “Variant” 这个概…

微服务--01--简介、服务拆分原则

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 微服务微服务架构&#xff0c;是服务化思想指导下的一套最佳实践架构方案。服务化&#xff0c;就是把单体架构中的功能模块拆分为多个独立项目。 单体架构微服务架构…

Unity Meta Quest 一体机开发(八):实现 Hand Grab 扔物体功能

文章目录 &#x1f4d5;教程说明&#x1f4d5;设置刚体和碰撞体&#x1f4d5;给物体添加 Physics Grabbable 脚本&#x1f4d5;给手部添加 Hand Velocity Calculator 物体 此教程相关的详细教案&#xff0c;文档&#xff0c;思维导图和工程文件会放入 Seed XR 社区。这是一个高…

二叉树前序、中序以及后序遍历(递归展开图)

目录 1.二叉树前置说明 2.前序遍历 2.1函数实现 2.2递归展开图 3.中序遍历 3.1函数实现 3.2递归展开图 4.后序遍历 4.1函数实现 4.2递归展开图 1.二叉树前置说明 在学习二叉树的基本操作前&#xff0c;需先要创建一棵二叉树&#xff0c;然后才能学习其相关的基本操作…

如何通过信息化为燃气管道提供数据监控、智能的调度和作业技术?

关键词&#xff1a;智慧燃气、燃气监控、智慧管网、智慧燃气管网、智慧燃气管网解决方案、城市燃气管网 在信息化迅猛发展的历史潮流中&#xff0c;如何通过信息化为燃气管道提供更广泛的数据监控、更紧密的数据集成、更智能的调度和作业、更智慧的分析和决策&#xff0c;为安…

干法制程中的辉光放电

在芯片制程中&#xff0c;几乎所有的干法制程&#xff0c;如PVD&#xff0c;CVD,干法刻蚀等&#xff0c;都逃不过辉光放电现象。辉光放电&#xff0c;是一种在低压下电离气体的过程&#xff0c;它在半导体制程中的许多重要步骤中有着核心作用。那您知道什么是“启辉”吗&#x…

UE4/UE5 c++绘制编辑器场景直方图(源码包含场景中的像素获取、菜单添加ToolBar)

UE4/UE5 c场景直方图 UE4/UE5 C绘制编辑器场景直方图绘制原理&#xff1a;元素绘制坐标轴绘制 源码处理 UE4/UE5 C绘制编辑器场景直方图 注&#xff1a;源码包含场景中的像素获取、菜单添加ToolBar 实现效果&#xff1a; 这个是用于美术统计场景中像素元素分布&#xff0c;类…

【100个Cocos实例】编码不规范,接手泪两行...

点击上方亿元程序员关注和★星标。 引言 规范编码&#xff0c;从文件头部注释规范做起。 头部注释规范是一种在代码文件开头添加注释信息的做法&#xff0c;通常用于描述文件的基本信息、作者、创建日期、修改历史等。 这有助于团队成员更好地理解和维护代码。 本文将介绍一…

JVM执行引擎

目录 &#xff08;一&#xff09;执行引擎概述 &#xff08;二&#xff09;Java代码编译和执行过程 &#xff08;三&#xff09;机器码&#xff0c;指令&#xff0c;汇编语言&#xff0c;字节码 1、机器码 2、指令 3、指令集 4、汇编 5、字节码 &#xff08;四&#x…

一、Oceanbase基础

一、集群相关概念 集群&#xff1a;整个分布式数据库。Region&#xff1a;表示区域&#xff0c;是地域的逻辑概念&#xff0c;如1个城市&#xff0c;1个集群可以有多个Region&#xff0c;用于跨城市远 距离容灾。Zone&#xff1a;表示分区&#xff0c;是机房或机架的逻辑概念…

深度学习【二】

1.运行时错误 1.1 ModuleNotFoundError: No module named ‘torch_scatter’ 参考 https://blog.csdn.net/weixin_42421914/article/details/132875571 pip install --no-index torch-scatter -f https://pytorch-geometric.com/whl/torch-1.13.1%2Bcpu.html

某思路等考通一级MSOffice的分析

看到有朋友寻求2021版的等级考试一级软件&#xff0c;秉承授人以鱼不如授人以渔的理念&#xff0c;特写这个帖子。 某思路等考通一级MSOffice&#xff0c;版本6.5。 用到的软件&#xff0c;ScanId&#xff0c;de4dot,dnSpy。 第一步&#xff1a;分析 软件启动后有在线激活提示&…

华为云CDN刷新与查询余量的Go实现及在Jenkins中的部署

引言 在华为云上&#xff0c;对CDN缓存内容进行刷新是一个常见的需求&#xff0c;以确保最新的内容能尽快被用户访问到。通过使用Go语言&#xff0c;我们可以开发一个自动化的工具来实现这一需求&#xff0c;并将其集成到Jenkins中以实现持续部署。下面我们将分步骤讲解如何实…

MySQL递归查询:洞悉数据的层层关联

在处理关系型数据库时&#xff0c;我们经常会遇到这样的情况&#xff1a;某些数据之间存在层级关系&#xff0c;例如目录、组织结构、评论等。在这些场景下&#xff0c;我们需要一种灵活的查询技术来处理这种层级关系。今天我们就来探讨MySQL中的递归查询&#xff0c;体验其独特…

ThinkPHP6学生选课管理系统

有需要请加文章底部Q哦 可远程调试 ThinkPHP6学生选课管理系统 一 介绍 此学生选课管理系统基于ThinkPHP6框架开发&#xff0c;数据库mysql8&#xff0c;前端bootstrap。系统角色分为学生&#xff0c;教师和管理员。学生登录后可进行选课&#xff0c;教师登录后可查看选课情况…

Android : 获取、添加、手机联系人-ContentResolver简单应用

示例图&#xff1a; MainActivity.java package com.example.mygetdata;import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat;import android.Mani…

图书管理系统源码,图书管理系统开发,图书借阅系统源码四TuShuManager应用程序MVC视图View

Asp.net web应用程序MVC之View视图 .ASP.NET MVC页面也就是要说的视图基本被放在Views文件夹下&#xff1b; 2.利用APS.NET MVC模板生成框架&#xff0c;Views文件夹下的默认页面为.cshtml页面&#xff1b; 3.ASP.NET MVC默认页面为Razor格式的页面&#xff0c;因此默认页面为.…

三、Lua变量

文章目录 一、变量分类二、变量赋值三、索引 一、变量分类 lua变量分为全局变量&#xff0c;局部变量。 全局变量&#xff1a;默认&#xff0c;全局有效。 局部变量&#xff1a;从作用范围开始到作用范围结束&#xff0c;需加local 修饰。 a1function ff()local b1 endprint(a…

spring boot的redis连接数过多导致redis服务器压力过大的一次问题排查

一、背景 在今天上午的时候&#xff0c;突然收到大量的sentry报错&#xff0c;都是关于redis连接超时的警告。 首先想到的是去查看redis的监控&#xff0c;发现那个时间段&#xff0c;redis的请求数剧增&#xff0c;cpu使用率和带宽都陡增双倍。 下面的是redis监控的cpu情况 …