20211023.Web后端参数检查的通用代码生成设计与实现

Web后端参数检查的通用代码生成设计与实现

前言

本文章来自这里,主要讨论&设计常见接口实现逻辑中的参数校验部分,主要是实现到Controller实现逻辑的时候决定修改总计划,不再追求全流程代码生成,原因不再赘述。

相关源代码库

元模型定义 https://github.com/956237586/hyldesigner

代码生成器  https://github.com/956237586/hyldesigner-codegen

realworld-mdd demo https://github.com/956237586/realworld-mdd

需求价值

简化&替代实际业务场景下常见的参数校验代码编写,提高开发效率,减少浪费在Copy And Paste上的宝贵生命

目标

实现参数校验部分的meta-model&对应的代码生成器

需要支持自动生成常见的校验策略,包括字符串trim后非空、字符串长度、字符串分中英文字符长度限制、数字范围、数据库存在性校验、多字段直接人工定义的约束校验等

校验策略支持2种,打断or完整校验

校验结果分为校验成功和校验失败

支持选择是否生成校验上下文记录额外IO的结果

其中完整策略下校验失败时支持生成详细的校验结果用于程序判断原因

支持校验结果自定义处理,打断策略下默认抛一个异常让开发者自己处理

支持的组合如下

1.打断策略

1.1.校验成功

1.1.1.不生成上下文

返回值void,结束方法运行

1.1.2.生成上下文

返回值中包含所有额外IO的原始结果变量

1.2.校验失败

打断策略下遇到任何一个校验错误都会直接停止后续校验,抛出异常和错误信息,这个错误信息给人看的,不支持程序来判断原因。

1.2.1.不生成上下文

异常中不包含上下文信息

1.2.2.生成上下文

异常中包含所有额外IO的原始结果变量

2.完整策略

2.1.校验成功

2.1.1.不生成上下文

返回值void,结束方法运行

2.1.2.生成上下文

返回值中包含所有额外IO的原始结果变量

2.2.校验失败

2.2.1.不生成上下文

异常中不包含上下文信息

2.2.2.生成上下文

异常中包含所有额外IO的原始结果变量

2.2.3.不生成详细的校验结果

异常中不包含详细错误信息

2.2.4.生成详细的校验结果

异常中需要包含校验失败的字段以及错误信息

 

需求分析

spring内置了@Valid注解来完成单一对象下单独属性的检查,且支持扩展自定义逻辑。看上去很好,很强大。

虽然但是,实际如果你用了,就会发现在真实业务流程下,这种校验有多鸡肋。

spring理解的校验:通过代码或注解定义静态规则,且可以让开发者拿到过不去规则的原因

简单的校验大概都是getXXX  然后做基础的校验,这部分基础校验就是spring理解的校验,如果失败打断流程,成功要把继续往下执行。

复杂校验往往依赖前面校验的结果,或依赖其他字段的状态,或依赖数据库来做校验。比如queryDbByXXX  成功则取部分内容继续流程,失败则打断。

根据工作经验来看,实际业务的校验往往不是单纯的单一规则校验就能完成,同时为了性能考虑校验过程中查到的数据库的结果还需要给下一步操作来使用。

这时候会面临方案选择:1.简单校验使用spring注解,复杂校验手动编写逻辑。2.全部手动编写逻辑

如果是写新需求:

方案1和2往往都是找个相同类型已有的类似代码,改字段、注解、属性名。这个过程将会占用大量时间在Copy And Paste,且毫无编程体验,可以说真的是板砖,并且还是从乱七八糟的地方搬

如果是维护改造类任务:

无论方案1还是2你会发现校验逻辑会比较分散,可能出现在类的属性注解、Controller的参数注解、各个Service的不知道第几层嵌套里。如果有人还用了反射来实现,那更是轻易找不全逻辑。

这时候如果想服用已有代码的部分业务流程,你就得小心的从原有屎山代码中拆分校验逻辑和业务逻辑。

虽然方案1可以通过ThreadLocal或缓存等手段来跨方法传递保存Spring校验过程中产出的上下文信息(比如从数据库查出的对象),但这样做无形中增加了代码后续的维护成本和理解难度。后续维护麻烦不说且容易改丢东西。所以实际开发中一般还是要手动写代码实现而不是仅简单的使用spring注解。

因此设想一种方案3,在代码之外尽可能简单的定义这些约束条件,通过代码生成器自动生成指定类实例(下文可能称之为对象,一个意思)的校验代码逻辑

概要设计

