Bootstrap

Java:基于注解对类实例字段进行通用校验

前言

后台服务处理前端的请求时,会有这样的一种需求,即校验请求中的参数是否符合校验规则。校验参数是否符合的一种方法是,罗列请求参数,基于校验规则一个一个的校验参数,如果存在不符合的,就返回字段值不符合规则的提示,通过就向下执行。这种方法是可以的,但是不通用,因为需要校验请求参数的地方太多了,罗列式的校验参数会显得效率低下,且,字段的校验规则都是大同小异的,这就有重复造轮子的可能,这是不可取的。所以,应该提取字段的通用校验代码,做到只要在接收参数的model中,通过注解在需要校验的字段上定义好字段不通过时的错误提示,再定义好字段的校验器(校验器是可复用的),即可校验model中的参数是否符合校验规则,如果不通过,返回字段对应的错误提示,如果通过,就返回null。接下来,就分别从代码包结构,两个注解,校验器、校验主逻辑和示例等几个方面,介绍对model的字段进行通用校验的代码。

包结构

如下为model字段校验通用代码的包结构,其中,annotation包中的两个注解之一JavaField是标注在字段上的,用于给字段添加校验规则,JavaFileds用于实现可重复添加相同的注解。validator包中的接口Validator是字段校验器抽象接口,用于定义字段的校验规则,impl包中的是实现Validator接口的实现类,是字段的不同校验器。ValidateUtils是基于注解的实现字段校验的主逻辑代码。
在这里插入图片描述

注解

JavaField注解是添加到需要进行字段值校验的字段上的,用于定义字段校验规则,其有两个参数,一是message,为字段校验不通过时的错误提示,二是validator,它的类型是Class,用于存储校验器的Class对象。

JavaField
package com.nursehealth.util.common.ValidateUtils.annotation;

import java.lang.annotation.*;

/**
 * @author wengym
 * @version 1.0
 * @desc Java字段校验注解
 * @date 2022/12/20 11:05
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(JavaFields.class)
public @interface JavaField {
    /**
     * 校验不通过的提示信息
     */
    String message();
    /**
     * 规则校验器
     */
    Class validator();
}

由于字段的校验可能是多重的,如先要判断字段值是否为空,如果不为空,还要判断值是否符合其他规则,而默认情况下,字段上是不能添加重复的注解的,所以需要让注解可重复添加。为了做到注解可重复添加,首先定义JavaFields注解(注解的名称可随意),该注解需要定义value()方法,注意,该方法的名称就是value而不能是其他的,方法的返回值是需要重复添加的注解数组,即JavaField[]。然后在需要重复添加的注解中,添加@Repeatable(JavaFields.class),如此,即可让注解重复添加。

JavaFields
package com.nursehealth.util.common.ValidateUtils.annotation;

import java.lang.annotation.*;

/**
 * @author wengym
 * @version 1.0
 * @desc Java字段校验注解
 * @date 2022/12/27 11:05
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface JavaFields {
    JavaField[] value();
}

校验器

校验器中只有一个校验字段的方法,该方法的作用是接收字段值,然后校验字段值是否符合规则,如果符合则返回true,如果不符合则返回false。校验器是可以复用的,即同一类的校验只要定义一个校验器即可,不用重复写,如判空校验器,只要定义了,就可处处调用。

接口
package com.nursehealth.util.common.ValidateUtils.validator;

public interface Validator {
    /**
     * 校验字段值是否符合规则,通过为true,不通过为false
     *
     * @param fieldValue
     *
     * @author wengym
     *
     * @date 2022/12/20 11:08
     *
     * @return java.lang.Boolean
     */
    Boolean validateField(Object fieldValue);
}
接口实现类
EmptyValidator:判空校验器

字段值为null,为空串,或为为null字符串,表示字段为空,返回false,表示不通过,否则返回true,表示通过。

package com.nursehealth.util.common.ValidateUtils.validator.impl;

import com.nursehealth.util.common.ValidateUtils.validator.Validator;

/**
 * @author wengym
 * @version 1.0
 * @desc 空字段校验器
 * @date 2022/12/20 11:12
 */
public class EmptyValidator implements Validator {

    @Override
    public Boolean validateField(Object fieldValue) {
        if (fieldValue == null || "".equals(fieldValue) || "null".equals(String.valueOf(fieldValue).trim())) {
            return false;
        }
        return true;
    }
}
GenderValidator:性别校验器

性别用1表示男,2表示女,性别只能是1和2,性别为1和2时,返回true,表示通过,否则返回false,表示不通过。

package com.nursehealth.util.common.ValidateUtils.validator.impl;

import com.nursehealth.util.common.ValidateUtils.validator.Validator;

