Validate核查框架——Mikilin

Mikilin 简介

Mikilin框架是自主设计的对象的属性核查框架,功能直接对应JSR-303协议,但是着眼点和用法不一样,暂时没有采用该协议(后续版本考虑进去),JSR-303协议对应的业内实现为hibernate.validate,但是该框架比hibernate.validate中的功能更多,使用和扩展也更简单。

JSR-303协议中的校验基本层面为属性本身的校验,而属性关联的校验就没有关注。而我们这里的框架,着眼点为数据领域,每个待核查的数据都归为一类进行匹配,其中数据之间的关联在概念上也作为一个类别。所有这些类别最后具象化到代码中为一个注解中的多个属性。
目前已经发布到maven中央仓库,直接使用即可

<dependency>
    <groupId>com.github.simonalong</groupId>
    <artifactId>mikilin</artifactId>
    <!--请替换为最新版本-->
    <version>${latest.release.version}</version>
</dependency>

框架文档:https://www.yuque.com/simonalong/mikilin/mduu3z
源码:https://github.com/SimonAlong/Mikilin

一、快速入门

该框架使用极其简单,添加注解和使用静态类核查即可
如下:给需要拦截的属性添加注解即可

@Data
@Accessors(chain = true)
public class WhiteAEntity {
    
    // 修饰属性name,只允许对应的值为a,b,c和null
    @Matcher(value = {"a","b","c","null"}, errMsg = "输入的值不符合需求")
    private String name;
    private String address;
}

在拦截的位置添加核查,这里是做一层核查,在业务代码中建议封装到aop中对业务使用方不可见即可实现拦截

import lombok.SneakyThrows;

@Test
@SneakyThrows
public void test1(){
    WhiteAEntity whiteAEntity = new WhiteAEntity();
    whiteAEntity.setName("d");
    // 可以使用带有返回值的核查
    if (!MkValidators.check(whiteAEntity)) {
        // 输出:数据校验失败-->属性 name 的值 d 不在只可用列表 [null, a, b, c] 中-->类型 WhiteAEntity 核查失败
        System.out.println(MkValidators.getErrMsgChain());
        // 输出:输入的值不符合需求
        System.out.println(MkValidators.getErrMsg());
    }
    // 或者 可以采用抛异常的核查,该api为 MkValidators.check 的带有异常的检测方式
    MkValidators.validate(whiteAEntity);
}

以上基本上就是该框架的所有功能。非常简单,不过功能却很多,多在注解@Matcher中的属性,后面一一介绍。

二、常见场景用法

以下场景均来自实际业务场景

1.字段不可为空

用户id不可为空

/**
 * 被邀请的用户id
 */
@Matcher(notNull = "true")
private Long userId;
2.字符串属性不可为空

用户名不可为空

/**
 * 用户名
 */
@Matcher(notBlank = "true")
private String name;
3.类型只可为0和1

对应属性的类型,只可为两个值,或者多个值

/**
 * 是否必需,0:不必填,1:必填
 */
@Matcher(value = {"0", "1"})
private Integer needKey;
4.类型为多个固定的值

对应属性的状态,只可为0、1、2、3、4、5、6、7中的一个

/**
 * 状态:0未构建,1编译中,2打包中,3部署中,4测试中,5测试完成,6发布中,7发布完成
 */
//@Matcher(value = {"0", "1", "2", "3", "4", "5", "6", "7"}) 也可以使用下面
@Matcher(range = "[0, 8]") 
private Integer deployStatus = 0;
5.对应的值为邮箱

字段为邮箱判断。除了邮箱之外,还有手机号、固定电话、身份证号和IP地址这么四个固定的类型判断。

/**
 * 邮箱
 */
@Matcher(notNull = "false", model = FieldModel.MAIL, errMsg = "邮箱:#current 不符合邮件要求")
private String email;
6.对应集合的长度

前端上传的图片最多为三个

/**
 * 预览图最多为三个
 */
@Matcher(range = "(, 3]")
private List<String> prePicUrlList;
7.字符长度最长为128

对前端传递过来的字符长度限制为128,因为数据库字段存储为最长128

/**
 * 地址长度
 */
@Matcher(range = "[0, 128]")
private String nameStr;
8.数据为空或者不空的话,长度不能超过200

其中@Matcher内部多个属性之间的匹配是或的关系

/**
 * 项目描述,可以为空,但是最长不可超过200
 */
@Matcher(notBlank = "false", range = "[0, 200]", errMsg = "描述的值不可过长,最长为200")
private String proDesc;
9.前端传递过来的id必须在db中存在,而且数据不可为空

@Matcher支持多个叠加形式,表示多个条件的与操作

@Matcher(notNull = "true")
@Matcher(customize = "com.xxx.yyy.ExistMatch#proIdExist", errMsg = "proId:#current在db中不存在")
private Long projectId;

其中匹配的写法

@Service
public class ExistMatch {

    @Autowired
    private ProjectService projectService;

    /**
     * appId存在
     */
    public boolean proIdExist(Long proId) {
        return projectService.exist(proId);
    }
}
10.项目名不可为空,而且数据库中不能存在,如果存在则拒绝

其中属性accept表示如果前面的条件匹配则拒绝,默认为true

/**
 * 项目名称
 */
@Matcher(notBlank = "true")
@Matcher(customize = "com.isyscore.iop.panda.service.ProjectService#projectNameExist", accept = false, errMsg = "已经存在名字为 #current 的项目")
private String proName;
11.在某个配置项下对应的字段值

业务场景:在字段为1的时候,另外一个对应的字段不可为空

/**
 * 处理类型:0,新增;1,编辑;2,搜索;3,表展示;4,表扩展
 */
@Matcher(range = "[0, 4]", errMsg = "不识别类型 #current")
private Integer handleType;

/**
 * 在编辑模式下,禁用的表字段
 */
