版本修订

版本修订历史
版本 作者 时间 描述

0.21

Lang

2023-11-13

起草最新版本

Chapter I: 核心原理

1. Zero中的查询引擎

那些张口就来肯定一个东西的优缺点、自己没使用过的无脑观点就不聊了,曾经有人给我疯狂吐槽Jooq的性能问题,但从目前我们系统的表现看起来,虽然性能不是最优,但也不至于到他说的那种程度,评估一个东西请用正常模式,别推一样踩一样。

软件工程师最忌讳的一个点就是 信仰主义

1.1. 查询引擎基础

1.1.1. Jooq总结

Zero内置使用了Jooq做底层数据访问,它的优缺点如下:

优点:

  • 类型安全:Jooq支持强类型的查询,减少了错误和歧义的可能性,从而使代码更加可靠和安全。

  • 可读性高:Jooq生成的代码类似于SQL语句,易于理解和维护,同时还提供了高级功能,如内部联接和复杂查询。

  • 易于使用:Jooq提供了简单易懂的API,使开发人员可以快速编写和执行数据库查询。

  • 与多种数据库兼容:Jooq支持多种数据库,包括MySQL、PostgreSQL、Oracle等,这使得它适用于不同的项目和团队。

  • 可扩展性强:Jooq允许开发人员添加自定义数据类型和操作符,以支持特定的业务需求和数据库类型。

  • 支持多种数据库特性:Jooq支持许多数据库特性,如分页、事务、聚合、分组等,这使得开发人员可以轻松地使用这些功能而不必担心不同数据库之间的兼容性。

缺点:

  • 学习曲线较陡峭:Jooq的API相对复杂,需要一定的学习和实践才能熟练掌握。

  • 不支持所有数据库特性:虽然Jooq支持许多数据库特性,但并不支持所有功能。因此,在使用Jooq之前,需要确认所需的功能是否得到支持。

  • 生成的代码量较大:Jooq生成的代码相对较多,可能会导致代码库变得复杂和冗长,从而增加了维护的难度。

  • 比较笨重:相对于其他数据库访问库,Jooq的代码库比较庞大,可能会对应用程序的性能产生一定的影响。

  • 文档不够完善:Jooq的文档不够详尽和全面,可能需要花费更多时间和精力来学习和理解其使用方法和技巧。

Jooq的性能影响因素主要包含如下几点:

  • 复杂的查询语句:Jooq支持复杂的查询语句,但这些语句可能会导致性能下降,尤其是当查询的数据量非常大时。在这种情况下,可以考虑使用更简单和直接的查询语句,以提高性能。

  • 非优化的数据库表结构:如果数据库表结构设计不良,会对Jooq的性能产生影响。例如,如果表中有太多的冗余字段、重复的数据或过度归一化的设计,都会增加查询和更新的成本。

  • 大量数据的操作:当处理大量数据时,Jooq的性能可能会受到影响。这时可以考虑使用批量插入或更新操作,以减少数据库交互的次数,从而提高性能。

  • 过多的SQL语句:如果使用Jooq时生成了过多的SQL语句,会对性能产生负面影响。这时可以考虑通过使用缓存或重构查询语句来减少SQL语句的生成次数,以提高性能。

  • 应用程序本身的性能问题:Jooq本身的性能问题可能并不是最终的瓶颈。在一些情况下,应用程序本身的性能问题,如磁盘I/O或网络延迟等,可能会对Jooq的性能产生更大的影响。

1.1.2. Qr引擎

Zero中为SQL语法提供了 Qr(Query Result) 的查询引擎语法,语法本身是JSON格式,在前后端可以通过代码直接提取数据或构造不同的查询语法,查询引擎的好处:

  1. Zero为查询引擎提供了前后端同步的API,不论在前端还是后端都提供了查询引擎语法树,您可以很方便地将查询条件进行转换、计算、合并等各种操作。

  2. Json语法格式简单,开发人员很容易理解查询引擎语法,并根据业务需求提炼复杂的查询条件。

  3. 前端组件中有很多和查询相关的内容,统计、视图定制、排序、列表,所有和数据特性相关的运算可直接使用查询引擎完成,不需要针对特定场景开发对应接口(项目赶进度比较重要)。

  4. Zero底层对语法树做过三种不同算法层面的优化,只需识别模型即可处理各种复杂的优化查询,从目前生产环境表现看起来还算 稳定靠谱

  5. Zero针对底层查询执行了跨表类处理(JOIN),跨表所有数据库级别的 DML 操作都使用同样的API实现,如此就解决了用户在复杂业务跨表操作的问题,包括多表同读和多表同写。

查询引擎语法同样适用于动态建模项目(zero-atom)中,只要在Zero框架中和数据库打交道,不论注释文档中书写提到了查询引擎(QR)部分,都可直接使用查询引擎语法搞定。

除此之外,spring-up 项目中还可以将查询引擎语法用于Spring框架,只是需要依赖 QueryDSL 项目。

1.2. 语法说明

参考: 1.10.珷玞:Jooq

1.2.1. Json结构

Zero中的查询引擎的JSON语法主要包含 4个关键属性

关键属性 格式 含义

criteria

JsonObject

查询条件语法树,Qr 条件语法结构。

projection

JsonArray

数据列过滤,包括列过滤和列排序两个功能。

sorter

JsonArray

数据排序,可支持第一排序列和第N排序列,每个列可支持升序和降序二选一。

pager

JsonObject

数据分页功能,一般用于前端界面呈现。

本章节主要关注参数 criteria,它遵循 Zero 内部定义的JSON格式的查询语法。

1.2.1.1. 语法初见

由于JSON格式是一个树型结构,所以按照树的节点,Zero 中定义的节点类型包含如下:

  • 直接节点 :最简单的键值结构:column=value,二元操作符结构中 value 可以随意。

  • 嵌套节点 :当前节点之下不是一个单纯的:column=value 结构,相反是 子语法树,这种模式下只要保证键不重复即可,通常会使用类似 $x 的格式来书写键值。

  • 连接节点 :一般是 "" = true/false 结构,用于表示本层语法的连接符(注意只局限于本层)。

为了使用示例演示,先提供一个表结构:

属性名 类型 列名

name

字符串

NAME

email

字符串

EMAIL

password

字符串

PASSWORD

age

数值

AGE

active

逻辑值

ACTIVE

在看不同节点之前,先看一个Json示例:

{
    "name,c": "Lang",
    "": true,
    "$0": {
        "": false,
        "email,s": "silent",
        "email,e": "zhaowing.com"
    }
}

上边的语法最终会展开成如下SQL语句:

NAME LIKE '%Lang%' AND (EMAIL LIKE 'silent%' OR EMAIL LIKE '%zhaowing.com')
1.2.1.2. 语法定义

根据前一个章节示例,相信您对Zero Qr查询语法有了一个直观的理解。其实Qr定义中每一个单独条件的实际语法如下:

"<field>,<op>": "<value>"
  • <field> :此部分通常是 属性名,但是为了兼容部分底层开发程序员,该属性值也可以是 数据库列名,如 name,c 也可以写成 NAME,c

  • <op>:这部分内容是 操作符,操作符可以参考下边操作符表,用于制定查询条件专用。

  • <value>:此处存放的就是对应的值。

操作符说明表
操作符 格式 含义 等价SQL

<

"age,<":20

小于某个值

AGE < 20

"age,⇐":20

小于等于某个值

AGE ⇐ 20

>

"age,>":16

大于某个值

AGE > 16

>=

"age,>=":16

大于等于某个值

AGE >= 16

=

"age,=":12"age":12

等于某个值

AGE = 12

<>

"name,<>":"LANG"

不等于

NAME <> 'LANG'

!n

"name,!n":"任意"

* 不为空

NAME IS NOT NULL

n

"name,n":"任意"

* 为空

NAME IS NULL

t

"active,t": "任意"

* 等于TRUE

ACTIVE = TRUE

f

"active,f": "任意"

* 等于FALSE

ACTIVE = FALSE

i

"name,i": ["A","B"]

在某些值内

NAME IN ('A','B')

!i

"name,!i": ["C","D"]

不在某些值内

NAME NOT IN ('A','B')

s

"name,s": "Lang"

以某个值开始

NAME LIKE 'Lang%'

e

"name,e": "Lang"

以某个值结束

NAME LIKE '%Lang'

c

"name,c": "Lang"

模糊匹配

NAME LIKE '%Lang%'

如此,您就可以组合各种复杂的SQL语句来完成 查询需求,从目前我们在真实项目中的应用看起来,足够解决大量问题了,所有在应用系统级别遇到的查询问题都可以在上述操作符中得到相关解答。