/**
 * @author wengym
 * @version 1.0
 * @desc 性别校验器
 * @date 2022/12/27 11:05
 */
public class GenderValidator implements Validator {

    @Override
    public Boolean validateField(Object fieldValue) {
        String gender = (String)fieldValue;
        if ("1".equals(gender) || "2".equals(gender)) {
            return true;
        }
        return false;
    }
}

校验主逻辑

校验主逻辑要做的就是遍历model字段,再取出字段值,取出字段的校验注解(有多个时是JavaFields,单个时是JavaField,需要分开处理),再实例化校验注解的校验器,然后就基于字段值调用校验器的校验字段方法(validateField),返回的结果如果为true,表示校验通过,则向下执行,如果为false,表示校验不通过,则停止向下执行,并返回校验注解的错误提示信息。如果遍历完字段后,还是没有不通过的字段,则返回null,表示model的参数都通过了校验。

package com.nursehealth.util.common.ValidateUtils;

import com.nursehealth.util.common.ValidateUtils.annotation.JavaField;
import com.nursehealth.util.common.ValidateUtils.annotation.JavaFields;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * @author wengym
 * @version 1.0
 * @desc Java字段检验工具类
 * @date 2022/12/20 11:17
 */
public class ValidateUtils {
    /**
     * 检验model中的字段值是否符合规则
     *
     * @param model
     * @return java.lang.String
     * @author wengym
     * @date 2022/12/20 11:18
     */
    public static <T> String validate(T model) {
        if (model == null) {
            throw new NullPointerException("参数不能为null");
        }
        Class cls = model.getClass();
        Field[] fields = cls.getDeclaredFields();
        String validateResult;
        for (Field field : fields) {
            Annotation[] ans = field.getAnnotations();
            if (ans.length < 1) {
                continue;
            }
            for (Annotation an : ans) {
                if (an instanceof JavaFields) {
                    JavaField[] values = ((JavaFields)an).value();
                    for (JavaField javaField : values) {
                        validateResult = handleJavaField(javaField, model, field);
                        if (validateResult != null) {
                            return validateResult;
                        }
                    }
                }
                if (an instanceof JavaField) {
                    JavaField value = (JavaField)an;
                    validateResult = handleJavaField(value, model, field);
                    if (validateResult != null) {
                        return validateResult;
                    }
                }
            }
        }
        return null;
    }

    /**
     * 处理单个字段校验注解
     *
     * @param javaField
     *
     * @param model
     *
     * @param field
     *
     * @author wengym
     *
     * @date 2022/12/27 11:24
     *
     * @return java.lang.String
     */
    private static <T> String handleJavaField(JavaField javaField, T model, Field field) {
        try {
            // 校验器的Class
            Class validatorCls = javaField.validator();
            // 检验方法
            Method validateMethod = validatorCls.getMethod("validateField", Object.class);
            // 调用方法
            field.setAccessible(true);
            Object fieldValue = field.get(model);
            field.setAccessible(false);
            Object result = validateMethod.invoke(validatorCls.newInstance(), fieldValue);
            // 结果处理
            Boolean isPass = (Boolean) result;
            if (!isPass) {
                // 没有通过,返回错误提示信息
                return javaField.message();
            }
        } catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
            e.printStackTrace();
        }
        return null;
    }
}

示例

model
package com.nursehealth.model.user;

import com.nursehealth.util.common.ValidateUtils.annotation.JavaField;
import com.nursehealth.util.common.ValidateUtils.validator.impl.EmptyValidator;
import com.nursehealth.util.common.ValidateUtils.validator.impl.GenderValidator;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @author wengym
 * @version 1.0
 * @desc 用户model
 * @date 2022/12/19 15:52
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class UserModel {
    /**
     * 用户ID
     */
    @JavaField(message = "用户ID不能为空", validator = EmptyValidator.class)
    private String userId;
    
    /**
     * 性别
     */
    @JavaField(message = "性别不能为空", validator = EmptyValidator.class)
    @JavaField(message = "性别只能为男或女", validator = GenderValidator.class)
    private String gender;
}
具体使用

调用ValidateUtils的validate静态方法校验model中的字段值是否符合规则,如果userId为空,则会返回“用户ID不能为空”,如果gender为空,则会返回“性别不能为空”,如果gender为3,则会返回“性别只能为男或女”,如果字段值符合字段校验规则,则返回null,所以可通过判断校验结果是否为null来判断校验是否通过。

public Object validate(UserModel model) {
    String result = ValidateUtils.validate(model);
    if (!CommonUtil.isNullStr(result)) {
        return APIResponse.errorBack(result);
    }
}
;