写点什么

Nop 平台与 SpringCloud 的功能对比

作者:canonical
  • 2023-09-17
    北京
  • 本文字数:7772 字

    阅读完需:约 25 分钟

Nop 平台是根据可逆计算原理从零开始设计并实现的新一代的低代码平台,它的目标并不是针对少数固化的场景提供预置的开发脚手架和可视化设计工具,而是打破描述式编程和传统命令式编程之间人为制造的藩篱,建立两者无缝相容的一种新的编程范式。不断扩大描述式编程所覆盖的语义空间。为了以最低的技术成本达到这一目标,Nop 平台没有采用目前业内主流的基础开源框架,而是选择了基于可逆计算原理重塑整个技术体系。本文将简单列举一下 Nop 平台所造的轮子,并与 SpringCloud 技术体系中现有的轮子做个对比。



Nop 平台可以直接作为类似 SpringCloud 的基础开发框架来使用,在 ProCode 层面也可以极大简化软件开发过程,并明显提升软件的可扩展性。

一. IoC 容器

声明式的 IoC 容器是 Spring 发家时的基本技能。但是自从 Spring2.0 版本之后,SpringIoC 就逐渐失去了它的声明式特性,导致现在它的执行掺杂了大量命令式逻辑,典型的体现就是:改变 Spring 中包扫描的顺序会以微妙的方式改变 bean 装配的结果


NopIoC 是在 Spring1.0 的装配语法的基础上补充了类似 SpringBoot 的条件装配逻辑,最终运行时完全归约为 Spring1.0 的语法来执行。beans 文件的装载顺序以及 bean 的声明顺序不会影响到最终装配的结果


@ConditionOnMissingBean@ConditionOnProperty("test.my-bean.enabled")@Componentpublic class MyBean{    @Inject    OtherBean other;}
复制代码


对应于 NopIoC 的如下配置


<bean id="myBean" ioc:default="true" class="test.MyBean">  <ioc:condition>     <if-property name="test.my-bean.enabled" />  </ioc:condition></bean>
复制代码


NopIoC 支持@Inject,@PostConstruct, @PreDestroy等规范化注解,也兼容 Spring 特有的@Autowired注解。但是它不使用包扫描来发现 bean 的定义,而是要求在 beans.xml 文件中声明 bean。


因为 Nop 平台大量使用模型驱动来动态生成代码,所以大量 bean 的定义会自动被生成到配置文件中,需要手工编写的配置内容其实很少。另外自己也可以写一个扩展扫描函数,利用 NopIoC 的元编程机制来动态生成 bean 的定义。例如


<beans>   <x:gen-extends>      <ioc-gen:scan packages="test.my,test.other" />   </x:gen-extends></beans>
复制代码

Bean 的定制

NopIoC 的独特之处在于可以通过标准的 Delta 定制来实现对装配逻辑的定制,例如可以通过如下代码删除已有的 bean 的定义


<beans x:extends="super">    <bean id="dataSource" x:override="remove" /></beans>
复制代码

动态配置

NopIoC 的设计中包含了与 NopConfig 配置中心的一体化设计。在 beans.xml 配置中可以通过特殊的前缀表示动态配置项:


<bean id="xx">   <property name="poolSize" value="@r-cfg:my.pool-size|5" /> </bean>
复制代码


@r-cfg:前缀表示是动态配置项,当配置中心更改配置时会自动更新 bean 的属性。


另外在 beans 语法中还定义专门的 ioc:config 节点


  <ioc:config id="nopOrmGlobalCacheConfig" class="io.nop.commons.cache.CacheConfig"     ioc:config-prefix="nop.orm.global-cache"  ioc:default="true"/>
复制代码


NopIoC 容器会自动跟踪所有对于ioc:config的引用,一旦配置发生变化,会自动触发那些使用 config 的 bean 上所定义的 refresh-config 方法。


ioc:config-prefix 的作用类似于 Spring 中@ConfigurationProperties,用于指定配置项在配置文件中所对应的前缀。

与 SpringIoC 的互操作

NopIoC 与 Spring 容器可以同时使用,一般情况下 NopIoC 会在 SpringIoC 初始化之后再启动,它可以通过 parentContainer 拿到 Spring 容器所管理的 bean。所以在 NopIoC 中可以通过@Inject直接注入 Spring 所管理的 bean,而在 Spring 所管理的 bean 中,可以使用 beanContainer 来按照名称或者类型获取 NopIoC 所管理的 bean。