连接节点 是Zero中一个比较巧妙的设计——使用了编程过程中的的一个禁忌,就是 "" 作为键值,使用它的目的:

  1. 方便开发人员记忆,只要学会之后,可以很快将想要的查询条件转换成核心语法。

  2. 空键 不具有任何业务意义,真实场景中不会和字段产生冲突,起到名副其实占位符的作用。

连接符
连接符

""

true

AND

""

false

OR(或者不写)

1.2.1.3. 语法技巧

Zero中存在一部分默认语法转换规则,此处进一步说明,为开发人员解惑,也提供部分技巧。

  1. 查询引擎中的默认连接符是 OR,也就是说如果不存在 "" 键,那么本层条件连接符使用 OR

  2. 如果值格式为 JsonArray,且左侧不带 <op> 节点,则语法自动转换成 IN 语法。

  3. 通常字段不书写 <op> 节点时,默认语法为 = 符号,但优先级弱于第二法则。

  4. * 号的四个二元操作符推荐在值部分(任意)书写部分注释,可提供给别人查阅。

  5. 子查询树 格式通常是 column = JSON,此时的 column 为了不和其他字段起冲突,建议使用 $ 前缀。

1.2.1.4. 示例
/* Qr语法:
{
    "name": "Lang",
    "email,s": "lang.yu"
}
*/
-- 查询名称等于Lang,或者email以 lang.yu 开始的记录
NAME = 'LANG' AND EMAIL LIKE 'lang.yu%'

/* Qr语法
{
    "name,c": "lang",
    "": true,
    "$0": {
        "email,c": "yu"
    }
}
*/
-- 查询名称中包含了lang,并且邮箱中还包含了 yu 的记录
NAME LIKE '%lang%' AND EMAIL LIKE '%yu%'

1.2.2. 前端Qr组件

1.2.2.1. ExListComplex简介

本章节主要讲解前端一个消费 Qr 语法很集中的组件 ExListComplex,通过对组件关于 Qr 部分的讲解,让您对 Zero UI的前端解析有一个更加清楚的认识,ExListComplex 是前端最高频的列表组件,使用了大量的查询语法,它支持的功能如下:

  • 普通增删查改

  • 批量增删,导入和导出

  • 分页、排序、跳页、页面尺寸修改

  • 列过滤、检索

  • 保存个人视图

    • 直接在列定义中执行列筛选、列排序功能保存个人视图

    • 设置当前视图的查询条件,保存个人视图

    • 视图管理执行完善的增删改以及选择后查询

  • 高级搜索,提供子表单执行高级搜索功能

ExListComplex 组件的查询入口主要有三个:

  1. 列过滤筛选,默认使用条件堆积,列和列之间使用 AND 操作符

  2. 输入框筛选,一般是核心字段检索功能,字段和字段之间使用 OR 操作符

  3. 高级搜索,一般是完整的查询表单,抽屉从右侧拉出,连接符可自己在表单中设置。

本章不是讲解 ExListComplex 组件,所以只抽取了和查询引擎强相关的内容进行讲解,其他话题不在此章节详细讲解(如:加载状态流、表单配置、扩展插件、导入导出等)。

ExListComplex 组件的核心查询变量如下:

常用查询属性表
属性名 来源 含义

config

props

从外层传入的列表标准配置,内部包含 query 节点。

$query

props

从外层传入的查询条件,当前组件作为被控组件时需使用此查询条件作为查询的基础条件。

$myDefault

props

当前列表运行时我的默认视图。

$myView

state

当前列表运行时运行的视图信息,我的视图。

$condition

state

列过滤专用条件属性。

$terms

state

列过滤元数据定义(支持查询的过滤列信息)。

$keyword

state

使用搜索时的 高亮 关键字。

$qr

state

高级查询表单专用的查询初始值。

__qr

ajax

使用查询引擎接口类似 POST /api/xxx/search 时,远程返回数据中如果包含 qr 则证明当前请求存在视图,而 qr 记录的是当前视图中 criteria 字段中存储的数据。

$queryDefault

state

当前列表的初始化查询核心参数,即列表查询参数的默认值。

$queryView

state

当前列表的初始化查询条件之后根据视图 __qr 计算的新的查询核心参数,即列表查询参数带视图的默认值。

$query

state

当前列表的运行时参数,每次请求过程中的参数以此参数为准。

$columns

state

当前列表访问模型的全列信息。

$columnsMy

state

当前列表访问模型的我的列信息(存在视图时来自视图)。

前端的 $query 通常意义上表示的是完整的查询引擎参数,类似如下结构:

{
    "criteria": {},
    "pager":{
        "page": 1,
        "size": 10
    },
    "sorter": [
        "xxxx=ASC"
    ],
    "projection": [
    ]
}

不仅如此,由于Zero前端存在 语法解析器,所以上述结构也可以使用简易语法:

{
    "criteria": {},
    "pager": "1,10",
    "sorter": "xxx=ASC",
    "projection: []
}
1.2.2.2. 查询数据流

接下来参考下边的数据流图来理解上述所有参数以及在 ExListComplex 中的协同工作原理。

zqr arch

2. 核心缓存

2.1. 安全缓存池

2.1.1. 账号登录缓存

账号登录缓存是在登录之后存储到系统会话中的缓存,它通过下边代码写入到缓存中:

    ScUser.login(data)   // 登录缓存初始化(静态调用)

其中此处的 data 数据结构如:

{
    "user": "账号ID",
    "role": [
        {
            "roleId": "角色ID",
            "priority": "数值,角色优先级"
        }
    ],
    "group": [
        {
            "groupId": "用户组ID",
            "priority": "数值,用户组优先级"
        }
    ],
    "habitus": "128位会话随机字符串,每次登录时生成,等价于会话ID",
    "session": "Vert.x生成的会话 Session 的 id"
}

账号登录缓存的数据结构如下图:

0

上述 data 数据输出会帮着用户初始化一个完整用户登录缓存信息,作以下几点说明:

  • 用户登录缓存的主缓存池使用了SharedMap中的固定池名称 ZERO-CACHE-HABITUS,但实际访问(引用)它的对象为 ScUser 对象。

  • 根据会话键 habitus,每个用户登录之后会分配一个唯一的 habitus 值,即真正开发时:您可以使用用户ID获取 ScUser 对象,然后直接操作 ScUser 对象实现用户登录数据的提取。

  • 存储数据本身不包含 habitus,参考左侧 remove

  • 最核心的两个对象为 profileview

    • profile 就是消费端提到的 64 种Profile,根据用户本身数据信息计算出来的。

    • view 则是一个动态结构,会根据用户发送请求计算 viewKey,最终根据这个值计算用户的视图信息,视图信息是序列化过后的 DataBound 对象。

2.1.2. 401认证缓存

认证缓存在系统中由核心框架提供 执行模式,开发人员则只需要提供存储数据结构即可,有了认证缓存后,用户就不需要每次发送请求都让系统运行 标准认证流程,只有当如下情况发生时才会要求用户重新认证:

  • 用户令牌过期,需要使用新令牌对用户重新执行认证。

  • 系统检测到用户出现了非安全性操作。

  • 用户账号出现了异常,导致请求过程中安全身份信息无法通过会话检查。

401认证缓存 使用了SharedMap中固定的池名称 ZERO-CACHE-401,它的数据结构如下:

0

从上边讲解可知,401认证缓存中实际存储的就是 JWT 令牌中的基本信息(去掉了角色和组部分的数据)

2.1.3. 403授权缓存

授权缓存在核心框架层只提供了接口,并未提供实现,而Zero Extension框架中提供了本章的实现。有了授权缓存后,用户就不需要每次发送请求申请资源数据时运行 扩展授权流程

403授权缓存 使用了SharedMap中固定的池名称 ZERO-CACHE-403,它的数据结构如下:

0

从数据结构上看,授权缓存的结构相对简单,habitus 可标识用户登录的当前会话ID,而每个资源都会包含 TRUE - 有权限,FALSE - 无权限二选一的值,正如在安全管理中讲到的,实际表示:用户是否有权限访问该资源。注意此处提到的是否可访问只牵涉最基础的授权,并不包含安全视图相关信息,安全视图解决的是 用户访问多少 的问题。

2.1.4. 资源缓存

Zero框架中定义的资源一般情况不会被高频修改(除非走开发中心资源定义),所以资源信息在第一次请求读取之后就被初始化放到了资源缓存中,资源缓存和 habitus 无关,它是所有角色和用户可共享的数据结构,这也符合RBAC模型的定义。

资源缓存(又称为资源池) 使用了SharedMap中可配置的池名称,默认 POOL_RESOURCES,资源池的结构如下:

0