@Matcher(condition = "(#current == null && #root.handleType != 1) || (#current != null && !#current.isEmpty() && #root.handleType == 1)", errMsg = "cantEditColumnList 需要在handleType为1的时候才有值")
private List<String> cantEditColumnList;
12.时间必须是过去的时间
/**
 * 应用发布时间
 */
@Matcher(range = "past")
@ApiModelProperty(value = "应用发布时间")
private Date createTime;
13.分页数据必须满足>0
@Matcher(range = "[0, )", errMsg = "分页数据不满足")
private Integer pageNo;
@Matcher(range = "[0, )", errMsg = "pageSize数据不满足")
private Integer pageSize;
14.【复杂场景】应用id在不同的场景下处理方式不同
  1. 在启动构建时候,状态必须在“开始”阶段
  2. 在测试完成动作,状态必须在‘测试中’阶段
  3. 在启动发布动作,状态必须在“测试完成”阶段
  4. 在停止动作,状态必须在“部署”状态之前
  5. 退出动作,要保证应用在“部署”状态之前

此外最基本的就是应用id不可为空,而且在db中必须存在。上面的几个动作都是不同的接口,但是所有的参数都相同,那么用group是最好的方式。其中group里面可以添加多个分组,其中group相同的,则表示两个@Mather之间是与的关系

@Data
public class AppIdReq {

    @Matchers({
        @Matcher(notNull = "true"),
        @Matcher(group = {MkConstant.DEFAULT_GROUP, "startBuild", "finishTest", "startDeploy", "stop", "quite"}, customize = "com.xxx.yyy.ExistMatch#appIdExist", errMsg = "应用id: #current 不存在"),
        // 启动构建 动作的状态核查
        @Matcher(group = "startBuild", customize = "com.xxx.yyy.DeployStatusMatch#startBuild", errMsg = "应用id: #current 不在阶段'未编译',请先退出"),
        // 测试完成 动作的状态核查
        @Matcher(group = "finishTest", customize = "com.xxx.yyy.DeployStatusMatch#finishTest", errMsg = "应用id: #current 不在阶段'测试中'"),
        // 启动发布 动作的状态核查
        @Matcher(group = "startDeploy", customize = "com.xxx.yyy.DeployStatusMatch#startDeploy", errMsg = "应用id: #current 不在阶段'测试完成'"),
        // 停止 动作的状态核查
        @Matcher(group = "stop", customize = "com.xxx.yyy.DeployStatusMatch#stopDeploy", errMsg = "停止的动作需要保证应用 #current 在部署状态之前"),
        // 退出 动作的状态核查
        @Matcher(group = "quite", customize = "com.xxx.yyy.DeployStatusMatch#stopDeploy", errMsg = "退出的动作需要保证应用 #current 在部署状态之前")
    })
    @ApiModelProperty(value = "应用id", example = "42342354")
    private Long appId;
}

对应的接口使用

@AutoCheck
@RequestMapping("${api-prefix}/deploy")
@RestController
public class DeployController {

    ...
        
    /**
     * 启动构建
     */
    @AutoCheck("startBuild")
    @PutMapping("startBuild")
    public Integer startBuild(@RequestBody AppIdReq appIdReq) {
        Long appId = appIdReq.getAppId();
        devopsService.startBuild(appId);
        return 1;
    }

    /**
     * 完成测试
     */
    @AutoCheck("finishTest")
    @PutMapping("finishTest")
    public Integer finishTest(@RequestBody AppIdReq appIdReq) {
        Long appId = appIdReq.getAppId();
        appService.chgDeployStatus(appId, DeployStatusEnum.TEST_FINISH);
        return 1;
    }

    /**
     * 启动发布
     */
    @AutoCheck("startDeploy")
    @PutMapping("startDeploy")
    public Integer startDeploy(@RequestBody AppIdReq appIdReq) {
        Long appId = appIdReq.getAppId();
        appService.startDeploy(appId, UserInfoContext.getUserContext().getUserId());
        return 1;
    }

    /**
     * 停止
     * <p>只有在发布之前,停止按钮可见
     */
    @AutoCheck("stop")
    @PutMapping("stop")
    public Integer stop(@RequestBody AppIdReq appIdReq) {
        appService.stopApp(appIdReq.getAppId());
        return 1;
    }

    /**
     * 退出
     * <p>如果已经进行过“发布”,则不可退出,而且该退出按钮只有在停止后才能退出
     */
    @AutoCheck("quite")
    @PutMapping("quite")
    public Integer quite(@RequestBody AppIdReq appIdReq) {
        appService.quite(appIdReq.getAppId());
        return 1;
    }
}

三、详解

注解

这里只有有三个注解

/**
* 匹配器:修饰属性
*/
@Matcher
/**
* 多个匹配器,不同的分组:修饰属性,为@Matcher的复数类型
*/
@Matchers
/**
* 复杂对象解析器:修饰属性,只有添加该注解,则复杂的属性,才会进行解析
*/
@Check

匹配器

匹配器就是指注解@Matcher,而注解中的各种各样的如下的属性,表示匹配项,而丰富的匹配项是该框架最强大和功能最丰富的的地方,每个属性(匹配项)定位是能够匹配该领域的所有类型