BeanContainer.intance().getBean(beanName)BeanContainer.intance().getBeanByType(beanType)
复制代码


NopIoC 的设计目标是成为一个更好的 SpringIoC,它坚持了严格的声明式设计,可以集成到 Spring 生态中使用,全部代码不超过 5000 行。更详细的设计可以参见文章: 如果重写SpringBoot,我们会做哪些不同的选择?

二. Web 框架

在 SpringCloud 的技术体系中,我们一般是在 Controller 中调用 Service 来完成具体业务逻辑,而在 Controller 中会处理一些对象结构转换或者组合的工作,例如将实体对象转换为 DTO 对象等。为了实现 GraphQL 服务接口,我们需要使用 graphql-java 包重新编写接口层代码,并不能直接复用 Controller 对外提供服务。


NopGraphQL 引擎极大简化了服务接口层的设计,一般情况下我们可以直接把领域模型对象暴露为对外服务接口,通过元数据层实现权限管控和结构适配,而无需通过 Controller 层实现适配转换


例如,即使对于最简单的 Web 服务,我们也需要指定 url 模式,指定参数传递方式(是通过 param 传递,还是通过 path 传递,或者是通过 body 传递)。很多时候还会在无意中引入对 HttpServlet 接口的依赖,导致整个服务与具体的 Web 运行时环境绑定。


@RestControllerpublic class MyController{    @PostMapping(value = "/echo/{id}")    public String echo(@QueryParam("msg") String msg,                 @PathParam("id") String id, HttpServletRequest request) {        return "Hello Nacos Discovery " + msg + ",id=" + id;    }}
复制代码


在 Nop 平台中,我们只需要在领域模型对象上增加@BizModel注解,然后标记服务方法及服务参数的名称即可。


@BizModel("MyObject")public class MyObjectBizModel{    @BizQuery    public PageBean<MyEntity> findPage(@Name("id") id,              FieldSelection selection, IServiceContext ctx){       return ....    }}
复制代码


  1. NopGraphQL 可以提供 REST 服务,但是它约定了固定的 url 模式:/r/{bizObjName}__{bizAction},无需为每个请求单独设计链接模式。

  2. 标记了 @BizQuery 注解的函数可以通过 POST 和 GET 方式来访问,而标记了 @BizMutation 的方法只支持 POST 方式访问。这种自动推导也避免了误用。

  3. 对于参数只需要通过 @Name 注解来标记参数名,无需指定参数传递方式

  4. 标记为 @BizMutation 的函数会自动启用事务管理,无需额外标记 @Transactional

GraphQL 作为通用分解组合方案


NopGraphQL 引擎不仅仅可以同时提供 GraphQL 和 REST 两种服务模式,它在 Nop 平台中的作用是作为一个通用的分解组合方案。在 Nop 平台中,所有的消息服务、批处理服务、RPC 服务等,在接收到请求消息之后,都是把它们投递到 GraphQLEngine 中去处理


一般的 Web 框架的服务方法都不支持直接返回实体对象,因为一般实体对象上包含过多的信息,并不适合直接序列化到前台。但是在 GraphQL 引擎的协调下,服务方法的返回值并不会直接作为结果数据返回给前台,而是要经过 selection 和 Data Loader 的增强处理。基于这种情况,NopGraphQL 将结果转换和适配工作完全交给 GraphQL 引擎(根据 XMeta 提供的元数据)去处理。也就是说可以通过配置的方式实现适配层,而无需单独设计的 Controller 适配层。


NopGraphQL 引擎可以脱离 Web 环境,在任何需要工作分解和结果选择、适配的场景下使用。它提供了与运行时无关的 IServiceContext 上下文对象(基本等价于一个 Map),可以利用它缓存在多个服务调用之间共享的数据,优化批处理的性能。

分布式 RPC

在 SpringCloud 的技术体系中,服务端启动时自动注册到服务注册中心,然后在客户端增加 Feign 接口,就可以实现负载均衡调用,灰度发布、AB 测试等都可以利用这里的服务路由机制来实现。


