简单实现 Bean 字段校验
关于 Bean 字段校验,我之前曾用 Apache BVal 探讨过,现在连这货都不想用,少一个依赖是一个。自己做,若完全按照 JSR 303 规范来实现会非常麻烦,没有那个必要。于是取舍一下,还是沿用 JSR 303 的注解作为约束条件,参考这位仁兄的基于反射的做法,自己实现一套 Bean 校验。
原理总的来说是,反射+自定义函数接口(Java 8)+Map 关联注解与验证实现,比较简单,顶多 100 行代码搞定,都是本着咱够用就行的要求,其他的不想 BB 那么多,要是真有问题,到时再说。
首先写个单测,Bean 如下:
class News {
@NotNull
private long id;
@NotBlank(message = "请输入名称")
private String name;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
可以看到,JSR 的 @NotNull
和 @NotBlank
分别绑定在 id 和 name 属性上,约束 id 字段不能为 null,由于 id 是 long 类型,故也不能为 0;name 属性不能为空字符串。name 属性还自定义了出错时的信息 message。
自定义函数接口
当 JDK 自带的函数接口类型不能满足时,就要自定义函数接口。BiFunction
只能支持两个参数,当下我们的场景是一个是 Bean 属性的值,一个是 Bean 属性对象本身(Field,又称字段对象,反射得来的),最后是约束条件,即注解,——一共三个参数,故 BiFunction
不能满足,只能自己另外写,如下所示。
package com.ajaxjs.validator;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
@FunctionalInterface
public interface Validator {
/** * 执行验证 * * @param value Bean 字段的值 * @param field Bean 字段对象 * @param ann Bean 字段上面的注解 * @return 错误信息,如果为 null 表示完全通过 */
public String valid(Object value, Field field, Annotation ann);
}
返回值是 String,指错误信息,如果通过则返回 null,非 null 说明哪一个属性(字段)不符合要求,这个 String 就是不符合要求的原因了。
验证器存储结构
不知如何更好地表达,存储结构——好像怪怪的,反正,就是一个简单 Map:key 是注解类,value 是验证码,这样它们构成了一对一的关系,作为静态成员保存着。怎么用?下面反射的时候会说,你看了就明白 Map 那样用的,一点都不复杂。
package com.ajaxjs.validator;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import com.ajaxjs.util.logger.LogHelper;
public class BeanValidator {
private static final LogHelper LOGGER = LogHelper.getLog(BeanValidator.class);
/** * 构成注解与验证器一一对应的关系 */
private static final Map<Class<?>, Validator> cache = new HashMap<>();
/** * 注册一个验证器 * * @param clzs 注解类 * @param validator 验证器 lambda */
public static void register(Class<?> clzs, Validator validator) {
cache.put(clzs, validator);
}
static {
register(NotNull.class, BuiltinValidator.NOT_NULL_VALIDATOR);
register(NotBlank.class, BuiltinValidator.NOT_BLANK_VALIDATOR);
}
……
}
验证器用之前需要注册,无非就是 put 进 Map 里面去,例如 register(NotNull.class, BuiltinValidator.NOT_NULL_VALIDATOR);
。NOT_NULL_VALIDATOR 就是应用函数接口的验证器,对应 @NotNull
的情况。NOT_NULL_VALIDATOR 就是一个普通的 lambda,前面已经说了,就是把握函数的参数和返回值,具体用途是什么,为什么要传那些参数?够不够用?返回哪种类型结果?NOT_NULL_VALIDATOR 源码如下。
public static final Validator NOT_NULL_VALIDATOR = (value, field, ann) -> {
if (value == null) {
NotBlank n = (NotBlank) ann;
return n.message() != null ? n.message() : field.getName() + " 不能为 null";
} else if (value != null && value instanceof Number) {
Number num = (Number) value;
if (num.equals(0) || num.equals(0L)) {
NotNull n = (NotNull) ann;
return n.message() != null ? n.message() : field.getName() + " 不能为 null";
} else
return null;
} else
return null;
};
执行验证
如果只是学怎么用,那么上面原理性的内容是不用看的,只是学会调用者 API 唯一的暴露方法 BeanValidator.validate(Object bean)
即可。这里就是对 Bean 反射操作,获取所需的信息用于判读是否符合 Bean 要求。
/** * 校验实体 * * @param bean 实体 * @return 错误集合,若数组为 length=0表示完全通过 */
public static String[] validate(Object bean) {
List<String> list = new ArrayList<>();
Class<?> cls = bean.getClass();
Field[] fields = cls.getDeclaredFields();
try {
// 获取实体字段集合
for (Field f : fields) { // 通过反射获取该属性对应的值
f.setAccessible(true);
Object value = f.get(bean);// 获取字段值
Annotation[] arrayAno = f.getAnnotations();// 获取字段上的注解集合
for (Annotation annotation : arrayAno) {
Class<?> clazz = annotation.annotationType();// 获取注解类型(注解类的Class)
Validator validator = cache.get(clazz);
if (validator == null) // 不是验证器的注解
continue;
String result = validator.valid(value, f, annotation);
if (result != null)
list.add(result);
}
}
} catch (Exception e) {
LOGGER.warning(e, "验证出错");
}
return list.toArray(new String[list.size()]);
}
小结
BuiltinValidator 内建的验证码考虑了一般情况,如非空、Max/Min/Size 等,也就是 JSR 默认那些。用户可以继续扩展,给出自己的验证码,然后注册一下即可,比说试试写个身份证验证的……就留给读者去做吧~
以上所有源码可以在 https://gitee.com/sp42_admin/ajaxjs 找到。
还没有评论,来说两句吧...