资源池的结构很简单,只有一点需说明就是此处为什么不是直接存储权限集,而要隔离一层Profile信息放在中间,主要原因是后续拓展资源执行多Profile模型时,资源池的概念会被抽象,一旦抽象之后资源池的内容就不再是只读模式,有可能会在运行过程出现调整,多个Profile并行访问资源池以及一个资源提供多种Profile的模型(比如打开组)。

2.1.5. 权限缓存

Zero框架中权限缓存是最简单的缓存,权限缓存 使用了SharedMap中可配置的池名称,默认 POOL_PERMISSIONS,权限缓存结构如下:

0

2.2. Cc结构

Cc结构是Zero中的核心缓存架构,该缓存架构可以帮助用户统一管理所有缓存,前文中的安全缓存也使用了 Cc结构 ,在整个Zero结构中,并不存在设计模式中的纯单件结构,所有的对象都是 线程单件 模式,即控制类组件在每个线程中只有一个实例(一种组件最少实例数和后端开的Worker/Agent一致,实例数量和Vert.x中的线程直接对齐),之后没有特殊情况不开新实例。

Cc全称为:Cloud Cache,该结构起源于云原生Aeon系统最初的设计,底层所有内存级 Map 结构都已被修订成Cc结构了,在单机环境中它们具有内存级的一致性,而在云环境中,单点中的缓存会维持节点级的内存一致性。

2.2.1. 静态方法

Cc接口中的静态方法主要如下:

    static <K, V> V pool(final ConcurrentMap<K, V> input, final K key, final Supplier<V> supplier) {
        return Fn.pool(input, key, supplier);
    }


    static <V> V pool(final ConcurrentMap<String, V> input, final Supplier<V> supplier) {
        return Fn.poolThread(input, supplier);
    }

    static <V> Cc<String, V> openThread() {
        return open(CcMode.THREAD);
    }

    static <K, V> Cc<K, V> open() {
        return open(CcMode.STANDARD);
    }

    static <K, V> Cc<K, Future<V>> openA(){
        return new CcAsync<>(CcMode.STANDARD);
    }

    static <K, V> Cc<K, Future<V>> openThreadA(){
        return new CcAsync<>(CcMode.THREAD);
    }

上述静态方法可直接使用 Cc.xxx 的方式调用,这些接口主要包含如下维度:

方法 全局/线程 含义

pool

全局/线程

外部传入 ConcurrentMap ,兼容大量旧版API,带 key 参数为全局级接口,不带 key 参数为线程级接口。

open/openThread

全局/线程

打开一个新的Cc结构,open打开全局Cc结构,openThread则打开线程级Cc结构。

openA/openThreadA

全局/线程

打开一个新的Cc结构,但此Cc结构使用了异步模式(非同步模式),内置实现可根据后续不同实现进行配置和扩展。

2.2.2. 缓存结构图

普通缓存结构实际就是底层一个内存级的Map(内存映射),而Cc结构在此处做了小切换,您需要理解:全局级和线程级 的缓存区别,它们完整的结构图如下:

0

  1. 全局缓存必须外层传入 key 值,若您不传入 key 值,则系统会抛出 501 Not Supported 的异常信息,全局级缓存在整个应用中只根据 key 执行维度共享,即:线程和线程之间有可能拿到同一个全局缓存引用。

  2. 线程缓存有两种模式:

    • 纯线程模式:纯线程模式不带 key 值,即每个线程中只有一个缓存对象用于存储相关数据。

    • 二维线程模式:这种模式下线程级缓存也可以带 key 值,即:假设 key 包含5种,那么每个线程中只有5个缓存对象用于存储相关数据。

参考下边的使用示例:

// 创建一个线级 Cc
// key 为字符串,value 为 Confine 实例
static final Cc<String, Confine> CC_FINITY = Cc.openThread();

// 延迟初始化,二维线程模式,`key` 值为实例所属类名
final Confine confine = CC_FINITY.pick(() -> Ut.instance(confineCls), confineCls.getName());

2.2.3. Cc实现类

Cc结构的实现类现阶段主要包含如下几种:

类名 含义

io.horizon.uca.cache.CcMemory

全局级缓存。

io.horizon.uca.cache.CcThread

线程级缓存。

io.horizon.uca.cache.CcAsync

异步缓存级(同时支持线程级和全局级)。

虽然前文提到了静态方法,此处再换一个角度看看四个API的不同用法。

静态方法 含义

open

创建一个新的全局级缓存。

openThread

创建一个新的线程级缓存。

openA

创建一个新的全局级异步缓存。

openThreadA

创建一个新的线程级异步缓存。

现阶段所有的线程实现都使用了 ConcurrentHashMap 的数据结构在最底层消费内存资源实现缓存的基础管理,注意:Cc结构一般不做数据缓存,主要用作配置缓存和系统缓存,在Zero内部最常用的场景为 组件缓存 以控制对象实例化的数量;由于该结构可支持单点并发,所以在云环境中它主要用来做单节点的资源控制。

2.3. 云原生节点缓存

2.3.1. 节点缓存基础

云原生节点缓存是从 Aeon 系统设计心得而来,它主要位于连接器和全局化操作,现阶段Zero中的云原生节点缓存主要包含如下三种(全局统一管理):

没看错,这些类名不是化学程序,但来自化学中的灵感,云原生节点缓存全部位于包:io.aeon.runtime 中,新版主框架的底层缓存中全部拿掉了 ConcurrentMap 的方式构造缓存,而采用 Cc 结构,这样的结构有几点优势:

  • 底层缓存实现可替换,由于支持同步和异步两种模式,底层缓存实现可以替换成任意您想要的模式(如 Redis)。

  • 缓存接口统一调用,所有和缓存相关的组件、元数据、数据、流程、业务、接口都直接使用 Cc 结构,且从 H1H / H2H / H3H 中提取相关数据。

  • 缓存统一标准化监控管理,监控程序上了之后,全框架缓存只需要捕捉此处的三个类就可以得到完整的缓存拓扑执行监控和管理。

内部缓存如下:

类名 翻译 含义

CH1H

氕(piē)

普通能源,用于存储云原生(单机运行)环境中的组件缓存。

CH2H

氘(dāo)(重氢)

稀有能源,用于存储云原生(单机运行)环境中的应用级数据缓存,多应用模式尤其重要。

CH3H

氚(chuān)

超稀有,用于存储云原生(单机运行)环境中的部署、租户级元数据缓存,云原生环境跨节点可共享。

2.3.2. 缓存常量

Zero新版中的缓存常量只有两个:

类全名 含义

io.horizon.runtime.cache.CStore

Metadata规范下的新项目基础缓存,一般不直接使用,由于应用级缓存直接从它继承(实际不是继承),所以真正使用时可直接操作应用级模式同样可访问原生定义的缓存结构。

io.aeon.runtime.CRunning

Aeon和Zero专用缓存类,它们的继承关系如:CStore / CH1H / CH2H / CH3H / CRunning,所以从 CRunning 中可访问所有父级接口中的缓存。

  • 此处内部缓存的访问控制域都是 package 默认域,所以不可以在此包之外访问,结合JVM安全模式,后期版本可直接限定开发人员的使用。

  • CStore虽然是可直接访问的缓存,但不推荐开发人员使用,该缓存只是单纯留给开发人员继承时使用。

  • 此处虽然是继承,但实际都是接口继承,采用了原型链的模式在操作,所以从概念上讲此处并非真正意义上的 继承

Chapter II: 高阶设计

3. 标准化:AMS

3.1. 元数据规范

新版Zero从原来的 zero-co 项目中分裂出一个新项目 zero-ams,全称为 Agreed Metadata Specification(翻译为:达成共识的元模型规范),该项目具备非常强的扩展性,可适用于任何和 Vert.x 相关的项目,提供了一个项目的基础底座,此项目为大量 Vert.x 项目提供了一种基础标准,即使您不使用Zero也可以在 Vert.x 项目中使用此依赖来完善元数据规范

3.1.1. 设计原则

抽象、体系、复用、深度。

zero-ams 的设计主要是重新抽象了 Zero 核心框架底层的功能包,用于跨框架执行,理论上只能在 Vert.x 框架中使用,但在实战中若和Spring做集成,可直接引入 zero-ams 的依赖包直接在 Spring 中使用。AMS 的设计核心在于:

  • 弥补JVM中部分语法定义处理得不当的地方,让整个环境变得更加自由(JVM支持函数式编程是8+之后的事,由于本身设计的缺陷是完全支持函数式编程理论的)。

  • 扩展部分JVM中的API以适应更多偏向于业务计算的场景,解决大部分重复劳动的工作量,如:MD5加密、序列化、IO等。

  • 从实战经验中总结相关教训,提供和平台无关的基础组件,让您的项目和框架本身语义(羽翼)变得更加丰满。

  • 定义一套基于:运行时、建模、云端纯接口规范(这是私货),让您在处理平台级应用时有一个设计层面的思考。

