Appearance
前后端表单统一验证
Hibernate Validator 简介
项目中,参数校验是必不可少的一环,这些校验出现在业务代码中,让我们的业务代码显得臃肿,而且,频繁的编写这类参数校验代码很无聊。鉴于此,觉得 Hibernate Validator 框架刚好解决了这些问题,可以很优雅的方式实现参数的校验,让业务代码 和 校验逻辑 分开,不再编写重复的校验逻辑。
说明
Hibernate Validator是 Bean Validation 的参考实现 . Hibernate Validator 提供了 JSR 303 规范中所有内置 constraint 的实现,除此之外还有一些附加的 constraint。
Bean Validation为 JavaBean 验证定义了相应的元数据模型和API。缺省的元数据是 Java Annotations,通过使用 XML 可以对原有的元数据信息进行覆盖和扩展。Bean Validation 是一个运行时的数据验证框架,在验证之后验证的错误信息会被马上返回。
Hibernate Validator 的作用
- 验证逻辑与业务逻辑之间进行了分离,降低了程序耦合度;
- 统一且规范的验证方式,无需你再次编写重复的验证代码;
- 你将更专注于你的业务,将这些繁琐的事情统统丢在一边。
Hibernate Validator 这么好,为啥我还要重复造轮子?
在Hibernate Validator的基础上,扩展更加适用的功能。
严谨的表单验证无疑需要同时支持前端表单验证
和后端接口验证
, 但作者工作多年来,发现基本所有的JavaWeb项目,均只能前端和后端各做各的表单验证,后期校验规则变更,又得前端和后端同时修改。
问题
那么, 有没有一种方式能让前端和后端的验证规则保持一致?一边修改了校验规则另一边实时生效?**
基于上面的场景,我在hibernate-validator
的基础上封装了acuity-validator-starter
模块,提供一个通用接口( /form/validator
),调用该接口即可以获取需要校验后端接口的校验规则,然后前端使用该接口返回的校验规则进行解析,解析成前端插件的校验规则,后期若规则改变,只需要后端修改规则即可。
实际上,我使用的表单验证组件还是Hibernate Validator,没有修改它的任何一个方法和注解,只是在它基础上有写了一个通过接口,通过该接口能优雅的返回后端某个接口的具体的校验规则,然后前端校验时,也使用同样的规则,即能达到统一校验的效果。
增强功能
- 本系统新增了一个自定义注解: NotEmptyPattern, 用于在参数不为空时,才判断正则表达式。它的约束实现类参考: NotEmptyPatternConstraintValidator
java
@RestController
@Validated
@RequestMapping("/baseOrg")
public class BaseOrgController {
// 新增机构的后端方法
@PostMapping("/save")
public R<Boolean> save(@RequestBody @Validated BaseEmployee baseEmployee) {
return R.success(baseEmployeeTestService.save(baseEmployee));
}
}
@RestController
@Validated
@RequestMapping("/baseOrg")
public class BaseOrgController {
// 新增机构的后端方法
@PostMapping("/save")
public R<Boolean> save(@RequestBody @Validated BaseEmployee baseEmployee) {
return R.success(baseEmployeeTestService.save(baseEmployee));
}
}
java
public class BaseOrgSaveVO implements Serializable {
private static final long serialVersionUID = 1L;
@NotEmpty(message = "请填写名称")
@Size(max = 255, message = "名称长度不能超过{max}")
private String name;
@Size(max = 2, message = "类型长度不能超过{max}")
@NotEmpty(message = "请填写类型")
private String type;
@Size(max = 255, message = "简称长度不能超过{max}")
private String shortName;
private Long parentId;
private Integer treeGrade;
@Size(max = 255, message = "树路径长度不能超过{max}")
private String treePath;
private Integer sortValue;
private Boolean state;
@Size(max = 255, message = "备注长度不能超过{max}")
private String remarks;
}
public class BaseOrgSaveVO implements Serializable {
private static final long serialVersionUID = 1L;
@NotEmpty(message = "请填写名称")
@Size(max = 255, message = "名称长度不能超过{max}")
private String name;
@Size(max = 2, message = "类型长度不能超过{max}")
@NotEmpty(message = "请填写类型")
private String type;
@Size(max = 255, message = "简称长度不能超过{max}")
private String shortName;
private Long parentId;
private Integer treeGrade;
@Size(max = 255, message = "树路径长度不能超过{max}")
private String treePath;
private Integer sortValue;
private Boolean state;
@Size(max = 255, message = "备注长度不能超过{max}")
private String remarks;
}
json
// 通过getValidateRules方法,调用后端 /form/valiator/xxx 接口,返回的新增机构的校验规则
{
"code": 0,
"data": [
{
"field": "shortName",
"fieldType": "String",
"constraints": [
{
"type": "Range",
"attrs": {
"min": 0,
"max": 255,
"message": "简称长度不能超过{max}"
}
}
]
},
{
"field": "remarks",
"fieldType": "String",
"constraints": [
{
"type": "Range",
"attrs": {
"min": 0,
"max": 255,
"message": "备注长度不能超过{max}"
}
}
]
},
{
"field": "type",
"fieldType": "String",
"constraints": [
{
"type": "Range",
"attrs": {
"min": 0,
"max": 2,
"message": "类型长度不能超过{max}"
}
},
{
"type": "NotNull",
"attrs": {
"message": "请填写类型"
}
}
]
},
{
"field": "name",
"fieldType": "String",
"constraints": [
{
"type": "Range",
"attrs": {
"min": 0,
"max": 255,
"message": "名称长度不能超过{max}"
}
},
{
"type": "NotNull",
"attrs": {
"message": "请填写名称"
}
}
]
},
{
"field": "treePath",
"fieldType": "String",
"constraints": [
{
"type": "Range",
"attrs": {
"min": 0,
"max": 255,
"message": "树路径长度不能超过{max}"
}
}
]
}
],
"msg": "ok",
"path": null,
"extra": null,
"timestamp": "1681045002724",
"errorMsg": "",
"isSuccess": true
}
// 通过getValidateRules方法,调用后端 /form/valiator/xxx 接口,返回的新增机构的校验规则
{
"code": 0,
"data": [
{
"field": "shortName",
"fieldType": "String",
"constraints": [
{
"type": "Range",
"attrs": {
"min": 0,
"max": 255,
"message": "简称长度不能超过{max}"
}
}
]
},
{
"field": "remarks",
"fieldType": "String",
"constraints": [
{
"type": "Range",
"attrs": {
"min": 0,
"max": 255,
"message": "备注长度不能超过{max}"
}
}
]
},
{
"field": "type",
"fieldType": "String",
"constraints": [
{
"type": "Range",
"attrs": {
"min": 0,
"max": 2,
"message": "类型长度不能超过{max}"
}
},
{
"type": "NotNull",
"attrs": {
"message": "请填写类型"
}
}
]
},
{
"field": "name",
"fieldType": "String",
"constraints": [
{
"type": "Range",
"attrs": {
"min": 0,
"max": 255,
"message": "名称长度不能超过{max}"
}
},
{
"type": "NotNull",
"attrs": {
"message": "请填写名称"
}
}
]
},
{
"field": "treePath",
"fieldType": "String",
"constraints": [
{
"type": "Range",
"attrs": {
"min": 0,
"max": 255,
"message": "树路径长度不能超过{max}"
}
}
]
}
],
"msg": "ok",
"path": null,
"extra": null,
"timestamp": "1681045002724",
"errorMsg": "",
"isSuccess": true
}
ts
import { getValidateRules } from '/@/api/acuity/common/formValidateService';
export const VALIDATE_API = {
[ActionEnum.ADD]: 'Save',
[ActionEnum.EDIT]: 'Update',
[ActionEnum.COPY]: 'Save',
};
async function setData(data: Recordable) {
await resetFields();
type.value = data?.type;
const { record = {}, parent = {} } = data;
record['parentName'] = parent?.name;
record['parentId'] = parent?.id;
await setFieldsValue({ ...record });
if (unref(type) !== ActionEnum.VIEW) {
// 新增、复制页面 = Api.Save 编辑页面 = Api.Update
let validateApi = Api[VALIDATE_API[unref(type)]];
// 通过getValidateRules方法,调用后端 /form/valiator/xxx 接口,获取 /xxx 接口的校验规则
getValidateRules(validateApi, customFormSchemaRules(type)).then(async (rules) => {
// 将后端返回的 rules ,更新到前端表单检验组件
rules && rules.length > 0 && (await updateSchema(rules));
});
}
}
import { getValidateRules } from '/@/api/acuity/common/formValidateService';
export const VALIDATE_API = {
[ActionEnum.ADD]: 'Save',
[ActionEnum.EDIT]: 'Update',
[ActionEnum.COPY]: 'Save',
};
async function setData(data: Recordable) {
await resetFields();
type.value = data?.type;
const { record = {}, parent = {} } = data;
record['parentName'] = parent?.name;
record['parentId'] = parent?.id;
await setFieldsValue({ ...record });
if (unref(type) !== ActionEnum.VIEW) {
// 新增、复制页面 = Api.Save 编辑页面 = Api.Update
let validateApi = Api[VALIDATE_API[unref(type)]];
// 通过getValidateRules方法,调用后端 /form/valiator/xxx 接口,获取 /xxx 接口的校验规则
getValidateRules(validateApi, customFormSchemaRules(type)).then(async (rules) => {
// 将后端返回的 rules ,更新到前端表单检验组件
rules && rules.length > 0 && (await updateSchema(rules));
});
}
}
前端逻辑
前端
前端请认真阅读formValidateService.ts源码,主要逻辑如下:
- 根据将入参validateApi替换为校验规则接口
- 从内存Map中查找规则缓存
- 请求校验规则接口
- 将后端返回的规则json,转换为前端能识别的json格式
- 将后端返回的规则和前端自定义规则合并