当前位置: 首页 > 科技观察

架构治理研究:规则、表达式和语言

时间:2023-03-21 21:54:15 科技观察

我们谈到了“分布式”场景下架构治理和规范治理相关的一系列问题。我们提到了一系列工具,例如Spectral,一个APIlinter和SQLFluff,一个数据库linter。为了提高ArchGuard中分布式规范的能力,对现有的几种工具进行了分析。对于我们来说,要构建一个类似的工具,需要考虑一些因素:插件。开发者可以在已有的守卫规则的基础上,开发一些新的架构守卫规则,比如API、数据库调用链接等。可测试性。如果使用fullDSL或者halfDSL,那么如何让后续的语言独立。如何在不被语言语法树束缚的情况下实现对多语言的支持。为此,我不得不拿起现有的代码进行一些分析。主要有四种工具,Kotlin语言的KtLint,OpenAPI的Spectral,多数据库的SQLFluff,MyBatis等表达式的公式语言Ognl。Kotlin代码治理:KtLintKtLint与一般的Lint工具略有不同,它自带自动格式化功能。KtLint的整体逻辑比较简单。基于单个文件生成AST,然后对AST进行规则匹配。Ktlint围绕Rule、Rulesets、RulesetsProvider构建规则的层次关系,并使用访问者(VisitorProvider)模式来解析AST。以下是KtLint的抽象规则:/***该方法将为AST中的每个节点执行(以DFS方式)。**@paramnodeAST节点*@paramautoCorrect指示规则是否应尝试自动更正*@param发出规则通知违规的方式(lint错误)*/abstractfunvisit(node:ASTNode,autoCorrect:Boolean,emit:(offset:Int,errorMessage:String,canBeAutoCorrected:Boolean)->Unit)评论里说了,三个参数代表了各自的用途。这里的ASTNode来源于Kotlin的AST树(kotlin-compiler-embeddable包)。模式也是获取配置,然后运行检测规则:valruleSets=ruleSetProviders.map{it.value.get()}valvisitorProvider=VisitorProvider(ruleSets,debug)其中对应访问:visitorProvider.visitor(params.ruleSets,preparedCode.rootNode,concurrent=false).invoke{node,rule,fqRuleId->}会过滤VistorProvider中对应的规则:}valenabledId=QualifiedRuleenabledRuleReferences.map{it.toQualifiedRuleId()}valenabledRules=ruleSets.flatMap{ruleSet->ruleSet.rules.filter{规则->toQualifiedRuleId(ruleSet.id,rule.id)inenabledQualifiedRuleIds}.filter{规则->isNotDisabled(rootNode,toQualifiedRuleId(ruleSet.id,rule.id))}.map{rule->"${ruleSet.id}:${rule.id}"torule}}.toMap()....然后,并行或串行运行规则中的访问。对于规则方法是通过ServicesLoader插件化:privatefungetRuleSetProvidersByUrl(url:URL?,debug:Boolean):Pair>{if(url!=null&&debug){logger.debug{“JAR规则集提供路径\"${url.path}\""}}valruleSetProviders=ServiceLoader.load(RuleSetProvider::class.java,URLClassLoader(listOfNotNull(url).toTypedArray())).toList()返回urltoruleSetProviders.toList()}如果粒度再大一点,使用Java9模块会不会更方便?基于API数据的Spectral与Ktlint的区别在于,Spectral是一个针对JSON/YAMLLint的工具,尤其针对OpenAPI文档(即swagger的yaml/json文件)。与Ktlint相比,Spectral最有趣的地方在于它提供了一个JSONPath(类似于XPath)的功能,可以对对象的特定部分采用特定的规则。下面是Spectral的示例:'oas3-valid-schema-example':{description:'Examplesmustbevalidagainsttheirdefinedschema.',message:'{{error}}',severity:0,formats:[oas3],推荐:true,type:'validation',given:["$.components.schemas..[?(@property!=='properties'&&@&&(@&&@.example!==void0||@.default!==void0)&&(@.enum||@.type||@.format||@.$ref||@.properties||@.items))]","$..content..[?(@property!=='properties'&&@&&(@&&@.example!==void0||@.default!==void0)&&(@.enum||@.type||@.format||@.$ref||@.properties||@.items))]","$..headers..[?(@property!=='properties'&&@&&(@&&@.example!==void0||@.default!==void0)&&(@.enum||@.type||@.format||@.$ref||@.properties||@.items))]","$..parameters..[?(@property!=='属性'&&@&&(@&&@.example!==void0||@.default!==void0)&&(@.enum||@.type||@.format||@.$ref||@.properties||@.items))]”,],然后:{function:oasExample,functionOptions:{schemaField:'$',oasVersion:3,type:'schema',},},}上面对象中给出的是针对对象中给出的相关属性作为条件执行的下面是then函数,具体可以参考官方文档:《Custom Rulesets》顺便说一句:Spectral使用nimma作为JSONPath表达式,Spectral的模型相对于Ktlint是因为Spectral与OpenAPI/AsyncAPI有相关的绑定和具体的正则表达式,所以它的数据模型稍微复杂一些,它的数据模型包括:描述,消息级别,给定-然后,上下文。如下:推荐。是否为推荐配置。启用。是否允许描述。规则描述消息.错误信息documentationUrl.文档地址.Severity.Severity,`error`,`warn`,`info`,or`hint`.formats.Formattingst标准,如OpenAPI2.0、OpenAPI3.0等已解决。已解决。给出。类似于CSS中的选择器,使用类似于XPath的JsonPath,然后是JSONPath。field,fieldfunction,function,modefunctionOptions也有简单的类型系统,以及对应的表达式判断。如下:CASES。flat,camel,pascal,kebab,cobol,snake,宏长度:最大值,最小值。数字布尔判断。类型系统。枚举总的来说,Spectral在实现上更加灵活有趣。与基于现有数据模型的Ktlint和Spectral相比,SQLFluff更具挑战性——它基于各种不同的数据库方言构建规则。SQLFluff直接基于源码分析,将不同的数据库方言转化为基本元素(分词)。然后根据分词的类型+规则进行处理。简单的说,就是一个比较抽象的分词上下文来构造对应的规则上下文。以下为片段。其核心是BaseSegment,定义了Lexing、Parsing、Linting三个基本要素,生成groupby_clause、orderby_clause、select_clause等分词。父堆栈。siblings_pre.siblings_post.原始堆栈。记忆。方言。作为语法运行时解析的基础。小路。小路。模板文件。模板文件。示例:{"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"}}随后的规则正在评估这些规则,例如:classRule_L021(BaseRule):def_eval(self,context:RuleContext)->Optional[LintResult]:"""AmbiguoususeofDISTINCTinselectstatementwithGROUPBY."""segment=context.functional.segmentif(segment.all(sp.is_type("select_statement"))#我们是否有一个groupby子句和segment.children(sp.is_type("groupby_clause"))):#select子句中是否有“DISTINCT”关键字.is_type("keyword")).select(sp.is_name("distinct")))ifdistinct:returnLintResult(anchor=distinct[0])returnNone这里所有的规则判断都是在某种意义上,语法树基于这种抽象构造了一个统一的抽象。本想进一步分析,却发现各种SQL方言中都有各种正则表达式,于是选择了暂时退却。表达式语言:OGNL最初,我是在实现ArchGuardScanner对MyBatis的SQL生成支持时看到OGNL表达式嵌套在XML中才发现OGNL的。在实现上,它与数据的结合比我之前想象的TreeSitter中的S-expression更加完美。同样,这里也可以用来做规则判断,可以用表达式来匹配数据。ObjectGraphNavigationLanguage(简称OGNL),是一种应用在Java中的开源表达式语言(ExpressionLanguage),用于获取和设置Java对象的属性,以及其他附加功能,如列表投影(projection)和选择和选择拉姆达表达式。您可以使用相同的表达式来获取和设置属性的值。Ognl类包含用于评估OGNL表达式的快捷方式。它可以分两个阶段完成,将表达式解析为内部形式,然后使用该内部形式设置或获取属性的值,或者它可以在一个阶段完成,直接使用字符串形式获取或设置属性表达。Ognl.getValue("name='jerry'",oc,oc.getRoot());Stringname2=(String)Ognl.getValue("#user1.name='jack',#user1.name",oc,oc.getRoot());本来想模仿OGNL写一个表达式语言,结果发现用的是Jacc,并没有Antlr的实现。所以,寻找更合理的方法。结论作为相关工具的分析,这里是开始。