说了这么多其实可以类比数据关系约束对这完整性的描述(可能不够严谨,但意思大概对):

1.实体完整性

对象中的每个字段,是否都满足自身的约束,如字符非空/字符长度/数字值范围/字符串枚举等

这些校验实现起来相对简单,无需额外IO,校验所依赖的数据输入仅是字段的值本身,按每个规则直接实现即可。如果有多个规则,则同时执行多条校验即可

2.参照完整性

情况1 对象中的每个字段如果是另一个对象的主属性

情况2 另一个对象通过引用形式嵌入到了当前对象中

对于情况1  这种校验往往仅靠CPU无法完成,需要额外IO操作,比如查mysql数据库、redis。额外IO操作的结果后续可能有用也可能没用,如果校验通过了,可能就要继续用,否则大概率是没用的

对于情况2 和关系型数据库定义不一样的是,当对象的字段是引用且必填时候,需要额外校验引用非空,同时递归校验该引用对象的完整性

3.用户定义完整性

除了1,2意外的规则都可以纳入用户定义完整性

例如:字段间的特殊关联约束(字段1=A时字段2必须=BCD)

这部分校验和业务逻辑会有部分重合,需要根据情况来慎重决定是应该算作校验还是业务逻辑,必要时需要慎重选择

个人建议当这个规则后续大概率在业务中永远需要校验时才算业务逻辑,否则均按校验逻辑处理,避免污染核心业务逻辑。

例如用户A仅能修改uid=A的数据行:在不需要管理后的时候,算业务逻辑。当管理后台有超管可随意修改时,算校验逻辑。

详细设计

校验流程依赖元模型提供校验规则信息,因此需要进行元模型设计

校验流程分为主流程方法签名、主流程实现、子流程方法签名、子流程实现四部分。主流程由多个子流程实现,子流程由多个子代码模板组合而成。

1.元模型设计

已有定义:请求参数(Payload)包含多个属性(Attr),每个属性类型可能是字符串(Str)、整数(Int)、长整数(Long)、引用类型(ReferencedType)

1.1.类抽象结构设计

abstract Eclass AbstractCheckRule{// 约束的抽象父类  使用EReference作为Payload和Attr的子节点

EString desc

}

abstract Eclass DomainEntityCheckRule extends AbstractCheckRule 实体完整性

Eclass ReferencedCheckRule extends AbstractCheckRule  参照完整性 {

EReference Dto;

}

abstract Eclass UserDefinedCheckRule extends AbstractCheckRule 用户定义完整性

Eclass CustomCheckRule extends UserDefinedCheckRule 留个自定义的类方便扩展任意校验操作

逻辑上第一种DomainEntityCheckRule和第二种ReferencedCheckRule 字段校验的规则附加在属性(Attr)上,第三种校验附加在Payload上

1.Payload的Attr来自于DomainEntity的Attr映射:优先使用DomainEntity的AbstractCheckRule 子节点,同名的checkRule覆盖DomainEntity的(表达式可能不好写,先不实现)

2.Payload的Attr不是来自于DomainEntity的Attr映射:使用自己的AbstractCheckRule 子节点

3.Payload来自于DomainEntity映射:优先使用DomainEntity的AbstractCheckRule 子节点,同名的checkRule覆盖DomainEntity的(表达式可能不好写,先不实现)

4.Payload不是来自于DomainEntity映射:使用自己的AbstractCheckRule 子节点

对有映射关系的Payload或Attr来说,暂时考虑实现为合并DomainEntity的AbstractCheckRule,同时Payload增加CheckRuleMixIn属性支持合并其他Payload的AbstractCheckRule(参考Python的MixIn设计模式可以灵活增加通用的规则,之所以不用继承是因为避免模型管理的困难,不便于后期搜索功能的实现)

1.2.常用校验规则设计

目前的数据类型分为基本数据类型PrimitiveType和引用类型ReferencedType

基本类型包括字符串str,整型int,布尔类型bool, 长整型long

引用目前类型包括Dto,RequestPayload,ResponseResult

继续声明常用具体的检测逻辑

abstract Eclass PrimitiveTypeCheckRule extends DomainEntityCheckRule

abstract Eclass ReferencedTypeCheckRule extends ReferencedCheckRule

Eclass MustNotNullCheckRule extends PrimitiveTypeCheckRule, ReferencedTypeCheckRule

Eclass MustNullCheckRule extends PrimitiveTypeCheckRule, ReferencedTypeCheckRule

1.2.1.字符串str