在 NopGraphQL 中,我们也是增加服务调用接口,


    public interface RuleService{
CompletionStage<ApiResponse<RuleResultBean>> executeRuleAsync(ApiRequest<RuleRequestBean> request);
default ApiResponse<RuleResultBean> executeRule(ApiRequest<RuleRequestBean> request){ return FutureHelper.syncGet(executeRuleAsync(request)); } }
复制代码


  1. 服务接口支持异步调用和同步调用模式,异步调用约定返回类型为 CompletionStage,而且方法名增加后缀 Async。

  2. 约定请求参数为 ApiRequest 类型,而返回结果为 ApiResponse 类型,这样可以在消息对象层面直接设置和读取 headers 扩展数据,而无需依赖底层的运行时接口。

  3. 在服务接口上无需增加额外注解,比如不用增加 @Path 等 REST 路径声明等


在 beans.xml 中通过如下配置创建分布式 RPC 客户端


    <bean id="testGraphQLRpc" parent="AbstractRpcProxyFactoryBean"          ioc:type="io.nop.rpc.client.TestRpc">        <property name="serviceName" value="rpc-demo-consumer"/>    </bean>
复制代码


通过设置 nop.rpc.service-mesh.enabled=true,可以启用服务网格模式,跳过客户端负载均衡代理。


详细介绍可以参见文章:


  1. 低代码平台中的GraphQL引擎

  2. 低代码平台中的分布式RPC框架(约3000行代码)

三. 存储层

NopORM 引擎包含了 SpringData, JPA 以及 MyBatis 的大部分功能,同时补充了大量业务开发中的常用功能,例如字段加密、逻辑删除、修改历史跟踪、扩展字段、多租户等。


在使用接口层面,NopORM 的使用模式与 Spring 中非常类似,只是它总是使用 XML 模型文件而不是 JPA 注解。


IEntityDao<NopAuthUser> dao = daoProvider.daoFor(NopAuthUser.class);

MyEntity example = dao.newEntity();example.setMyField("a");// 查找满足条件的第一个MyEntity entity = dao.findFirstByExample(example);
QueryBean query = new QueryBean();query.setFilter(and(eq(MyEntity.PROP_NAME_myField,"a"), gt(MyEntity.PROP_NAME_myStatus,3)));query.setLimit(5);
List<MyEntity> list = dao.findPageByQuery(query);
复制代码


一般情况下内置的 IEntityDao 上提供的方法已经足够的丰富,可以通过它支持非常复杂的查询条件,因此没有必要单独为每个实体生成独立的 Dao 接口,一般都是通过 DaoProvider 获取到对应的 IEntityDao 接口来使用。


对于复杂查询语句,可以直接使用类似 MyBatis 的 sql-lib 机制,通过 XML 模型来管理复杂动态 SQL。


@SqlLibMapper("/app/mall/sql/LitemallGoods.sql-lib.xml")public interface LitemallGoodsMapper {    void syncCartProduct(@Name("product") LitemallGoodsProduct product);}
复制代码


在 Mapper 接口上增加 @SqlLibMapper 注解来定义 sql-lib 模型与 Java 接口的映射关系。


在 sql-lib 模型中,可以通过 Xpl 模板语言来生成 SQL 语句


        <eql name="syncCartProduct" sqlMethod="execute">            <arg name="product"/>
<source> update LitemallCart o set o.price = ${product.price}, o.goodsName = ${product.goods.name}, o.picUrl = ${product.url}, o.goodsSn = ${product.goods.goodsSn} where o.productId = ${product.id} </source> </eql>
复制代码

Excel 模型驱动

Nop 平台提供了非常强大的模型驱动开发模式,可以解析 Excel 数据模型文件自动生成实体定义、Mapper 接口定义、元数据定义,后台 GraphQL 服务,甚至包括前台增加改查页面等。



详细设计可以参见文章


  1. 低代码平台需要什么样的ORM引擎?(2)

  2. 数据驱动的差量化代码生成器

  3. 基于可逆计算理论的开源低代码平台:Nop Platform

  4. 低代码平台如何在不改表的情况下为实体增加扩展字段

四. 底层语言

Nop 平台提供了专为 DSL 开发而设计的 XLang 语言,它包含了 XScript 脚本语言,Xpl 模板语言,XTransform 结构转换语言,XDef 元模型定义语言等多个子语言。其中


  1. XScript 脚本语言的语法类似于 JavaScript 语法,并支持类似 Java 的类型系统,可以作为一般性的表达式语言来使用。

  2. Xpl 模板语言是类似于 FreeMarker 的模板语言,支持自定义标签库,支持编译期宏处理。

  3. XDef 元模型定义语言类似于 XML Schema 模式定义语言,是一种非常简单直观的元模型定义语言。

  4. XTransform 是类似于 XSLT 的通用 Tree 结构转换语言。



