架构治理调研:规则、表达式还有语言

开发 开发工具 架构
为了在 ArchGuard 中完善分布式规范的能力,便分析了几个现有的工具。

我们谈论到了 “分布式” 场景下,对于架构治理和规范治理的一系列问题。我们提及了一系列的工具,如 API Linter 工具 Spectral,数据库 Linter 工具 SQLFluff。而为了在 ArchGuard 中完善分布式规范的能力,便分析了几个现有的工具。

对于我们来说,构建一个类似的工具,需要考虑的一些因素有:

  • 插件化。开发人员可以根据已有的守护规则,开发一些新的架构守护规则,如针对于 API 的,针对于数据库调用链路的。
  • 可测试性。如果采用的是完全 DSL 或者 半 DSL,那么如何让后续的
  • 语言无关。如何不绑定于语言的语法树,而实现对于多种语言的支持。

出于这个目的,只好拿起现有的代码进行一番分析,主要有四个工具,适用于 Kotlin 语言的 KtLint、适用于 OpenAPI 的 Spectral、适用于多数据库的 SQLFluff,以及被诸如 MyBatis 采用的表达式语言 Ognl。

Kotlin 代码的治理:KtLint

KtLint 与一般的 Lint 工具稍有区别的是,它自带了一个自动格式化的功能。KtLint 整体的逻辑还是比较简单的,基于单个文件进行 AST 生成,随后针对于 AST 进行规则匹配。Ktlint 围绕于 Rule、Rulesets、RulesetsProvider 构建了规则的层级关系,同时用 Vistor (即 VisitorProvider)模式围绕 AST 进行分析,如下是 KtLint 的抽象 Rule:

/**
* This method is going to be executed for each node in AST (in DFS fashion).
*
* @param node AST node
* @param autoCorrect indicates whether rule should attempt auto-correction
* @param emit a way for rule to notify about a violation (lint error)
*/
abstract fun visit(
node: ASTNode,
autoCorrect: Boolean,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
)

如注释中所说的,三个参数代表了各自的用途。这里的 ASTNode 是来源于 Kotlin 的 AST 树( kotlin-compiler-embeddable 包)。模式上也是获取配置,然后运行检测规则:

val ruleSets = ruleSetProviders.map { it.value.get() }
val visitorProvider = VisitorProvider(ruleSets, debug)

其中对应的 visit:

visitorProvider
.visitor(
params.ruleSets,
preparedCode.rootNode,
concurrent = false
).invoke { node, rule, fqRuleId -> }

在 VistorProvider 中会过滤对应的规则:

val enabledRuleReferences =
ruleReferences
.filter { ruleReference -> isNotDisabled(rootNode, ruleReference.toQualifiedRuleId()) }
val enabledQualifiedRuleIds = enabledRuleReferences.map { it.toQualifiedRuleId() }
val enabledRules = ruleSets
.flatMap { ruleSet ->
ruleSet
.rules
.filter { rule -> toQualifiedRuleId(ruleSet.id, rule.id) in enabledQualifiedRuleIds }
.filter { rule -> isNotDisabled(rootNode, toQualifiedRuleId(ruleSet.id, rule.id)) }
.map { rule -> "${ruleSet.id}:${rule.id}" to rule }
}.toMap()
....

然后,再去并行或者串行地运行 Rule 里的 visit。

而对于规则的方式是通过 ServicesLoader 进行的插件化方式:

private fun getRuleSetProvidersByUrl(
url: URL?,
debug: Boolean
): Pair<URL?, List<RuleSetProvider>> {
if (url != null && debug) {
logger.debug { "JAR ruleset provided with path \"${url.path}\"" }
}
val ruleSetProviders = ServiceLoader.load(
RuleSetProvider::class.java,
URLClassLoader(listOfNotNull(url).toTypedArray())
).toList()
return url to ruleSetProviders.toList()
}

如果粒度更大的情况下,采用 Java 9 的模块是不是会更加方便?

基于 API 数据的 Spectral