虽然从实际过程中可以看到这部分内容的设计略有冗余,但若您选择使用 Vert.x 作底层框架时,我相信这套基础规范可以让项目实施和交付变得更加流畅。

3.1.2. 功能支持表

AMS 支持的功能点参考下表:

功能 说明

逆向注解

提供新注解,针对特殊的功能实现元数据分析(代码分析代码的模式):

  • @Development:开发注解,定义部分私有成员辅助开发,可使用代码和脚本对项目本身执行分析。

  • @HighOrder:连接注解,定义和 ams 项目相关的工具调用,以标记代码。

  • @Memory:缓存注解,可对Cc架构执行缓存注解。

  • @ChatGPT:使用ChatGPT编写的机器代码部分

逆向注解是非必须,若没有在代码中标注,仅在逆向工程中无法收集相关元数据,对正常程序的运行不产生任何影响。

通用数据结构

提供部分通用数据结构定义,不带任何业务色彩,纯功能支持型内容。

异常架构

提供任何项目都可使用的异常规划,可加载不同资源文件实现扩展异常定义,动参和继承相结合,实现项级的完整异常体系,完整抽象层异常体系如下:

java.lang.Throwable

  -- java.lang.Exception

        # 编译时顶级 Checked 异常
     -- io.horizon.exception.ProgramException

           # 和资源文档绑定的检查异常,通常在编程过程中可抛出
           # 1. 配置强化校验
           # 2. 容器后台程序校验
        -- io.horizon.exception.DaemonException

     -- java.lang.RuntimeException

           # 运行时顶级抽象异常
        -- io.horizon.exception.AbstractException

              # 内部专用异常,不绑定资源文件,直接编程使用硬编码方式定义
           -- io.horizon.exception.InternalException

              # 和资源文档绑定的启动异常,在容器启动过程中出现
              # 1. 模型校验
              # 2. 容器启动校验
           -- io.horizon.exception.BootingException

              # Web容器运行过程中的通用异常
              # 通用异常包括常见的:500、501、400、401、403 等常见异常
           -- io.horizon.exception.WebException

函数包

提供 级别的函数顶层包 HFn(Java语法糖),实现工具独立规划和分析,且在函数包中引入增强的防御式、文学式编程范式,以更优雅的方式处理JVM级别的异常,且扩展 函数接口 让Java可支持 lambda 中的检查异常抛出机制。

  • 由于Java语言中的 @FunctionalInterface 内部不支持异常捕捉,若您在 lambda 块中写了 检查异常 抛出相关的代码会导致 编译错误HFn 扩展 Throwable 函数接口定义了可抛异常的接口。

  • 除开官方提供的 Predicate、Supplier、Consumer、Funcation 四种常用函数式接口之外,HFn 定以了纯执行类接口 Actuator,此接口 无参数,且返回值为void

  • try-catch 的小封装,提供几个级别的快速函数执行接口,不同前缀处理不同类型的异常:

    • bug:处理所有 ProgramException 类型的异常,这种类型的API要求底层必须以 throws 抛出对应异常,属于静态检查。

    • fail:处理所有 Exception 类型的异常,虽然 Exception 是检查性异常,API封装后外层 lambda 不抛出。

    • jvm:处理所有 Throwable 类型的异常,API封装后外层 lambda 不抛出。

    • run:广泛定义 RuntimeException 级别的异常,API内部可随意抛出这种类型异常,不检查。

  • 在原始异常结构中定义了新的异常体系,和 AMS 中的部分 SPI 插件接口对接,实现任意框架级的资源文件扩展,除开 Zero 中的 vertx-error.yml 中的实现,您可以开发自定义的资源文件实现方案。

  • 提供常用的 异步 编排组合函数,您可以将多个异步 Monad 组合成复杂结构:串行、并行、网状、树状 等——编排是异步编程中的重点,也是 Zero 框架本身的一大亮点。

  • 开放部分类的 高阶 组合函数,您可以在二阶以上实现基础的编排工作,近似于标准库中的 BiXx,但扩展部分还可能包含 TiXx 的更加高阶的函数模型。

规范

提供 运行时、建模、云端 三种规范接口定义,这种类型的定义仅限 interface 关键字,所以整个 AMS 中不提供任何实现,代码内部注释比较齐全,所有接口都是 H 前缀的高阶设计,主要包含四大类:

  • 基础功能类

  • 建模类

  • 云端类

  • 开发定制类

工具包

提供 级别的工具顶层包 HUt,实现工具独立调用,工具支持:

  • 彩色Console日志

  • 对象比对、值比对

  • 加密/解密模块(MD5、RSA、公私钥非对称、Base64等)

  • 集合运算(添加、基础、聚集、查找、拉平、反序、分组、映射、移除等)

  • 环境变量加载

  • 异常快速构造

  • String / JsonObject / JsonArray和其他数据结构的相互转换

  • IO目录、文件读取、不同格式文件读取(包括jar内部资源文件)、以及大文件(>10G)算法

  • 判断性原子函数

  • 专用 lambda 模式的集合迭代

  • 建模 抽象工具类

  • 网络检查专用功能包

  • 日期/时间 格式化、转换、解析包

  • 随机数、字符串、验证码生成功能包

  • 安全值读写

  • 常用反射包(单件、实例、池化、SPI级核心反射)

  • 表达式解析包

  • AOP插件执行包

  • Jackson 基础序列化/反序列化包

UCA架构

全称为 User Component Architecture,用户自定义组件架构,规范中支持的自定义组件如:

  • CC缓存架构:Cloud Cache,支持内存缓存、异步缓存、线程缓存,全局统一管理缓存

  • 比对组件:可针对Java语言中的强类型执行通用比对,差异性比对、等价性比对

  • 转换组件:特殊数据结构之间的相互转换功能函数

  • 加解密组件:公私钥非对称加解密专用组件,内部支持HED实现敏感数据加解密功能

  • 日志组件:基础日志、扩展日志专用组件,提供可扩展日志结构,外可连ELK平台

  • 本地文件组件:提供本地文件类似 mkdir / rm 等常用命令的文件操作

  • 网络组件:IPv4和IPv6专用网络检查功能包

  • Qr查询引擎组件:支持 Qr 查询引擎语法的专用组件

  • 启动组件:可插拔启动器架构的核心组件

  • AOP组件:新版AOP编排组件

3.1.3. 框架对接

zero-ams 中的异常架构是基于 SPI 扩展,若要对接新框架如Spring,则需要实现基础资源关联部分的SPI,才可以和 zero-ams 完整集成,否则 可配置 异常架构无法在新框架中发挥作用,本章主要讲解对接步骤以及注意事项,您可以完全隔离 Zero 框架在任何支持 Vert.x 的结构中直接使用此教程。

SPI(Service Provider Interface)是Java语言中原生的一种机制,用于实现模块化、可扩展性的应用程序,它允许开发者定义一组接口(或抽象类),而允许不同的实现者提供具体的视线,这些实现可以在运行时被动态加载或替换,而无需修改源代码。它使用场景如下:

  • 插件系统

  • 驱动程序加载

  • 扩展框架

它是一种实现松耦合、可扩展、可插拔的方式,允许开发人员在不改变核心功能的前提下扩展应用程序。

3.1.3.1. 资源文件

新框架对接中基本要求:必须提供异常资源文件连接,如 Zero 中使用 vertx-error.yml 资源文件做绑定,您可以在新的框架如 Spring 中参考 spring-up 项目中的配置,使用可国际化的 application-error.yml 资源文件做绑定内容。SPI实现过程中并没有强制要求您从文件路径中加载资源,只是要求从返回的数据结构中(JsonObject类型)实现两个核心的数据结构

  • 异常代码/系统信息表

  • 异常代码/业务信息表(可选,根据业务需求定义)

这两个信息表在Zero框架中位于 vertx-error.yml / vertx-readable.yml 两个资源文件定义,您可以在自己对接其他框架时,采用其他实现手段来完成,不影响整体结构。以下是 Zero 框架中的异常信息表的范例:

vertx-error.yml

# 20001 - 29999
# Thirt part error for different integration
E20001: (401) - Qiy interface of "/iqiyi/authorize" met errors, code = {0}, message = {1}
E20002: (401) - Qiy token record does not contain "access_token", client_id = {0}

E80203: "(449) - (RBAC) The user `{0}` could not be found in database"
E80204: "(401) - (RBAC) The user''s ( username = {0} ) password that you provided is wrong"

vertx-readable.yml

# 登录异常
80204: "对不起,用户名和密码错误!"
80203: "对不起,找不到您提供的用户信息!"