@Repeatable(Matchers.class)
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Matcher {

    /**
     * 针对不同场景下所需的匹配模式的不同,默认"_default_",详见{@link com.simonalong.mikilin.MkConstant#DEFAULT_GROUP}
     * <p>
     * 该参数使用一般结合{@link Matchers}这个注解使用
     * 
     * @return 分组
     */
    String[] group() default {MkConstant.DEFAULT_GROUP};

    /**
     * 匹配属性为对应的类型,比如Integer.class,Long.class等等
     */
    Class<?>[] type() default {};

    /**
     * 如果要匹配值为null,那么添加一个排除的值为"null",因为不允许直接设置为null,修饰的对象可以为Number类型也可以为String类型,也可以为Boolean类型
     * @return 只允许的值的列表
     */
    String[] value() default {};

    /**
     * 可用的值对应的类型
     * @return 对应的枚举类型
     */
    FieldModel model() default FieldModel.DEFAULT;

    /**
     * 枚举类型的判断
     *
     * 注意:该类型只用于修饰属性的值为String类型或者Integer类型的属性,String为枚举的Names,Integer是枚举的下标
     *
     * @return 该属性为枚举对应的类,否则不生效
     */
    Class<? extends Enum>[] enumType() default {};

    /**
     * 数据范围的判断
     * <p> 该字段修饰的类型可以为数值类型,也可以为时间类型,也可以为集合类型(集合类型用来测试集合的size个数的范围)
     *
     * @return
     * 如果是数值类型,则比较的是数值的范围,使用比如:[a,b],[a,b),(a,b],(a,b),(null,b],(null,b),[a, null), (a, null)
     * 如果是集合类型,则比较的是集合的size大小,使用和上面一样,比如:[a,b]等等
     * 如果是时间类型,可以使用这种,比如["2019-08-03 12:00:32.222", "2019-08-03 15:00:32.222"),也可以用单独的一个函数关键字
     * past: 表示过去
     * now: 表示现在
     * future: 表示未来
     * 同时也支持范围中包含函数(其中范围内部暂时不支持past和future,因为这两个函数没有具体的时间),比如:
     * past 跟(nul, now)表示的相同
     * future 跟(now, null)表示的相同
     * 支持具体的范围,比如:("2019-08-03 12:00:32", now),其中对应的时间类型,目前支持这么几种格式
     * yyyy
     * yyyy-MM
     * yyyy-MM-dd
     * yyyy-MM-dd HH
     * yyyy-MM-dd HH:mm
     * yyyy-MM-dd HH:mm:ss
     * yyyy-MM-dd HH:mm:ss.SSS
     */
    String range() default "";

    /**
     * 数据条件的判断
     *
     * 根据Java的运算符构造出来对应的条件表达式来进行判断,而其中的数据不仅可以和相关的数据做条件判断,还可和当前修饰的类的其他数据进行判断,
     * 其中当前类用#root表示,比如举例如下,对象中的一个属性小于另外一个属性,比如:{@code #current + #root.ratioB + #root.ratioC == 100}
     * 其中#current表示当前的属性的值,#root表示当前的属性所在的对象,ratioB为该对象的另外一个属性,如上只有在属性ratioA是大于ratioB的时候核查才会拦截
     *
     * @return 用于数据字段之间的条件表达式(即条件结果为true还是false),当前条件支持Java的所有运算符,以及java的所有运算结果为boolean的表达式
     * 算术运算符:{@code  "+"、"-"、"*"、"/"、"%"、"++"、"--"}
     * 关系运算符:{@code "=="、"!="、">"、"<"、">="、"<="}
     * 位运算符:{@code "&"、"|"、"^"、"~"、"<<"、">>"、">>>"}
     * 逻辑运算符:{@code "&&"、"||"、"!"}
     * 赋值运算符:{@code "="、"+="、"-="、"*="、"/="、"(%)="、"<<="、">>="、"&="、"^="、"|="}
     * 其他运算符:{@code 条件运算符(?:)、instanceof运算符}
     * {@code java.lang.math}中的所有函数,比如:{@code min,max,asb,cell}等等
     */
    String condition() default "";

    /**
     * 可用的值对应的正则表达式
     * @return 对应的正则表达式
     */
    String regex() default "";

    /**
     * 系统自己编码判断
     *
     * @return 调用的核查的类和函数对应的表达式,比如:"com.xxx.AEntity#isValid",其中#后面是方法,方法返回boolean或者包装类,其中参数根据个数支持的类型也是不同,参考测试类{@link com.simonalong.mikilin.judge.JudgeCheck}
     */
    String judge() default "";
    
    /**
     * 核查失败后的返回语句
     *
     * @return 核查失败后返回的语句
     */
    String errMsg() default "";

    /**
     * 过滤器模式
     * <p>
     *     其他的属性都是匹配,而该属性表示匹配之后对应的数据的处理,是接受放进来,还是只拒绝这样的数据
     * @return true:accept(放进来),false:deny(拒绝)
     */
    boolean accept() default true;
    
    /**
     * 是否启用核查
     * @return true:禁用核查,false:启用核查
     */
    boolean disable() default false;
}

group:分组匹配

上面看到,每个属性只有一种核查规则,但是如果我们要在不同的场景中使用不同的规则,那么这个时候应该怎么办呢,分组就来了,新增一个注解Matchers

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Matchers {

    Matcher[] value();
}

使用时候,通过属性中的group进行定义不同的规则,核查的时候,采用函数MkValidators.check(String group, Object obj)进行核查,如果采用MkValidators.check(Object obj)则采用默认的组,即下面的没有设置组的匹配规则

@Data
@Accessors(chain = true)
public class GroupEntity {

    @Matchers({
        @Matcher(range = "[50, 100]", accept = false),
        @Matcher(group = "test1", range = "[12, 23]", accept = false),
        @Matcher(group = "test2", range = "[1, 10]", accept = false)
    })
    private Integer age;

    @Matchers({
        @Matcher(value = {"beijing", "shanghai", "guangzhou"}),
        @Matcher(group = "test1", value = {"beijing", "shanghai"}),
        @Matcher(group = "test2", value = {"shanghai", "hangzhou"})
    })
    private String name;
}

用例