与 Ktlint 不同的是 Spectral 是一个针对于 JSON/YAML Lint 的工具,特别是针对于 OpenAPI 文档(就是 swagger 的 yaml/json 文件)。与 Ktlint 相比,Spectral 最有趣的地方是,它提供了一个 JSON Path(类似于 XPath)的功能,可以针对于对象中的特定部分,进采用特定的规则。如下是 Spectral 的示例:

'oas3-valid-schema-example': {
description: 'Examples must be valid against their defined schema.',
message: '{{error}}',
severity: 0,
formats: [oas3],
recommended: true,
type: 'validation',
given: [
"$.components.schemas..[?(@property !== 'properties' && @ && (@ && @.example !== void 0 || @.default !== void 0) && (@.enum || @.type || @.format || @.$ref || @.properties || @.items))]",
"$..content..[?(@property !== 'properties' && @ && (@ && @.example !== void 0 || @.default !== void 0) && (@.enum || @.type || @.format || @.$ref || @.properties || @.items))]",
"$..headers..[?(@property !== 'properties' && @ && (@ && @.example !== void 0 || @.default !== void 0) && (@.enum || @.type || @.format || @.$ref || @.properties || @.items))]",
"$..parameters..[?(@property !== 'properties' && @ && (@ && @.example !== void 0 || @.default !== void 0) && (@.enum || @.type || @.format || @.$ref || @.properties || @.items))]",
],
then: {
function: oasExample,
functionOptions: {
schemaField: '$',
oasVersion: 3,
type: 'schema',
},
},
}

上面对象中的 given 即是针对于对象中的相关属性作为条件,执行后面的 then 函数,详细可以见官方的文档:《 Custom Rulesets 》。顺带一提:Spectral 采用的是 nimma 作为 JSON Path 表达式。

Spectral 的模型

与 Ktlint 相比,由于 Spectral 是与 OpenAPI/Async API 进行了相关的绑定,加上特定的规则表达式,所以其数据模型稍微复杂一些。其数据模型包含了:描述,消息级别,given - then,上下文。如下所示:

  • recommended。是否是推荐配置。
  • enabled。是否允许
  • description。规则描述
  • message。错误信息
  • documentationUrl。文档地址。
  • severity。严重程度,`error`, `warn`, `info`, or `hint`。
  • formats。格式化标准,如 OpenAPI 2.0、OpenAPI 3.0 等。
  • resolved。是否已解决。
  • given。类似于 CSS 中的选择器,使用类似于 XPath 的 JsonPath, JSONPath
  • then。
  • field,字段
  • function,函数,模式
  • functionOptions

此外,它还有一个简单的类型系统,以及对应的表达式判断。如下:

  • CASES。flat、camel、pascal、kebab、cobol、snake、macro
  • 长度:最大值、最小值。
  • 数字
  • Boolean 判断。
  • 类型系统。枚举

总的来说,Spectral 在实现上比较灵活有趣。

SQLFluff

与 Ktlint 和 Spectral 这种基于已有的数据模型的应用来说,SQLFluff 显得更有挑战性 —— 它是基于多种不同的数据库方言来构建规则的。SQLFluff 是直接基于源码来进行分析的,将不同的数据库方言转换为基本元素(分词)。随后,基于分词的类型 + 规则 ,来对它们进行处理。简单来说,就是更抽象的分词上下文,构建对应的规则上下文。如下是

  • segement。位于其核心的是 BaseSegment,它定义了 Lexing、Parsing 和 Linting 三个基本的元素,产生诸如: groupby_clause  orderby_clause  select_clause 等分词。
  • parent_stack。
  • siblings_pre。
  • siblings_post。
  • raw_stack。
  • memory。
  • dialect。作为语法运行时解析的基础。
  • path。路径。
  • templated_file。模板文件。

示例:

{
"file": {
"statement": {
"select_statement": {
"select_clause": {
"keyword": "SELECT",
"whitespace": " ",
"select_clause_element": {
"column_reference": {
"identifier": "foo"
}
}
},
"whitespace": " ",
"from_clause": {
"keyword": "FROM",
"whitespace": " ",
"from_expression": {
"from_expression_element": {
"table_expression": {
"table_reference": {
"identifier": "bar"
}
}
}
}
}
}
},
"statement_terminator": ";",
"newline": "\n"
}
}

随后的规则,便是在对这些规则进行 eval ,如下示例:

class Rule_L021(BaseRule):
def _eval(self, context: RuleContext) -> Optional[LintResult]:
"""Ambiguous use of DISTINCT in select statement with GROUP BY."""
segment = context.functional.segment
if (
segment.all(sp.is_type("select_statement"))
# Do we have a group by clause
and segment.children(sp.is_type("groupby_clause"))
):
# Do we have the "DISTINCT" keyword in the select clause
distinct = (
segment.children(sp.is_type("select_clause"))
.children(sp.is_type("select_clause_modifier"))
.children(sp.is_type("keyword"))
.select(sp.is_name("distinct"))
)
if distinct:
return LintResult(anchor=distinct[0])
return None

在这里所有的规则判断都是基于这种抽象的语法树。从某种意义上来说,构建了一个统一的抽象。本来想进一步分析,但是发现各种 SQL dialect 里是各种正则表达式,我就选择了临时性撤退。

表达式语言:OGNL

起初,我是在实现 ArchGuard Scanner 对于 MyBatis 的 SQL 生成支持时,看到了 XML 中嵌套的 OGNL 表达式,发现了 OGNL。从实现上来说,它比我之前设想的 TreeSitter 中的 S 表达式,在与数据结合的完善度上更高。同样,也可以用于这里的规则判断,可以用表达式来对数据进行匹配。

对象导航图语言(Object Graph Navigation Language),简称 OGNL,是应用于 Java 中的一个开源的表达式语言(Expression Language),用于获取和设置 Java 对象的属性,以及其他附加功能,例如列表投影(projection)和选择以及 lambda 表达式。您可以使用相同的表达式来获取和设置属性的值。Ognl 类包含了评估 OGNL 表达式快捷方式。它可以分两个阶段执行此操作,将表达式解析为内部形式,然后使用该内部形式设置或获取属性的值;或者可以在一个阶段完成,并直接使用表达式的字符串形式获取或设置属性。

Ognl.getValue("name='jerry'", oc, oc.getRoot());
String name2 = (String) Ognl.getValue("#user1.name='jack',#user1.name", oc, oc.getRoot());

本来想模仿 OGNL 编写一个表达式语言,但是发现使用的是 Jacc,也没有 Antlr 实现。所以,在寻找一种更合理的方式。

结论

作为相关工具的分析,这里先开个头。

责任编辑:张燕妮 来源: Phodal全栈工程师
相关推荐

2011-04-22 11:07:24

groovyJava

2010-08-09 14:43:25

Flex正则表达式

2010-08-09 14:14:13

Flex正则表达式

2010-08-13 15:31:11

Flex正则表达式

2010-08-09 14:30:39

Flex正则表达式

2014-01-05 17:41:09

PostgreSQL表达式

2023-07-31 07:43:07

Java语言表达式变量

2009-04-09 09:19:25

C#规则表达式.NET

2010-08-13 15:23:10

Flex正则表达式

2009-08-14 16:50:59

C#正则表达式语法

2013-09-27 09:43:44

Linux Shell脚本数学表达式

2024-03-25 13:46:12

C#Lambda编程

2013-01-05 02:19:50

JavaLambda表达式JVM

2009-09-11 12:32:33

LINQ表达式

2009-09-10 15:35:07

LINQ查询表达式

2009-09-15 15:18:00

Linq Lambda

2009-07-03 18:31:04

JSP表达式

2011-10-28 16:34:13

LINQ

2009-09-09 13:01:33

LINQ Lambda

2022-12-05 09:31:51

接口lambda表达式
点赞
收藏

51CTO技术栈公众号