上述结构取决于 io.horizon.spi.HorizonIo 接口下的实现逻辑,Zero中的实现逻辑因为历史原因,两个文件的基础 命名并没有维持一致,一个是 EXXXXX 一个是 XXXXX,但若 XXXXX 部分相同,则代表描述的是同一个错误代码对应的信息。此处设计成五位是考虑到在日志打印过程中让前缀的长度维持一致,一般框架内部或扩展使用五位,而您若开发其他应用则考虑六位

3.1.3.2. HorizonIo

对接框架,您只需要提供SPI接口 io.horizon.spi.HorizonIo 接口的相关实现,该接口的定义如下:

package io.horizon.spi;

import io.vertx.core.json.JsonObject;

/**
 * 资源文件加载专用SPI模式
 * - 日志器:HLogger 是高阶实现,Annal 为Zero默认实现
 * - 资源加载器:
 * --- spring 中加载 application-error.yml
 * --- vertx zero 中加载 vertx-error.yml
 * - 最终实现完整加载流程
 * 该组件SPI为底层资源加载组件,用于如下作用
 * 1. 对接不同的 Annal 扩展组件,实现日志器的替换扩展流程。
 * 2. 对接错误信息的资源提取流程,提取错误信息专用,构造成一个JsonObject包含所有资源类错误信息定义。
 *
 * @author lang : 2023/4/28
 */
public interface HorizonIo {
    /**
     * 资源加载,加载对应的异常资源文件,内部实现可自定义
     *
     * @return {@link JsonObject}
     */
    JsonObject ofError();

    /**
     * 资源加载,加载对应的异常资源文件,和 ofError() 可成对出现
     * 该方法返回的内容可直接提取可读部分,用于前端展示
     *
     * @return {@link JsonObject}
     */
    JsonObject ofFailure();

    /**
     * 日志获取器,可读取扩展日志类型,实例时基于 Class<?>
     *
     * @return {@link io.horizon.specification.uca.HLogger}
     */
    default Class<?> ofLogger() {
        return null;
    }
}