def "测试指定分组"() {
    given:
    GroupEntity entity = new GroupEntity().setAge(age).setName(name)

    expect:
    def act = MkValidators.check("test1", entity);
    Assert.assertEquals(result, act)
    if (!act) {
        println MkValidators.errMsg
    }

    where:
    age | name        | result
    10  | "shanghai"  | true
    12  | "beijing"   | false
    23  | "beijing"   | false
    50  | "beijing"   | true
    100 | "guangzhou" | false
}

value:指定某些值匹配

@Data
@Accessors(chain = true)
public class WhiteAEntity {
    @Matcher({"a","b","c","null"})
    private String name;
    private String address;
}

注意:测试代码这里全部采用Spock测试框架进行工具方面的测试,主要是为了方便

def "只有指定的值才能通过"() {
    given:
    WhiteAEntity entity = new WhiteAEntity()
    entity.setName(name as String)

    expect:
    boolean actResult = MkValidators.check(entity)
    if (!actResult) {
        println MkValidators.getErrMsgChain()
    }
    Assert.assertEquals(result, actResult)

    where:
    name | result
    "a"  | true
    "b"  | true
    "c"  | true
    null | true
    "d"  | false
}

type:属性类型匹配

@Data
@Accessors(chain = true)
public class TypeEntity {

    /**
     * 没有必要设置type
     */
    @Matcher(type = Integer.class)
    private Integer data;

    @Matcher(type = CharSequence.class)
    private String name;

    @Matcher(type = {Integer.class, Float.class})
    private Object obj;

    @Matcher(type = Number.class)
    private Object num;
}

测试代码

def "测试不明写类继承关系1"() {
    given:
    TypeEntity entity = new TypeEntity().setObj(obj)

    expect:
    boolean actResult = MkValidators.check(entity, "obj")
    if (!result) {
        println MkValidators.getErrMsgChain()
    }
    Assert.assertEquals(result, actResult)

    where:
    obj    | result
    'c'    | false
    "abad" | false
    1232   | true
    1232l  | false
    1232f  | true
    12.0f  | true
    -12    | true
}

注意:

  1. 如果设置的类型不是属性的类型或者父类则会报错
  2. 如果为具体的类型,则再设置与其相同的类型,则没有必要,就像上面的data属性

model:内置类型匹配

目前内置了常见的几种类型:身份证号、手机号、固定电话、邮箱、IP地址

ID_CARD :身份证号
PHONE_NUM :手机号
FIXED_PHONE :固定电话
MAIL :邮箱
IP_ADDRESS: IP地址

@Data
@Accessors(chain = true)
public class IpEntity {

    @Matcher(model = FieldModel.IP_ADDRESS)
    private String ipValid;
    @Matcher(model = FieldModel.IP_ADDRESS, accept =false)
    private String ipInvalid;
}
def "IP测试"() {
    given:
    IpEntity entity = new IpEntity().setIpValid(valid).setIpInvalid(invalid)

    expect:
    boolean actResult = MkValidators.check(entity)
    if (!result) {
        println MkValidators.getErrMsgChain()
    }
    Assert.assertEquals(result, actResult)

    where:
    valid             | invalid           | result
    "192.231asdf"     | "192.123.231.222" | false
    "192.231asdf"     | "192.231asdf"     | false
    "192.123.231.222" | "192.231asdf"     | true
    "192.123.231.222" | "192.123.231.222" | false
}

range:范围匹配

目前该属性不只是数值类型(Integer, Long, Float, Short, Double等一切数值类型),也支持时间类型,也支持集合类型(集合比较的是集合的大小),范围是用的是数学的开闭写法

数值范围
@Data
@Accessors(chain = true)
public class RangeEntity4 {

    /**
     * 属性为大于100
     */
    @Matcher(range = "(100, null)")
    private Integer num1;

    /**
     * 属性为大于等于100
     */
    @Matcher(range = "[100, null)")
    private Integer num2;

    /**
     * 属性为大于20且小于50
     */
    @Matcher(range = "(20, 50)")
    private Integer num3;

    /**
     * 属性为小于等于50
     */
    @Matcher(range = "(null, 50]")
    private Integer num4;
    
    /**
     * 属性为大于等于20且小于等于50
     */
    @Matcher(range = "[20, 50]")
    private Integer num5;

    /**
     * 属性为大于等于100,同属性num2一样
     */
    @Matcher(range = "[100, )")
    private Integer num6;
    
    /**
     * 属性为大于等于100,同属性num2一样
     */
    @Matcher(range = "[100,)")
    private Integer num7;
    
    /**
     * 属性为小于等于5,同属性num4一样
     */
    @Matcher(range = "(, 50]")
    private Integer num8;
}
时间范围

修饰的类型可以为Date类型,也可以为Long类型

@Data
@Accessors(chain = true)
public class RangeTimeEntity {

    /**
     * 属性为:2019-07-13 12:00:23.321 到 2019-07-23 12:00:23.321的时间
     */
    @Matcher(range = "['2019-07-13 12:00:23.321', '2019-07-23 12:00:23.321']")
    private Date date1;

    /**
     * 属性为:2019-07-13 12:00:23.000 到 2019-07-23 12:00:00.000 的时间
     */
    @Matcher(range = "['2019-07-13 12:00:23', '2019-07-23 12:00']")
    private Date date2;
    
    /**
     * 属性为:2019-07-13 00:00:00.000 到 2019-07-01 00:00:00.000 的时间
     */
    @Matcher(range = "['2019-07-13', '2019-07']")
    private Long dateLong3;
        
    /**
     * 属性为:现在时间 到 2019-07-23 12:00:23.321 的时间
     */
    @Matcher(range = "(now, '2019-07-23 12:00:23.321']")
    private Date date4;
    
    /**
     * 属性为:2019-07-13 00:00:00.000 到现在的时间
     */
    @Matcher(range = "['2019-07-13', now)")
    private Date date5;
    
    /**
     * 属性为:过去的时间,同下面的past
     */
    @Matcher(range = "(null, now)")
    private Date date6;
    