Nop 平台底层对于 XML 语言进行了很多改造,它只是使用基本的 XML 语法形式,并没有使用 JAXB 标准,也没有使用 Java 中常用的 XML 解析器,而是完全从零编写了 XML 解析器和 JSON 解析器。相比于常用的 XML 解析器和 DOM 模型,Nop 平台的 XNode 结构要更加简单、直观,并且它内置 SourceLocation 跟踪机制,可以直接作为通用的 AST 语法树节点来使用。


XNode node = XNodeParser.instance().parseFromText(loc, text);
node.getTagName() // 读取标签名node.getAttr(name) // 读取属性node.setAttr(name,value) // 设置属性
node.attrText(name) // 得到文本属性,如果文本值为空,则返回null而不是空串node.attrTextOrEmpty(name) // 如果属性值为空,则返回null。属性值不存在时返回nullnode.attrInt(name) // 得到属性值并转换为Integer类型node.attrInt(name, defaultValue) // 如果属性值为空,则返回缺省值node.attrBoolean(name) // 读取属性值并转换为Boolean类型node.attrLong(name) // 读取属性值并转换为Long类型node.attrCsvSet(name) // 读取属性值,如果是字符串,则按照逗号分隔转换字符串集合

node.getAttrs() // 得到属性集合node.getChildren() // 得到子节点集合node.childByTag(tagName) // 根据子节点名查找得到对应子节点node.childByAttr(attrName, attrValue) // 根据属性值查找得到子节点node.getContentValue() // 读取节点值
node.hasChild() // 是否有子节点node.hasAttr() // 是否有属性node.hasContent() // 直接内容是否不为空node.hasBody() // 是否有子节点或者直接内容
node.getParent() // 得到父节点
node.cloneInstance() // 复制节点
list = node.cloneChildren() // 复制所有子节点
node.detach() // 解除父子关系
node.remove() // 从父节点中删除
node.replaceBy(newNode) // 在父节点的children集合中将本节点替换为newNode

node.xml() // 得到节点的XML文本node.innerXml() // 得到节点内部对应的XML文本
node.toTreeBean() // 转换为TreeBean对象
XNode.fromTreeBean(treeBean) // 从TreeBean转换为XNode
复制代码


与 Spring 生态的表达式语言和模板语言相比,Nop 平台 XLang 语言的设计一致性更强,


  1. 多个子语言共用大量的语法特性

  2. 多个子语言共用全局函数库

  3. 可以将一般的 Java 函数注册为全局函数

  4. 语法类似于 JavaScript,支持对象函数调用

  5. 提供了一个很容易定制的表达式解析器 SimpleExprParser。在报表引擎中使用 ReportExprParser 以及在规则引擎中使用的 RuleExprParser 都是基于这个表达式引擎定制而来。

  6. 系统化的支持编译期元编程机制,例如支持宏函数、宏标签等


进一步的详细信息可以参考文章:


  1. 低代码平台中的元编程(Meta Programming),

  2. 替代XSD的统一元模型定义语言:XDef,

  3. 从可逆计算看DSL的设计要点

五. 导入导出

NopReport 是基于可逆计算理论从零开始独立实现的一套开源中国式报表引擎,它的核心代码很短,只有 3000 多行(参见nop-report-core模块),具有较高的性能(性能测试代码参见TestReportSpeed.java),以及其他报表引擎难以达到的灵活性和可扩展性。


NopReport 在Nop平台中的定位是对表格形式数据结构的通用建模工具,所有需要生成表格形式数据的功能都可以转化为 NopReport 报表模型对象来实现。例如,NopCli 命令行工具提供的数据库逆向工程命令会逆向分析数据库表结构,生成 Excel 模型文件,这个 Excel 模型文件就是通过将导入模板转化为报表输出模型来生成。




与其他报表引擎相比,NopReport 具有如下非常鲜明的个性化特点:


  1. 采用 Excel 作为模板设计器

  2. 可以直接采用领域模型对象作为数据对象,而数据集(DataSet)仅仅是一种可选的数据对象形式。(一般的报表引擎只能使用表格数据)

  3. 在通用的表达式语法基础上扩展层次坐标概念,而不是使用专门设计的报表表达式语法

  4. 支持多 Sheet 和循环生成