上述接口实现过程中,解释一下:

  1. ofError 用于返回 异常代码/系统信息表

    格式如:EXXXXX = xxxxx

  2. ofFailure 用于返回 异常代码/业务信息表

    格式如:XXXXX = xxxxx

  3. ofLogger 用于返回 io.horizon.specification.uca.HLogger 实现类名,反射替换原始日志记录器(若不替换则使用默认的 slf4j 接口。

上述实现完成之后,您可以在 /META-INF/services/io.horizon.spi.HorizonIo 中追加默认SPI的实现类,在自己的项目中直接对接异常架构,如此您的系统就可以享受 zero-ams 带来的完整的可配置异常架构和函数模型,若您使用的是 Zero 框架,则不需要做任何对接,默认的 zero-co 中已经包含了和 Zero 相关的所有实现。

最后需说明一点,上述接口的实现在 JVM 中会出现三种不同场景,都和类加载器有关:

  • 标准场景:直接使用单个JVM运行 jar 程序,这种模式下无需额外配置。

  • OSGI场景:由于OSGI场景下类加载器是相互隔离或独立的,所以在实现 HorizonIo 的过程中,您需要先精确定位类加载器,再读取相关配置才可以。

  • Spring场景:Spring 框架对类加载器本身做过调整,特别是IO读取文件这一层,若要对接 Spring 您必须保证底层 IO 访问的资源是可达的。

3.2. 编程规范

引入标准化项目 zero-ams 后,整个Zero框架体系已经形成了一套完整的特殊编码规范,由于此编码规范并非面向开发人员,只是提供给开发人员理解 语义化 定义部分,并理解整个架构。

io 作为根包名是历史原因,源于 io.vertx 最初的使用,所以Zero框架内大多数包名都是 io 前缀。

3.2.1. 核心术语

Zero中为了区别于其他框架,定义了部分核心术语,从高阶层面防止包重名的情况发生,部分命名依旧遵循原始Java定义(带 * 号部分):

基础包名

单词 翻译 含义

horizon

地平线

「基础功能包」隐喻为土壤,能够看见地平线的地方,都有着肥沃土壤,供生长。

macrocosm

宏观

「云端」隐喻为宇宙和整体,代表着生灵上了云端之后的宏观世界。

modello

模型

「建模」意大利语中的大型艺术品,隐喻为建模本身就是打造精致的艺术品。

annotations*

注解

对应Java注解。

atom

原子、微粒

对应Java中定义模型和数据对象,以及静态DTO。

eon

永久、千万年

对应Java中的常量。

em

(无)

Enum缩写。

exception*

异常

对应Java中的异常。

fn

(无)

Function缩写,对应Java的函数操作。

runtime*

运行时

运行时数据结构、缓存、内存存储空间。

specification*

规范

元模型标准。

spi

(无)

Service Provider Interface缩写。

uca

(无)

User Component Architecture缩写。

util*

效用

工具类专用缩写。

职能名

单词 翻译 含义

action*

操作

执行行为专用定义。

app*

应用

Application缩写,应用定义。

boot

启动

启动器。

cache*

缓存

缓存架构专用定义。

cloud*

io.macrocosm 包中的相关定义。

common*

常用

常用定义。

context*

上下文

上下文环境。

error*

错误

异常专用定义。

fs

(无)

File System文件系统缩写。

modeler

建模

模型设计器 / 建模执行器。

typed*

类型

基础类型定义。

3.2.2. 包规范

基于上述基础术语定义,zero-ams 中的包职能如下:

3.2.2.1. 水平底座

水平底座位于包 io.horizon 中,horizon 含义为水平线,在整个底层框架中充当了横向扩展规范定义,后续 元模型云原生 两个扩展都是基于该底座而定义的。

包名 类别 含义

io.horizon.annotations

注解

标准规范注解专用定义包,定义了 元数据规范 的特殊注解。

io.horizon.atom

内置静态模型、数据对象(DTO)专用定义包,定义了规范本身所需类:

  • app:应用级模型定义

  • common:常用功能级模型定义

io.horizon.eon.em

枚举

全局枚举定义:

  • app:应用级枚举定义

  • typed:数据类型专用枚举定义

  • uca:组件级枚举定义

  • web:容器级专用枚举定义

io.horizon.eon.error

枚举/接口

接口常量定义,内置异常代码专用定义,不绑定资源文件。

io.horizon.eon

接口

接口常量定义,全局专用常量定义。

io.horizon.exception

抽象类

抽象异常架构,内置部分不同类型的异常实现:

  • internal:内置异常实现

  • web:如常见的500、400、401、403等异常实现

io.horizon.fn

函数接口/HFn

函数专用API统一接口,扩展函数接口定义( @FunctionalInterface

io.horizon.runtime

接口

接口常量定义,运行时专用常量:

  • cache:运行时缓存

io.horizon.specification

接口

标准规范 interface 定义:

  • action:基础操作规范

  • app:应用接口规范

  • typed:数据类型专用底层语言级规范

  • uca:组件专用规范

io.horizon.spi

接口

标准规范 SPI 定义:

  • cloud:云原生基础SPI

  • typed:数据类型专用SPI

io.horizon.uca

类/接口

User Component Architecture用户组件架构扩展组件定义包。

io.horizon.util

类/HaS

静态工具类,只有 HUt 对外暴露。

3.2.2.2. 元模型

元模型位于包 io.modello 中,modello 含义为:意大利语中的(大型艺术作品的)模型,为了区别于 modeler 做建模实现,刻意使用了 modello 单词,元模型中只定义了 建模/模型平台 专用接口部分( interface 定义)。

包名 类别 含义

io.modello.atom

建模专用静态模型定义。

io.modello.emf

接口/类

基于Eclipse专用的EMF对接规范定义。

io.modello.specification

接口

建模专用规范接口定义。

io.modello.util

建模专用工具类。

3.2.2.3. 云原生

云原生位于包 io.macrocosm 中,macrocosm 含义为:宇宙、宏观世界,隐喻为:云端,同样是为了区别于 cloud 做云原生实现,刻意是了 macrocosm 单词,云原生中之定义了 原生云 专用接口部分( interface 定义)。

包名 类别 含义

io.macrocosm.atom

云原生专用静态模型定义。

io.macrocosm.specification

接口

云原生专用规范接口定义。

元模型和云原生都是标准水平底座规范的子扩展规范,为了拥有 单一职责,只做了分包,不做模块分离(位于同一个项目),且包的核心结构基本维持一致,并且可实现完整模块化。云原生/元模型 两个扩展规范中的SPI部分依旧位于 io.horizon 包中,其中:

  • io.horizon.spi.cloud 中定义了云原生SPI

  • io.horizon.spi.modeler 中定义了元模型SPI

3.2.3. 类名规范

类名规范在整个Zero框架中使用了:前缀法反人类 命名规则,之所以说反人类命名规则实际是一般Java程序员无法接受的命名规则,但从实战却发现在排序和视觉上不错的使用规则。

3.2.3.1. 缩写含义

Zero 扩展规范中经常会出现三字母类,这些类都采用了缩写模式,全大写或偶尔有小写。

缩写 全称 含义

HOI

High Order Owner ID

拥有者、租户统一标识。

HET

High Order Environment Tenant

租户专用上下文环境,云端使用。

HED

High Order Encrypt / Decrypt

高阶加密解密模块。

HFS

High Order File System

高阶抽象文件系统。

HFn

High Order Function

(工具类)高阶函数统一接口,通常使用时继承(语法继承)。

HUt

High Order Utility

(工具类)高阶工具统一接口,通常使用时继承(语法继承)。

3.2.3.2. 类名前缀

Zero 定义的特殊类通常都带有类名前缀,不同前缀含义有所差异。

前缀 全称 含义 说明

H

High Order

高阶类

io.aeon 包(Aeon云原生平台)以及 zero-ams 中常用前缀,包括接口和类。

C

Cache

缓存类

全局 interface 接口缓存常量专用类。

K

Kernel

核心

用于定义规范和标准专用的核心类,可作为模块之间数据规范下的 DTO 进行传输。

V

Value

值相关

通常用于定义常量文件,只有 interface 接口定义的常量类。

T

Type

类型

数据结构专用类,通常定义和数据结构相关的内容。

R

Reference

外部关联

建模过程中专用的引用实现类名,用于描述模型和模型之间的关系。

M

Model

模型

建模过程中专用的模型类名,用于描述模型本身。

__

(无)

包内私有

(双下划线)用于定义某个 package 内部使用的数据结构。

_

(无)

原型链

(单下划线)仅用于包内继承语法,实现静态类原型链,以减少代码去重。

_XXX

(无)

Web异常

(单下划线,带状态码)仅用于 Web类型的异常定义。

3.2.3.3. 变量前缀

Zero 定义的常来的前缀会在某种程度和类形成绑定,不同前缀其含义有所区别。

范围 规则 含义

包内

CACHE

通常在包内某个接口之外定义不带访问修饰符的缓存Cc连接位置,可独立文件,也可直接在某个接口之外直接定义:

interface CACHE {

    @Memory(ED.class)
    Cc<String, ED> CCT_ED = Cc.openThread();
}

包内

__MESSAGE

通常用来定义包内某个类使用的消息输出信息,如:

interface __MESSAGE {
    interface Io {
        String INF_PATH = "「I/O」Absolute path is hitted: {0}.";
    }
    // io.horizon.util.io 类专用消息
}

包内

__T

包内专用工具类缩写,如果有多个可直接放到单独文件中。

包内

_ 前缀

原型链 专用写法,只用于静态工具类。

全局/包内

缓存类型

Cc缓存架构专用,包含几个子规则:

  • CC_ :全局缓存专用名。

  • CCT_ :线程缓存专用名。

  • CCA_ :异步缓存专用名,异步缓存不考虑全局或线程级。

3.2.3.4. 类名后缀

Zero 定义的类名后缀只有三种,且一般用于枚举:

  • Type:表示类型

  • Mode:表示模式

  • Status:表示状态

3.2.3.5. 特殊类名

Zero 中还定义了两种规则的特殊类名:

  • _ 前缀的类名,这种类名一般在包内或私有部分通常会使用,左侧IDE排序会排列在最前边。

  • 小写类名,Zero中类名小写一般是和树型配置文件绑定,如 YAML / JSON,绑定之后类名本身是配置数据中的节点名,这样的编码规范可以避免开发人员在众多配置中去搜搜常量信息,您的配置文件是如何树型排版,那么在代码这一级,定义就是如何树型构造,最终会形成十分直观的配置结构。

如下图:

zams name spec

反人类的点就在于Java语言中很少会使用下划线 _ 做类名,而 Zero 中不仅使用了 下划线,还使用了 双下划线,但当你使用IDE打开项目并且从结构上查看时,就可以看到这样的类名可以一眼让您对某个包中的所有定义很清楚,文件排序时某些相同职能的类会自然排到一起,方便开发人员做项目维护。

Zero中所有的命名规范和基础都是为了代码阅读以及和IDE互动,包括对设计模式的使用也是为代码阅读量身打造。

3.2.4. 高阶接口

Zero高阶设计中会包含三种接口,分别对应包名参考下表:

类型 含义

Horizon

io.horizon.specification

基础层、配置层、环境层。

Macrocosm

io.macrocosm.specification

云原生,平台层。

Modello

io.modello.specification

模型层、集成层、业务对接层。

Mature

io.mature.extension

(非接口)标准化组件、插件。

接口规范章节会按三种不同的接口分别梳理,:上述提到的三种接口全部使用 interface 定义,没有任何实现类。

3.3. 编程技巧

3.3.1. 元数据注解

Zero中的 zero-ams 提供了四个核心的元数据注解,这些注解位于 io.horizon.annotations 包中。

注解 位置 含义和使用

Development

方法

该注解用于标注只有开发过程中才会使用的方法,如:

    @Development("IDE视图专用")
    private int __11001() {
        return this.getCode();
    }

HighOrder

方法

该注解用于标注 非继承 模式调用了 HFn/HUt 工具类的地方,以方便系统反向扫描哪些位置需要调整和升级,如:

    @HighOrder(HUt.class)       // 此方法调用了 HUt 中的方法
    public static <K, V> V pool(final ConcurrentMap<K, V> pool,
                                final K key, final Supplier<V> poolFn) {
        return HUt.pool(pool, key, poolFn);
    }

Legacy

该注解用于标注抽象分裂之后,原始对象依旧在使用的类,由于反射现阶段无法直接 @Deprecated 的部分,如:

@Legacy("旧版由于使用反射无法直接重命名,"
    + "所以保留了Zero内部的数据库定义,并且该定义位于 zero-atom 核心位置,"
    + "不可以直接被取消,但该类可从 KDatabase 高阶对象中继承"
)
public class Database extends KDatabase {
    // ......
}

Memory

成员

该注解用于标注 Cc 缓存架构中使用的标准化缓存相关信息,如:

    @Memory(KApp.class)
    Cc<String, KApp> CC_APP = Cc.open();
    @Memory(HOI.class)
    Cc<String, HOI> CC_OI = Cc.open();

3.3.2. 自定义异常

3.3.2.1. 完整异常树

虽然前文中有说明,此处依旧枚举下整体抽象异常树:

  • 父类:io.horizon.exception.AbstractException(运行时异常)。

    • 内部异常:io.horizon.exception.InternalException

    • 启动异常:io.horizon.exception.BootingException

    • Web异常:io.horizon.exception.WebException

  • 父类:io.horizon.exception.ProgramException(检查类异常)。

    • 守护进程异常:io.horizon.exception.DaemonException

3.3.2.2. 异常代码表

系统内部定义异常表(带属性的枚举值),参考下边标准定义中的 ErrorCode 枚举:

package io.horizon.eon.error;

public enum ErrorCode {
    _11000(-11000,
        "Cannot find META-INF/services/ {} on classpath")
    , _11001(-11001,
        "( Depend ) ArgumentWrongException")
    , _11002(-11002,
        "The `filename` of io stream is empty, filename = `{}`")
    , _11003(-11003,
        "The error of `{}` has not been defined and could not be found")
    , _11004(-11004,
        "The system met json decoding/encoding exception: {}")
    , _11005(-11005,
        "This operation is not supported! ( method = {}, class = {} )")
    , _11006(-11006,
        "This method is forbidden and not supported when you called")
    , _11007(-11007,
        "Input `pool` argument is null, may cause NullPointerException / Terminal")
    , _11008(-11008,
        "The input key of `Pool` is null, it''s conflict in current environment")
    , _11009(-11009,
        "The input cache mode should not be null, please check your code")
    , _11010(-11010,
        "The META-INF/services/io.horizon.spi.BootIo component is null, please configure.")
    , _11011(-11011,
        "The Reflection invoking does not satisfy the pre-condition. Method = {}")
    ;
    private final String message;
    private final int code;

    ErrorCode(final int code, final String message) {
        this.code = code;
        this.message = message;
    }

    public String M() {
        return this.message;
    }

    public int V() {
        return this.code;
    }
}

这种定义会让每个枚举都带有 异常信息异常代码,并且可使用不同的方式直接输出:

方法名 含义

String M()

读取该异常信息的错误信息,错误信息可以是带有 {}{0} 的模式字符串。

int V()

读取该异常信息的错误代码。

异常代码规划如下:

区间 绑定资源 含义

-10001 ~ -11000

(无)

保留区间,往下的扩展异常区间

- -10001 ~ -10007:现阶段第一个区间用于容器启动级配置异常 DaemonException

-11001 ~ -15000

(无)

内部标准化异常,不绑定资源文件,从 11000 开始。

-15001 ~ -20000

(无)

应用内部异常,不绑定资源文件,从 15000 开始。

-20001 ~ -30000

绑定

第三方集成异常,通常是 WebException,运行时抛出。

-30001 ~ -40000

绑定

系统启动类异常,通常是 BootingException

-40001 ~ -50000

绑定

模块、功能启动类异常,通常是 BootingException

-50001 ~ -60000

绑定

云原生插件、容器、模块特殊异常。

-60001 ~ -70000

绑定

专用Web容器异常,通常是 WebException,运行时抛出。

-80001 ~ -90000

绑定

扩展模块专用异常,通常是 WebException,运行时抛出。

3.3.2.3. 编写方法

参考下边代码理解内部异常扩展和资源绑定型异常扩展的区别:

内部类型(使用ErrorCode)

package io.horizon.exception.internal;

import io.horizon.annotations.Development;
import io.horizon.eon.error.ErrorCode;
import io.horizon.exception.InternalException;
import io.horizon.util.HUt;

public class EmptyIoException extends InternalException {

    public EmptyIoException(final Class<?> caller, final String filename) {
        super(caller, HUt.fromMessage(ErrorCode._11002.M(), filename));
    }

    @Override
    protected int getCode() {
        return ErrorCode._11002.V();
    }

    @Development("IDE视图专用")
    private int __11002() {
        return this.getCode();
    }
}

资源绑定型

package io.horizon.exception.web;

import io.horizon.annotations.Development;
import io.horizon.eon.em.web.HttpStatusCode;
import io.horizon.exception.WebException;

public class _403ForbiddenException extends WebException {

    public _403ForbiddenException(final Class<?> clazz) {
        super(clazz);
    }

    @Override
    public int getCode() {
        return -60013;
    }

    @Override
    public HttpStatusCode getStatus() {
        return HttpStatusCode.FORBIDDEN;
    }

    @Development("IDE视图专用")
    private int __60013() {
        return this.getCode();
    }
}

书写 HorizonIo 的SPI组件实现的主要目的就是在此处实现资源绑定,若未提供实现,则资源绑定会失败。

此处提供默认资源绑定代码,若您想要使用默认的 WebException,这些错误代码是必须的。

错误代码 状态码 含义

-60011

400

标准 400 Bad Request 异常。

-60012

401

标准 401 Unauthorized 异常。

-60013

403

标准 403 Forbidden 异常。

-60059

412

标准参数检查异常,412 Precondition Failed,通常参数为 null 时有必要抛出该异常则使用。

-60060

500

协变 500 Internal Server Error 异常,深度调用 getCause 提取堆栈顶端。

-60007

500

标准 500 Internal Server Error 异常。

-80413

501

标准 501 Not Implemented 异常,方法未实现专用异常(编程过程忘记写)。

-60050

501

标准 501 Not Implemented 异常,方法不支持专用异常,强制空方法不支持。

-60022

400

(Qr专用)查询参数中的数据格式不合法:criteria, pager, sorter, projection 属性。

-60023

400

(Qr专用)查询引擎分析出来的 pager 参数中丢失了 sizepage 属性。

-60024

500

(Qr专用)元数据中出现了 NULL,如查询属性、查询操作、连接符。

-60025

400

(Qr专用)查询引擎分页参数的页码 < 1 时抛出该异常。

-60026

400

(Qr专用)查询引擎分析出来的操作符不在预定义中,操作符非法。

上述代码的消息模式参考如下(Zero框架部分,您可以按照代码中的 {} 部分重写,注意参数对齐,您可以直接拷贝到自己的资源文件中):

# ----------- AMS 部分
# BootException: io.horizon.exception.boot
#    E40101 = CombineAppException
#    E40102 = CombineOwnerException
#    E40103 = AmbientConnectException
#    E40104 = DuplicateRegistryException
# New for Booting
E40101: "(Boot) The two app of HArk could not been combined, the current name = `{1}`, ns = `{0}`"
E40102: "(Boot) The two owner of HArk could not been combined, the current id = `{0}`, target = `{1}`"
E40103: "(Boot) The container try to connect HArk, but `HArk` is null and could not start up"
E40104: "(Boot) The stored Ambient has already been registry, current stored size = `{0}`"
# WebException: io.horizon.exception.web
# -- Web
#    E60011 = _400BadRequestException
#    E60007 = _500InternalServerException
E60007: "(500) - The system detected internal error, contact administrator and check details = {0}"
E60011: "(400) - The system detected invalid ( Bad ) request in request"

# -- Secure
#    E60012 = _401UnauthorizedException
#    E60013 = _403ForbiddenException
E60012: "(401) - (Security) Unauthorized request met in request"
E60013: "(403) - (Security) Access denied/forbidden in request"

# -- Qr
#    E60022 = _400QQueryAttributeException
#    E60023 = _400QPagerInvalidException
#    E60024 = _500QQueryMetaNullException
#    E60025 = _400QPagerIndexException
#    E60026 = _400QOpUnknownException
E60022: "(400) - (Ir) You''ll try to parse Ir key = \"{0}\", the expected type is {1} but now it''s {2}"
E60023: "(400) - (Ir) You''ll try to build \"pager\", the key \"{0}\" of pager missing."
E60024: "(500) - (Ir) Your query meta is null, expected input meta object is not null reference."
E60025: "(400) - (Ir) The \"pager\" started page number should start from 1, now input \"page\" is {0}"
E60026: "(400) - (Ir) The op is not supported, please provide valid op of \"<, >, <=, >=, =, <>, !n, n, t, f, i, !i, s, e, c\", current is {0}"

# -- JVM
#    E60050 = _501NotSupportException
#    E60059 = _412ArgumentNullException
#    E60060 = _500InternalCauseException
#    E80413 = _501NotImplementException
E60050: "(501) - This api is not supported or implemented, please check your specification, source = {0}"
E60059: "(400) - (Progma) Detected Null Value = `{0}`"
E60060: "(500) - Method invoking met exception, but could not find WebException, ex = {0}"
E80413: "(501) - (Jet) This api Method does not implement here, please provide in programming"

新版本中由于完整的启动器架构已经实现和落地,所以多出了几个新的异常,但此异常的使用取决于您使用了Zero原生的启动器,若不想使用此启动器而是自己设计或实现一套新的,这四个异常代码是非必须的,所以没有在表格中列出。

还有一点需要说明是 WebException 的子类文件名使用 _XXX 前缀,此处 _XXX 中的 XXX 表示HTTP状态码,有了这种定义可以从文件名中直接观察该异常信息,再配合 @Development 部分的定义,您就可以看到如下截图:

0

  1. 截图中可以直接看到这个异常返回的HTTP状态码:501

  2. 由于使用了 @Development 注解,这个异常的错误信息码可直接使用反射提取。

  3. 该异常的定义中包含了 __60050() 私有方法,该方法不能不调用,但可以在IDE中看到。

3.3.3. 常量/工具原型链

最后提到的一个编程技巧是Java语言级的一个知识点:Java语言中静态方法和静态类不能被直接继承,但是使用 extends 关键字时,接口中的常量可以从子类调用,而方法也可以从子类调用,不仅如此,若子类定义了重名方法时,父类的方法将会被隐藏(而不是重写)。本章以 HUt 中的方法扩展为例:

3.3.3.1. 步骤一:书写包内方法

参考截图中的 HSPI 实现:

0

package io.horizon.util;

final class HSPI {
    private HSPI() {
    }
}

说明:

  1. 该类是 final class 修饰,即不可以被继承,加上是包域访问,只能在 io.horizon.util 包中使用。

  2. 该类的构造方法是 private 修饰,所以不可以被实例化,形成了标准的 工具类

3.3.3.2. 步骤二:书写功能类

参考截图中的 _Reflect 实现:

0

package io.horizon.util;
class _Reflect extends _Random {
    protected _Reflect() {

    }
}

说明:

  1. 该类是包域访问,只能在 io.horizon.util 包中使用。

  2. 由于构造函数是 protected 修饰,所以只能在子类中调用构造函数。

  3. 而且该类从 _Random 中继承,也可以使用该静态方法。

3.3.3.3. 步骤三:原型链

原型链是Zero中工具类的一种特殊的情况,所有原型链上的类全部使用 _ 单下划线前缀,实现整体的限制和开放,如 HUt 的核心原型链如:

0

上述截图中,左侧所有的类都是按文件排序继承的,如三份定义如:

// 顶层定义
class _Color {
    protected _Color(){}
}
// 中间定义
class _Compare extends _Color {
    protected _Compare() {
    }
}
// 底层定义
public class HUt extends _Value {
    private HUt() {
    }
}

此处定义方式和JavaScript中的原型链很像,所以取名为 原型链 定义,这样设计的好处在于:

  1. 所有父类只有方法是 public,由于构造函数是 protected 子类才能调用,而只有最底层 HUt 是对外的,意味着整个链上的类都是不可以构造实例的,满足工具类的特征。

  2. 原型层充当了 interface 接口层,会直接调用包内的功能部分,功能部分按职责进行区分,同样是 private 的构造函数,不可以被实例化,且只能在包内使用。

  3. 将原版的 HUt 归口整理成了原型链统一归口,原型链按函数前缀划分,执行了职能的二次组合,如:

    0

    左侧的 _EDS 为加密解密模块专用方法,真正调用时可直接使用 X.encryptMD5() 的方式,而此处加解密的方法来自不同的职能类(加解密、编解码),如此分散之后,所有的代码内部行数都在纯逻辑的 200 ~ 300 行范围内,而注释直接写在原型类中。

3.3.3.4. 步骤四:使用

在您的程序中,推荐不直接使用 HFn / HUt 两个类,虽然这两个类是 public,直接找另外一个应用型的工具类,从 HUt 直接继承,您就可以在任何地方使用了,如:

继承 HUt

// 应用级工具统一归口
public final class Ut extends HUt {
    private Ut() {
    }
}

调用工具方法

    final JsonObject qbeJ = Ut.toJObject(Ut.decryptBase64(qbe));
    return hqbe.before(qbeJ, envelop);

从上边调用代码可以知道,decryptBase64 并没有在 Ut 中定义,而是位于原型链中的 _EDS 类中定义,但由于Java语言特性,我们依旧可以在 Ut 类使用时直接操作:Ut.decryptBase64 来调用该方法,并且可以防止在包外其他地方调用该方法,就完成了工具类的最终修订版本,而我们自定义的方法可以参考这种模式依旧在 Ut 类所在的包中继续 原型链 扩展,并且由于 Ut 限定了 private 方法(原型的最后一个子类使用),任何地方都没有办法实例化我们的类(不造成内存损耗),且可以像调用JavaScript全局函数一样直接调用我们书写的 静态方法

3.4. 设计思路

3.4.1. 设计原则

本章为新追加章节,用于处理 zero-ams 的设计原则,让它可支持核心的系统级功能:

Zero开扩展模块不需要做任何和 CRUD 相关的事,所以 业务应用 中不包含基础模块的 15 个标准化流程,此流程已经处于持续迭代过程。

主分类 功能点

JVM语言级

同步、异步编排处理,函数模式的编程,支持流模式计算

日志处理,后期云端可直接对接 ELK 服务实现日志系统集成

容错,现阶段课直接使用 Zero 提供的容错扩展系统

IO处理,包括资源文件加载、国际化、桥接动态资源配置等

JNI本地接口,SPI模块化解耦,OSGI热部署解耦

业务应用

配置管理:应用配置、模块化配置管理

菜单、页面、LIST/FORM 的加载管理

个性化配置管理

生命周期管理:启动、停止、激活、钝化

报表模块:基于配置的报表管理器

静态 / 动态模块配置桥接,标准化,可扩展模式实现

对外集成

集成接口访问,统一配置集成插件和集成通道

软件集成接口规范,包括邮件、提醒、短信、支付接口

硬件接口规范,包括身份证扫描、刷卡机、智能门锁、电路控制

云原生

多租户、多语言容器对接层

租户之下的多应用对接层、多模块的模块化、模块授权处理

发布模型,对接 K8S 中的 Pod/Service 等概念实现完整模块发布

监控链路层,针对现阶段组件运行状态提供看板可查看详细数据内容

网格管理,Service Mesh 对接。

建模

元模型定义,如:类型、属性、枚举、注解、接口等相关定义

范围定义:名空间、包、应用空间,对接 JMX

模型定义:模型、属性、引用、约束、键、规则、实体、表空间等

语义:多态、继承、派生、组合,和面向对象完全相关语义定义

标准化组件:服务、事件、触发器、任务、调度器、过滤器、标准化

行为定义:针对模型的各种操作型接口基础规范

开发中心(基于模型)

表单设计器:排版、多状态、字段、验证规则、依赖处理

任务管理器:定时任务、触发任务、周期任务

接口设计器(包括集成接口设计器):RESTful被动式、IOT信号接口、WebSocket主动式

事件管理器:发布订阅,支持事件网和处理器单独扩展

报表设计器:类似Excel的透视图

页面配置器:页面流、布局选择、模板化,前端高阶组件

流程设计器:流程图设计、节点定义、规则

图(脑图、拓扑)管理器:实体核心定义、关联关系定义、组织结构定义

规则定义:验证规则、触发规则、过滤规则、转换规则、标识规则等核心规则统一定义

硬件控制台:看板监控、本地对接、SDK调用

3.4.2. 模块协同

目前 zero-ams 的子包有三个,分别承担不同职责的应用:

包名 接口规范包 含义

io.horizon

io.horizon.specification

水平接口规范的专用包,定义全系统接口。

io.macrocosm

io.macrocosm.specification

云原生对接规范的专用包,专用于云原生、数字化、物联网、人工智能。

io.modello

io.modello.specification

建模管理规范的专用包,做语言级、应用级、工业级模型接口定义。

子包规范如下(如 action 则表示 io.horizon.specification.action 等):

子包名 含义和职责

action

针对模型的行为接口

typed

原子类型定义

config

配置专用

uca

自定义组件

boot

启动器

atom

建模核心(非原子类型)

element

标准和规范

meta

元模型专用(模型的模型)

app

应用容器和配置管理

program

开发设计中心

secure

安全管理

tenant

租户领域

4. 机器之心

5. 高阶之 Horizon

5.1. HInstall

「模块启动器」io.horizon.specification.boot.HInstall

5.1.1. 基本介绍

模块启动器主要用于模块生命周期(安装/卸载)管理,此接口使用了泛型定义模块类型,可用来管理不同种类模块的生命周期。

常用可管理模块如:

名称 依赖方式 含义

i config 扩展模块

YML/JSON模式

Zero 标准框架中的静态配置模式。

i config 标准模块

Maven模式

Zero Extension 静态可扩展模块。

i config t 配置模块

数据模式

Zero Extension 中动态建模( zero-atom )专用的方式,和 X_MODULE 配合实现可配置的热部署模块。

i config t OSGI模块

类加载模式

基于 OSGI 规范的标准化模式,和 OSGI 中的 Bundle 的生命周期绑定。

上述模块中两种模块的区别:

  • i config 静态方式

    依赖容器重启,扩展过程中虽然不需重编译,但由于配置数据是存储在静态文件中的,等价于配置数据本身是只读的,您不可以在运行状态下去修改配置达到所见即所得的效果,这也是静态一词的由来。

  • i config t 动态方式

    动态方式下,容器 无需重启,配置数据本身可能存储在数据库、配置中心或其他可委托管理的位置,这样的场景下,您就可以通过修改配置所见即所得地对系统进行扩展。

上述两种不同方式的扩展近似于:JDK 9 中的模块化 和 OSGI 模块化的区别。

5.1.2. 泛型:BND

细心的小伙伴会发现,HInstall 的接口定义如下:

public interface HInstall<BND> {

}

此处的 BND 表示您的模块类型,不同模块类型其实现模式不同,使用泛型进行限定降低此接口的抽象级别,当您提供实现时可以根据具体的模块类型来选择,而 BUD 本身作为参数会更加友好。

比如您可以使用下边代码创建 OSGI 模块的 启动器

public class TestInstall implements HInstall<Bundle> {

}

5.1.3. 生命周期

模块 启动器 的整体生命周期规划如下:

签名 必须 含义

安装:install(BND)

安装模块专用生命周期。

配置:configure(BND)

注册模块过程的配置处理或预处理。

启动:start(BND)

启动模块、激活模块。

停止:stop(BND)

停止模块(若存在容器则停止容器)。

注销:unregister(BND)

注销模块,将模块本身从环境中移除。

清理:cleanup(BND)

模块注销之后的回调,用于资源释放。

此接口中包含了模块基于生命周期的方法定义,其参考为 OSGI 中的生命周期:

zams i HInstall

注意上图中蓝色是 OSGI 的标准触发动作(非接口定义),而绿色则是 HInstall 定义的核心方法,最后有一点需说明:HInstall 接口不负责刷新、重试等迭代型行为,而上述生命周期中针对 refresh 动作会有额外的接口来实现,这样做到组件定义的职责分离。