    /**
     * 属性为:过去的时间,同下面的past
     */
    @Matcher(range = "('null', 'now')")
    private Date date7;
    
    /**
     * 属性为:过去的时间,同上
     */
    @Matcher(range = "past")
    private Date date8;
    
    /**
     * 属性为:未来的时间,同下面的future
     */
    @Matcher(range = "(now, null)")
    private Date date9;
    
    /**
     * 属性为:未来的时间,同下面的future
     */
    @Matcher(range = "future")
    private Date date10;
}
集合大小范围

集合这里只核查集合的数据大小

@Data
@Accessors(chain = true)
public class CollectionSizeEntityA {

    private String name;

    /**
    * 对应集合的个数不为空,且个数小于等于2 
    */
    @Matcher(range = "(0, 2]")
    private List<CollectionSizeEntityB> bList;
}

condition:表达式匹配

这里的表达式只要是任何返回Boolean的表达式即可,框架提供两个占位符,#current和#root,其中#current表示当前属性的值,#root表示的是当前属性所在的对象的值,可以通过#root.xxx访问其他的属性。该表达式支持java中的任何符号操作,此外还支持java.lang.math中的所有静态函数,比如:min、max和abs等等

@Data
@Accessors(chain = true)
public class ConditionEntity {

    /**
    * 当前属性和属性num3的值大于100 
    */
    @Matcher(condition = "#current + #root.num2 > 100")
    private Integer num1;

    /**
    * 当前属性的值小于 20 
    */
    @Matcher(condition = "#current < 20")
    private Integer num2;

    /**
    * 当前属性的值大于31并自增 
    */
    @Matcher(condition = "(++#current) >31")
    private Integer num3;
    
    /**
    * 当前属性的值大于31并自增 
    */
    @Matcher(condition = "(++#current) >31")
    private Integer num4;
    
    /**
    * 其中某个属性为true 
    */
    @Matcher(condition = "#root.judge")
    private Integer age;

    private Boolean judge;
    
    /**
    * 当前值和另外值的最小值大于第三个值 
    */
    @Matcher(condition = "min(#current, #root.num6) > #root.num7")
    private Integer num5;
    private Integer num6;
    private Integer num7;
}

regex:正则表达式匹配

@Data
@Accessors(chain = true)
public class RegexEntity {

    @Matcher(regex = "^\\d+$")
    private String regexValid;

    @Matcher(regex = "^\\d+$", accept = false)
    private String regexInValid;
}

customize:自定义扩展匹配

上面都是系统内置的一些匹配,如果用户想自定义匹配,可以自行扩展,需要通过该函数指定一个全限定名的类和函数指定即可,目前支持的参数类型有如下几种,比如

自定义函数路径匹配
@Data
@Accessors(chain = true)
public class JudgeEntity {

    /**
    * 外部定义的匹配器,只传入属性的参数
    */
    @Matcher(customize = "com.simonalong.mikilin.judge.JudgeCheck#ageValid")
    private Integer age;

    /**
    *  外部定义的匹配器,传入属性所在对象本身,也可传入属性的参数类型
    */
    @Matcher(customize = "com.simonalong.mikilin.judge.JudgeCheck#ratioJudge")
    private Float mRatio;

    private Float nRatio;

    /**
    * 这里自定义的第一个参数是属性本身,第二个参数是框架的上下文(用户填充匹配成功或者失败的信息) 
    */
    @Matcher(customize = "com.simonalong.mikilin.judge.JudgeCheck#twoParam")
    private String twoPa;

    /**
    * 这里自定义的第一个参数是属性所在对象,第二个是属性本身,第三个参数是框架的上下文(用户填充匹配成功或者失败的信息) 
    */
    @Matcher(customize = "com.simonalong.mikilin.judge.JudgeCheck#threeParam")
    private String threePa;
}

对应的匹配逻辑,其中匹配函数的入参是上面注解修饰的属性的类型(或者子类)

public class CustomizeCheck {

    /**
     * 年龄是否合法
     */
    public boolean ageValid(Integer age) {
        if(null == age){
            return false;
        }
        if (age >= 0 && age < 200) {
            return true;
        }

        return false;
    }
    
    /**
     * 能够传递核查的对象,对于对象中的一些属性可以进行系统内部的配置
     *
     * mRatio + nRatio < 1.0
     */
    private boolean ratioJudge(JudgeEntity judgeEntity, Float nRatio) {
        if(null == nRatio || null == judgeEntity){
            return false;
        }
        return nRatio + judgeEntity.getMRatio() < 10.0f;
    }
    
    /**
     * 两个函数
     */
    private boolean twoParam(String funName, MkContext context) {
        if (funName.equals("hello")){
            context.append("匹配上字段'hello'");
           return true;
        }
        context.append("没有匹配上字段'hello'");
        return false;
    }
    
    /**
     * 三个函数
     */
    private boolean threeParam(JudgeEntity judgeEntity, String temK, MkContext context) {
        if (temK.equals("hello") || temK.equals("word")){
            context.append("匹配上字段'hello'和'word'");
            return true;
        }
        context.append("没有匹配上字段'hello'和'word'");
        return false;
    }
}
spring的Bean自定义匹配器

上面看到了,我们指定一个全限定路径即可设置过滤器,其实是反射了一个代理类,在真实的业务场景中,我们的bean是用spring进行管理的,因此这里增加了一个通过spring管理的匹配器,如下 使用时候需要在指定为扫描一下如下路径即可

@ComponentScan(value = “com.simonalong.mikilin.util”)

下面的函数对应的参数跟上面非spring时候一样,可以有三种格式

@Service
public class JudgeCls {

    // 该引用只是举例
    @Autowire
    private UserSevice userSevice;

    /**
     * 年龄是否合法
     */
    public boolean ageValid(Integer age) {
        if(null == age){
            return false;
        }
        if (age >= 0 && age < 200) {
            return true;
        }

        return false;
    }
}