NopReport 除了用于导出数据之外,它还支持自定义数据结构的导入。无需编程,只需要少量配置即可实现 Excel 数据模型的导入。Nop 平台中所有使用的 Excel 模型文件,例如 ORM 实体模型,API 接口模型等都是使用这里的报表导入机制实现导入。


详细介绍可以参见:


  1. 采用Excel作为设计器的开源中国式报表引擎:NopReport

  2. 如何用800行代码实现类似poi-tl的可视化Word模板

  3. 导入导出Excel时如何支持动态列和动态样式

六. 逻辑编排

Nop 平台提供了业务规则、工作流引擎、批处理引擎、任务调度引擎等逻辑编排相关的引擎实现,可以对一般的业务逻辑进行完整的描述。



具体介绍可以参见 采用Excel作为可视化设计器的规则引擎 NopRule

七. 自动化测试

SprintBootTest 提供了 Spring 框架与 JUnit 等测试框架的集成。在 Nop 平台中,从 JunitAutoTest 基类继承即可使用依赖注入来得到需要测试的 Bean。


@NopTestConfig(localDb = true,initDatabaseSchema = true)public class TestGraphQLTransaction extends JunitAutoTestCase {    @Inject    IDaoProvider daoProvider;
@Inject IGraphQLEngine graphQLEngine;
@EnableSnapshot @Test public void testRollback() { GraphQLRequestBean request = new GraphQLRequestBean(); request.setQuery("mutation { DemoAuth__testFlushError }"); IGraphQLExecutionContext context = graphQLEngine.newGraphQLContext(request); GraphQLResponseBean response = graphQLEngine.executeGraphQL(context); output("response.json5", response); assertTrue(response.hasError()); assertTrue(daoProvider.daoFor(NopAuthRole.class).getEntityById("test123") == null); }}
复制代码


通过 @NopTestConfig 注解可以很简单的控制自动化测试相关的一些配置开关。


NopAutoTest 自动化测试框架的独特之处是它提供了模型驱动的自动化测试能力,可以通过录制、回放机制实现复杂业务功能的自动化测试,无需手工编写测试代码。


具体设计参见 低代码平台中的自动化测试

八. 外围工具

Nop 平台提供了与 Maven 相集成的代码生成器,它可以在 Nop 平台之外独立使用,基于用户自定义的模型文件,应用用户自定义的代码模板,以增量化的方式生成指定代码。而一般的代码生成器,无论是模型还是代码模板都很难支持细粒度的定制。


详细设计参见 数据驱动的差量化代码生成器


NopIdeaPlugin 提供了一个通用的 IDEA 插件,它会自动识别 XML 根节点上 x:schema 属性所指定的元模型,然后根据元模型自动实现属性提示、链接跳转、格式校验等,对于代码段还提供了断点调试功能。



总结

Nop 平台的实现与 SpringCloud 相比,主要有如下特点:


  1. 实现原理简单直接,代码量普遍比 Spring 框架中的组件小一个数量级以上,但是核心功能反而更加丰富

  2. 支持差量化定制,可以在 Delta 目录下增加 Delta 模型文件来定制所有已有功能(包括平台内置功能),例如所有的数据模型、所有的设计器页面都可以自由定制。

  3. 利用代码生成、元编程等机制实现自动推导,极大降低需要手工编写的代码量,保证系统内部信息结构的高度一致性。

  4. 统一使用 XDSL 规范来实现模型 DSL,可以随时新增 DSL 模型,也可以在原有 DSL 模型中增加新的扩展属性。IDE 插件自动识别并支持新的 DSL 模型,无需特殊编写支持插件。

  5. 模型驱动集成在 DevOps 开发流程中,在 maven 打包过程中实现代码生成、模型转换等,不需要单独部署模型管理平台。


Nop 平台可以作为运行在 SpringCloud 之上的一种扩展组件,它与 SpringCloud 内置机制并不冲突,它内部的各种组件也可以被替换为 SpringCloud 的实现,只是会丢失很多高级特性、损失性能、损失扩展性,破坏程序结构的可推理性。


发布于: 2023-09-17阅读数: 3
用户头像

canonical

关注

还未添加个人签名 2022-08-30 加入

还未添加个人简介

评论

发布
暂无评论
Nop平台与SpringCloud的功能对比_Spring Cloud_canonical_InfoQ写作社区