abstract Eclass StringTypeCheckRule extends PrimitiveTypeCheckRule

Eclass NotEmptyCheckRule extends StringTypeCheckRule

Eclass LengthCheckRule extends StringTypeCheckRule {

EInteger min

EInteger max

}

Eclass RegexCheckRule extends StringTypeCheckRule {

EString regex

}

1.2.2.数字int long

abstract Eclass NumberTypeCheckRule extends PrimitiveTypeCheckRule

abstract Eclass IntegerTypeCheckRule extends NumberTypeCheckRule

abstract Eclass LongTypeCheckRule extends NumberTypeCheckRule

Eclass IntegerRangeCheckRule extends IntegerTypeCheckRule {

EInteger min

EInteger max

}

Eclass LongRangeCheckRule extends LongTypeCheckRule{

ELong min

ELong max

}

1.2.3.布尔 bool

abstract Eclass BoolTypeCheckRule extends PrimitiveTypeCheckRule

Eclass TrueValueCheckRule extends BoolTypeCheckRule

Eclass FalseValueCheckRule extends BoolTypeCheckRule

1.2.4.引用类型

abstract Eclass DtoTypeCheckRule extends ReferencedTypeCheckRule

Eclass HasValueRefCheckRule extends DtoTypeCheckRule, MustNotNullCheckRule

 

2.模板设计

子模板需要提供方法签名模板、各数据类型校验流程模板

2.0.代码转写

进行元模型M2T设计,仅凭设计稿基本难以一次性写对,因此在这里开始代码人工转写。基本都是人工先转写一版,再照着转写的结果进行M2T代码的开发。

2.1.方法签名设计

根据目标描述,对校验行为分类,确定Java返回值和参数签名

1.1.1.打断策略、校验成功、不生成上下文|返回值void,结束方法运行
1.2.1.打断策略、校验失败、不生成上下文|返回异常,异常中不包含上下文信息
1.1.2.打断策略、校验成功、生成上下文|返回上下文,上下文中包含所有额外IO的原始结果变量
1.2.2.打断策略、校验失败、生成上下文|返回异常,异常中包含所有额外IO的原始结果变量
2.1.1.完整策略、校验成功、不生成上下文|返回值void,结束方法运行
2.1.2.完整策略、校验成功、生成上下文|返回上下文,上下文中包含所有额外IO的原始结果变量
2.2.1.完整策略、校验失败、不生成上下文|返回异常,异常中不包含上下文信息
2.2.2.完整策略、校验失败、生成上下文|返回异常,异常中包含所有额外IO的原始结果变量
2.2.3.完整策略、校验失败、不生成详细的校验结果|返回异常,异常中不包含详细错误信息
2.2.4.完整策略、校验失败、生成详细的校验结果|返回异常,异常中需要包含校验失败的字段以及错误信息

2.2.方法签名生成逻辑设计

[xxxx/]代表Acceleo中的元模板占位

Payload元模型节点定义

Payload.genClassName 生成类名

Payload.name Payload的名字

对Payload子类的实例对象aPayload做M2T转换规则设计如下:

2.3.数据类型校验设计

对任意属性 checkAttr(anAttr:Attr)

做对anAttr的type做类似于instance of的类型判断如下:

对字符串 checkStr(v->predicate(v))

对数值 checkNumber(v->predicate(v))

对布尔checkBool(v->predicate)

对引用类型来说 checkRef(ref->predicate(ref)) && ref.attrs.forEach(attr->checkAttr(attr))

MustNotNullCheckRule

2.1.1.字符串 str

NotEmptyCheckRule

Eclass LengthCheckRule

RegexCheckRule

2.1.2.数字

代码结构设计 https://github.com/956237586/realworld-mdd/blob/master/src/main/java/cn/hylstudio/mdse/demo/realworld/util/ValueUtils.java#L69

模板设计参考 https://github.com/956237586/hyldesigner-codegen/blob/main/src/cn/hylstudio/mdse/demo/gen/abstractCheckRule.mtl#L41

2.4.主校验流程设计

因限定了参数校验的范围,因此主流程可以固定下来,不会陷入无限接近代码的怪圈。

按前面所说校验分为三类依次执行,其中2和3可能产生CheckContext。

对指定对象的每个属性,依次获取子节点下+引用源子节点下的所有规则,并根据规则执行生成逻辑

20220130目前只取了当前子节点下的,未获取源子节点下的,CheckContext因和数据库有关,因此推迟

0 Comments
Leave a Reply