enumType:枚举值匹配

@Data
@AllArgsConstructor
@Accessors(chain = true)
public class JudgeEntity {

    @Matcher(enumType = AEnum.class)
    private String name;

    @Matcher(enumType = {AEnum.class, BEnum.class})
    private String tag;

    @Matcher(enumType = {CEnum.class}, accept = false)
    private String invalidTag;
}
@Getter
public enum AEnum {
    A1("a1"),
    A2("a2"),
    A3("a3");

    private String name;

    AEnum(String name) {
        this.name = name;
    }
}
def "枚举类型测试"() {
    given:
    JudgeEntity judgeEntity = new JudgeEntity(name, tag, invalidTag)

    expect:
    def act = MkValidators.check(judgeEntity)
    Assert.assertEquals(result, act)
    if (!act) {
        println MkValidators.errMsg
    }

    where:
    name | tag  | invalidTag | result
    "A1" | "A1" | "c"        | true
    "A1" | "B1" | "c"        | true
    "A1" | "B2" | "c"        | true
    "A1" | "B3" | "c"        | true
    "A1" | "A1" | "C1"       | false
    "A1" | "A1" | "C2"       | false
}

accept:拦截还是拒绝

该属性表示匹配后的数据是接收,还是拒绝,如果为true表示接收,则表示只接收按照匹配器匹配的数据,为白名单概念。如果为false,则表示值拒绝对于匹配到的数据,为黑名单概念。白名单就不再介绍,这里介绍下为false情况

@Data
@Accessors(chain = true)
public class DenyEntity {

    @Matcher(value = {"a", "b", "null"}, accept = false)
    private String name;
    @Matcher(range = "[0, 100]", accept = false)
    private Integer age;
}

拦截用例

def "测试指定的属性age"() {
    given:
    DenyEntity entity = new DenyEntity().setName(name).setAge(age)

    expect:
    def act = MkValidators.check(entity);
    Assert.assertEquals(result, act)
    if (!act) {
        println MkValidators.errMsgChain
    }

    where:
    name | age | result
    "a"  | 0   | false
    "b"  | 89  | false
    "c"  | 100 | false
    null | 200 | false
    "d"  | 0   | false
    "d"  | 200 | true
}

errMsg:自定义拦截文案

自定义的文案

version:>=1.5.0errMsg是用于在当前的数据被拦截之后的输出,比如刚开始的介绍案例,如果

@Data
@Accessors(chain = true)
public class WhiteAEntity {
    
    // 修饰属性name,只允许对应的值为a,b,c和null
    @Matcher(value = {"a","b","c","null"}, errMsg = "输入的值不符合需求")
    private String name;
    private String address;
}

在拦截的位置添加核查,这里是做一层核查,在业务代码中建议封装到aop中对业务使用方不可见即可实现拦截

import lombok.SneakyThrows;

@Test
@SneakyThrows
public void test1(){
    WhiteAEntity whiteAEntity = new WhiteAEntity();
    whiteAEntity.setName("d");

    // 可以使用带有返回值的核查
    if (!MkValidators.check(whiteAEntity)) {
        // 输出:数据校验失败-->属性 name 的值 d 不在只可用列表 [null, a, b, c] 中-->类型 WhiteAEntity 核查失败
        System.out.println(MkValidators.getErrMsgChain());
        // 输出:输入的值不符合需求
        System.out.println(MkValidators.getErrMsg());
    }

    // 或者 可以采用抛异常的核查,该api为 MkValidators.check 的带有异常的检测方式
    MkValidators.validate(whiteAEntity);
}
采用系统生成的文案

version:>=1.0.0如果我没不写errMsg,如下这种,那么返回值为系统默认的错误信息,比如

@Data
@Accessors(chain = true)
public class WhiteAEntity {
    
    // 修饰属性name,只允许对应的值为a,b,c和null
    @Matcher(value = {"a","b","c","null"})
    private String name;
    private String address;
}

执行结果

import lombok.SneakyThrows;

@Test
@SneakyThrows
public void test1(){
    WhiteAEntity whiteAEntity = new WhiteAEntity();
    whiteAEntity.setName("d");

    // 可以使用带有返回值的核查
    if (!MkValidators.check(whiteAEntity)) {
        // 输出:数据校验失败-->属性 name 的值 d 不在只可用列表 [null, a, b, c] 中-->类型 WhiteAEntity 核查失败
        System.out.println(MkValidators.getErrMsgChain());
        // 输出:属性 name 的值 d 不在只可用列表 [null, a, b, c] 中
        System.out.println(MkValidators.getErrMsg());
    }

    // 或者 可以采用抛异常的核查,该api为 MkValidators.check 的带有异常的检测方式
    MkValidators.validate(whiteAEntity);
}
errMsg中添加属性的值

version:>=1.5.1自定义文案中如果要显示我们修饰的属性的值,那么可以采用变量#current即可

@Data
@Accessors(chain = true)
public class ErrMsgEntity3 {

    @Matcher(value = {"a", "b", "c"}, errMsg = "值#current不符合要求")
    private String name;
}
def "提供占位符的要求"() {
    given:
    ErrMsgEntity3 entity = new ErrMsgEntity3().setName(name)

    expect:
    def act = MkValidators.check(entity)
    if (!act) {
        println MkValidators.getErrMsgChain()
        println MkValidators.getErrMsg()
    }
    Assert.assertEquals(result, act)

    where:
    name | result
    "a"  | true
    "b"  | true
    "c"  | true
    "d"  | false
}

四、匹配方式

前面介绍了匹配器有哪些,那么怎么进行匹配,这里简单介绍下

1.匹配器内部有多个匹配项

