我们在开发过程中可能会遇到很多的校验,这些校验很大一部分是可以通过注解来完成,但是java那些包提供的注解毕竟是有限的(如javax.validation.constraints包,hibernate validation),我们可能就需要使用自定义一个注解来完成入参的校验。
定义一个注解
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* @author cly
* @create 2019-05-16 20:02
*/
@Target({ ElementType.FIELD })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = { ValidNameNotContainValue.class })
public @interface NameNotContainValue {
String message() default "不能包含非法字符";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String value() default "";
}
这里我们会用到一个叫做元注解的东西,我们可以认为他是注解的注解,帮助我们实现我们想要实现的注解,元注解包括:@Retention、 @Target、 @Document、 @Inherited和@Repeatable(JDK1.8加入)五种
@Target
使用@Target元注解表示我们的注解作用的范围就比较具体了,可以是类,方法,方法参数变量等,同样也是通过枚举类ElementType表达作用类型
- @Target(ElementType.TYPE) 作用接口、类、枚举、注解
- @Target(ElementType.FIELD) 作用属性字段、枚举的常量
- @Target(ElementType.METHOD) 作用方法
- @Target(ElementType.PARAMETER) 作用方法参数
- @Target(ElementType.CONSTRUCTOR) 作用构造函数
- @Target(ElementType.LOCAL_VARIABLE)作用局部变量
- @Target(ElementType.ANNOTATION_TYPE)作用于注解(@Retention注解中就使用该属性)
- @Target(ElementType.PACKAGE) 作用于包
- @Target(ElementType.TYPE_PARAMETER) 作用于类型泛型,即泛型方法、泛型类、泛型接口 (jdk1.8加入)
- @Target(ElementType.TYPE_USE) 类型使用.可以用于标注任意类型除了 class (jdk1.8加入)
@Retention
使用@Retention表示注解存在阶段是保留在源码(编译期),字节码(类加载)或者运行期(JVM中运行)。在@Retention注解中使用枚举RetentionPolicy来表示注解保留时期
- @Retention(RetentionPolicy.SOURCE),注解仅存在于源码中,在class字节码文件中不包含
- @Retention(RetentionPolicy.CLASS), 默认的保留策略,注解会在class字节码文件中存在,但运行时无法获得
- @Retention(RetentionPolicy.RUNTIME), 注解会在class字节码文件中存在,在运行时可以通过反射获取到
这里我们用的注解校验是作用于运行时期。如果有使用lombok你会发现lombok的注解仅存在源码中,目的仅仅只是为了生成那些getter,setter方法
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface Data {
/**
* If you specify a static constructor name, then the generated constructor will be private, and
* instead a static factory method is created that other classes can use to create instances.
* We suggest the name: "of", like so:
*
* <pre>
* public @Data(staticConstructor = "of") class Point { final int x, y; }
* </pre>
*
* Default: No static constructor, instead the normal constructor is public.
*
* @return Name of static 'constructor' method to generate (blank = generate a normal constructor).
*/
String staticConstructor() default "";
}
@Documented
它的作用是能够将注解中的元素包含到 Javadoc 中去。这个注解我们用的不多
@Inherited
Inherited的英文意思是继承,但是这个继承和我们平时理解的继承大同小异,一个被@Inherited注解了的注解修饰了一个父类,如果他的子类没有被其他注解修饰,则它的子类也继承了父类的注解。
@Repeatable
Repeatable的英文意思是可重复的。顾名思义说明被这个元注解修饰的注解可以同时作用一个对象多次,但是每次作用注解又可以代表不同的含义。这个注解是java8加入的一个新注解,在此之前我们可能会碰到一些校验问题,既想让这个属性满足A条件又想让他满足B条件,但是如果重复使用注解java会报错,那么程序猿们就只能再写一个注解来实现,这样就会让代码变得十分难看,于是java8支持了重复使用同一个注解。如果是一个正则@Pattern校验,他们的关系是且关系而非或关系。
那么回到我们上面的那个注解定义上
@Constraint 与约束注解关联的验证器,也就是校验的逻辑需要写在ValidNameNotContainValue类里。
message() 校验不通过的错误信息
groups() 分组校验,一般与@Validated搭配使用,可以新建一个接口作为一个分组条件
payload() 约束注解的有效负载,这个一般默认的就行,没怎么用到
value() 自定义的值,比如这里我们把value作为一个不能哪些包含字符串,你也可以定义其他的字段名,叫value可以不用在注解上加字段名=xxxx
实现注解
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
/**
* @author cly
* @create 2019-05-16 20:04
*/
public class ValidNameNotContainValue implements ConstraintValidator<NameNotContainValue, String> {
private String notContainValue;
@Override
public void initialize(NameNotContainValue constraintAnnotation) {
this.notContainValue = constraintAnnotation.value();
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if(value.contains(notContainValue)){
return false;
}
return true;
}
}
这个实现类需要实现ConstraintValidator<A extends Annotation, T> 接口,泛型A就是你定义的注解,T就是你注解作用的类型,如作用于String字段就是String了。这个类重写了接口的两个方法initialize(),isValid():
-方法 initialize :对验证器进行实例化,它必须在验证器的实例在使用之前被调用,并保证正确初始化验证器,它的参数是约束注解;
-方法 isValid: 是进行约束验证的主体方法,其中 value 参数代表需要验证的实例,context 参数代表约束执行的上下文环境。在这里我们初始化拿到了value的值,在isValid中运用了这个value的值来校验
@NameNotContainValue("a")
private String name;
这里我们的字段定义是vaue所以是可以把value隐去。
验证注解是否生效
这里我们用到了之前的文章里的校验方法
ValidatorFactory vf = Validation.buildDefaultValidatorFactory();
Validator validator = vf.getValidator();
User user = new User();
user.setName("asdsds");
Set<ConstraintViolation<User>> set = validator.validate(user);
for (ConstraintViolation<User> constraintViolation : set) {
System.out.println(constraintViolation.getMessage());}
}
扩展
我们从上面的元注解知道了java8现在支持一个注解在同一个属性下多次使用,比如我这里希望name属性不仅仅不能包含"a",他也不能包含"b",那么我们怎么解决呢
@Target({ FIELD })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = { ValidNameNotContainValue.class })
@Repeatable(NameNotContainValues.class)
public @interface NameNotContainValue {
String message() default "不能包含非法字符";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String value() default "";
@Target({ FIELD })
@Retention(RUNTIME)
@Documented
@interface NameNotContainValues {
NameNotContainValue[] value();
}
}
我们使用了 @Repeatable注解,在注解里加了一个容器,重复的注解就会被放到容器里一一校验,其value方法(默认方法)需要返回其元注解的列表,否则这个容器类设计将不符合规范。
@NameNotContainValue("b")
@NameNotContainValue("a")
private String name;
User user = new User();
user.setName("azzbcd");
我们可以看到属性被校验了两次,达到了我们的目的。
参考:https://juejin.im/post/5cc7133f5188252532632f35
https://www.ibm.com/developerworks/cn/java/j-lo-beanvalid/index.html