匹配器就是指@Matcher 这样的一个注解,这个注解中有很多属性,这些属性称之为匹配项对于修饰属性的某个匹配器而言,如果能够命中任何一个匹配项,则认为匹配上了该匹配器

@Data
@Accessors(chain = true)
public class MultiMatcherEntity {

    /**
     * 市编码
     */
    @Matcher(value = "12")
    private String cityCode;
    /**
     * num:数字为11或者0~10(包含边界值)
     */
    @Matcher(value = "11", range = "[0, 10]")
    private Integer num;

    /**
     * code:数字为33或者10~20(左开右闭)
     */
    @Matcher(value = "33", range = "(10, 20]", accept = false)
    private Integer code;
}
def "一个匹配器多配器项(或)测试"() {
    given:
    MultiMatcherEntity entity = new MultiMatcherEntity().setCityCode(cityCode).setNum(num).setCode(code)

    expect:
    boolean actResult = MkValidators.check(entity)
    if (!actResult) {
        println MkValidators.getErrMsg()
        println MkValidators.getErrMsgChain()
        println()
    }
    Assert.assertEquals(result, actResult)

    where:
    cityCode | num | code | result
    "12"     | 5   | 5    | true
    "12"     | 11  | 5    | true
    "13"     | 5   | 5    | false
    "12"     | 120 | 5    | false
    "12"     | 5   | 33   | false
    "12"     | 5   | 15   | false
}

2.多个匹配器进行匹配

如果要求有些值要进行多种条件限制,那么这个时候就要与的操作了,那么这种多种严格条件的限制,可以采用多匹配器方式,即有多个注解修饰同一个属性(注解@Matcher是支持多注解模式的)

@Data
@Accessors(chain = true)
public class MultiMatcherEntity2 {

    /**
     * 数据:为偶数,而且是在0~100这个范围
     */
    @Matcher(condition = "#current %2 ==0", errMsg = "值#current不是偶数")
    @Matcher(range = "[0, 100]", errMsg = "值#current没有在0~100范围中")
    private Integer code;
}
def "多个匹配器测试"() {
    given:
    MultiMatcherEntity2 entity = new MultiMatcherEntity2().setCode(code)

    expect:
    boolean actResult = MkValidators.check(entity)
    if (!actResult) {
        println MkValidators.getErrMsg()
    }
    Assert.assertEquals(result, actResult)

    where:
    code | result
    0    | true
    1    | false
    2    | true
    3    | false
    102  | false
}

3.多个匹配器多个不同的组

对上面2的补充,在多个不同组的情况下,也可以在不同组情况下的多个匹配

@Data
@Accessors(chain = true)
public class MultiMatcherEntity3 {


    /**
     * 数据:为偶数,而且是在0~100这个范围
     */
    @Matcher(group = "偶数", condition = "#current %2 ==0", errMsg = "值#current不是偶数")
    @Matcher(group = "偶数", range = "[0, 100]", errMsg = "值#current没有在0~100范围中")
    @Matcher(group = "奇数", condition = "#current %2 ==1", errMsg = "值#current不是奇数")
    @Matcher(group = "奇数", range = "[100, 200]", errMsg = "值#current没有在100~200范围中")
    private Integer code;
}
    def "多group的奇数配置"() {
        given:
        MultiMatcherEntity3 entity = new MultiMatcherEntity3().setCode(code)

        expect:
        boolean actResult = MkValidators.check(group, entity)
        if (!actResult) {
            println MkValidators.getErrMsg()
        }
        Assert.assertEquals(result, actResult)

        where:
        group | code | result
        "偶数"  | 0    | true
        "偶数"  | 1    | false
        "偶数"  | 2    | true
        "偶数"  | 3    | false
        "偶数"  | 102  | false

        "奇数"  | 101  | true
        "奇数"  | 102  | false
        "奇数"  | 103  | true
        "奇数"  | 201  | false
        "奇数"  | 202  | false
    }

五、核查方式

核查的时候,其实就是怎么校验数据的正确还是错误,核查函数为静态类 MkValidators 内的所有静态函数
注意:类MkValidators是版本 v1.4.5及之后改名的,之前为Checks

/**
* 核查对象
*/
public boolean check(Object object){}
/**
* 核查对象的某些属性
*/
public boolean check(Object object, String... fieldSet){}
/**
* 根据分组核查属性
*/
public boolean check(String group, Object object) {}
/**
* 核查分组中的对象的某些属性
*/
public boolean check(String group, Object object, String... fieldSet){}
/**
* 返回错误信息链
*/
public String getErrMsgChain() {}
/**
* 获取最后设置错误信息
*/
public String getErrMsg() {}

/**
 * 核查对象失败抛异常
 */
public void validate(Object object) throws MkException

/**
 * 核查对象指定属性失败抛异常
 */
public void validate(Object object, String ...fieldSet) throws MkException

/**
 * 根据组核查对象失败抛异常
 */
public void validate(String group, Object object) throws MkException

/**
 * 根据组核查对象指定属性失败抛异常
 */
public void validate(String group, Object object, String ...fieldSet) throws MkException

核查整个对象

那么会核查对象中的所有添加@Matcher注解(而且disable=false)的属性

核查某些属性

有些情况下,我们可能不是核查整个对象,而是可能核查某些属性,那么就可以使用核查某些属性的功能

@Data
@Accessors(chain = true)
public class TestEntity {

    @Matcher(value = {"nihao", "ok"}, accept = false)
    private String name;
    @Matcher(range = "[12, 32]")
    private Integer age;
    @Matcher({"beijing", "shanghai"})
    private String address;
}
def "测试指定的属性age"() {
    given:
    TestEntity entity = new TestEntity().setName(name).setAge(age)

    expect:
    def act = MkValidators.check(entity, "age");
    Assert.assertEquals(result, act)
    if (!act) {
        println MkValidators.errMsg
    }

    where:
    name     | age | result
    "nihao"  | 12  | true
    "ok"     | 32  | true
    "hehe"   | 20  | true
    "haohao" | 40  | false
}

核查某个组

对于一个属性,在不同的情况下,对这个属性值的要求可能不同,那么这个时候该怎么办呢,那么group属性就排上用场了

@Data
@Accessors(chain = true)
public class GroupEntity {

    @Matchers({
        @Matcher(range = "[50, 100]", accept = false),
        @Matcher(group = "test1", range = "[12, 23]", accept = false),
        @Matcher(group = "test2", range = "[1, 10]", accept = false)
    })
    private Integer age;

    @Matchers({
        @Matcher(value = {"beijing", "shanghai", "guangzhou"}),
        @Matcher(group = "test1", value = {"beijing", "shanghai"}),
        @Matcher(group = "test2", value = {"shanghai", "hangzhou"})
    })
    private String name;
}
def "测试指定分组指定属性"() {
    given:
    GroupEntity entity = new GroupEntity().setAge(age).setName(name)

    expect:
    def act = MkValidators.check("test1", entity, "age");
    Assert.assertEquals(result, act)
    if (!act) {
        println MkValidators.errMsgChain
    }

    where:
    age | name        | result
    10  | "shanghai"  | true
    12  | "beijing"   | false
    23  | "beijing"   | false
    50  | "beijing"   | true
    100 | "guangzhou" | true
}

六、泛型支持

前面提到的修饰的属性,其实都是各种各样的属性,但是都不是泛型类型,该框架也是支持泛型累心搞的。泛型类型分为四种: ParameterizedType:代表参数中是显而易见的类型,如:Map<String, TestEntity> TypeVariable:代表泛型类型中的字符类型,如:T t、Map<R, U>,或者Set,这种包含字符型的泛型 WildcardType:代表的是泛型类型中含有通配符?,如:Set<? extends MyEntity>或者, 或只有一个?。注意为WildcardType的前提是这个对象一定是ParameterizedType GenericArrayType:代表的范型数组。 它的组成元素是 ParameterizedType、TypeVariable、WildcardType 或者GenericArrayType

ParameterizedType

// <>标签 这种泛型
@Data
@Accessors(chain = true)
public class ParameterizedTypeEntity {

    private String word;

    @Check
    private Map<String, DataEntity> dataEntityMap;
}
def "<>符号测试"() {
    given:
    Map<String, DataEntity> dataEntityMap = new HashMap<>();
    dataEntityMap.put("a", new DataEntity().setName(name));
    ParameterizedTypeEntity entity = new ParameterizedTypeEntity().setDataEntityMap(dataEntityMap);

    expect:
    boolean actResult = MkValidators.check(entity)
    if (!actResult) {
        println MkValidators.getErrMsgChain()
        println MkValidators.getErrMsg()
    }
    Assert.assertEquals(result, actResult)

    where:
    name | result
    "a"  | true
    "b"  | true
    "c"  | false
    "d"  | false
}

TypeVariable

// TypeVariable:类型匹配符
@Data
@Accessors(chain = true)
public class TypeVariableEntity<T> {

    private Integer pageNo;
    @Matcher(range = "[0, 100]", value = "null")
    private Integer pageSize;

    @Check
    private T data;

    @Check
    private List<T> dataList;
}
def "泛型类型(字符类型)测试"() {
    given:
    DataEntity dataEntity = new DataEntity().setName(name);
    TypeVariableEntity<DataEntity> entity = new TypeVariableEntity().setPageSize(pageSize).setData(dataEntity);

    expect:
    boolean actResult = MkValidators.check(entity)
    if (!result) {
        println MkValidators.getErrMsg()
        println MkValidators.getErrMsgChain()
    }
    Assert.assertEquals(result, actResult)

    where:
    pageSize | name | result
    0        | "a"  | true
    100      | "b"  | true
    200      | "a"  | false
    100      | "c"  | false
}

WildcardType

// 通配符测试
@Data
@Accessors(chain = true)
public class WildcardTypeEntity {

   private String wildName;

   @Check
   private Map<String, ? extends DataEntity> dataMap;
}
def "通配符测试"() {
    given:
    Map<String, ChildDataEntity> dataEntityMap = new HashMap<>()
    dataEntityMap.put("a", new ChildDataEntity().setNameChild(name))
    WildcardTypeEntity entity = new WildcardTypeEntity().setDataMap(dataEntityMap)

    expect:
    boolean actResult = MkValidators.check(entity)
    if (!actResult) {
        println MkValidators.getErrMsgChain()
        println MkValidators.getErrMsg()
    }
    Assert.assertEquals(result, actResult)

    where:
    name | result
    "a"  | true
    "b"  | true
    "c"  | false
    "d"  | false
}

GenericArrayType

// 泛型数组:GenericArray
@Data
@Accessors(chain = true)
public class GenericArrayTypeEntity<T> {

    @Check
    private T[] dataArray;

    @Check
    private T[][] dataArrays;
}
def "泛型数组测试"() {
    given:
    DataEntity[] dataEntities = new DataEntity[4];
    dataEntities[0] = new DataEntity().setName(name1)
    dataEntities[1] = new DataEntity().setName(name2)
    GenericArrayTypeEntity<DataEntity> entity = new GenericArrayTypeEntity().setDataArray(dataEntities)

    expect:
    boolean actResult = MkValidators.check(entity)
    if (!actResult) {
        println MkValidators.getErrMsgChain()
        println MkValidators.getErrMsg()
    }
    Assert.assertEquals(result, actResult)

    where:
    name1 | name2 | result
    "a"   | "a"   | true
    "a"   | "b"   | true
    "b"   | "a"   | true
    "b"   | "b"   | true
    "c"   | "b"   | false
    "c"   | "c"   | false
}

交流反馈:

技术讨论群:
请先加WX(zhoumo187108),并注明来源

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值