kotlin validation 自定义校验注解

hibernate validation 提供了很多做数据校验用的注解类,我相信同学们一定使用过@Valid 注解和 BindingResult 验证请求参数的合法性并处理校验结果。

update 方法

    @PutMapping(value = ["/{id:\\d+}"])
    fun update(@Valid @RequestBody user: User, errors: BindingResult): User {

        if (errors.hasErrors()) {         
          errors.allErrors.forEach { error -> println(error.defaultMessage)
         }     
        println(user.id)
        user.id = "1"
        return user            
    }

  @Valid 与 @NotBlank @Email @Pattern 要配合使用,这样在传入的参数发生错误时才能在控制台输出错误的信息。BindingResult 有着这样一个作用,它能让我们带着异常进入到update这个方法中,并通过判断 if (errors.hasErrors()) { } 将非法的参数记录下来。

  虽然hibernate validator 有提供很多甚至像@Pattern这样可以用正则表达式去自定义注解,但是很多情况下hibernate提供的这些注解,并不能满足我们的需求 只是非常简单的逻辑最复杂的只是让你写个正则表达式如图中的@Pattern注解类, 但是很多时候我们校验是要基于数据的比如说这个用户 这个订单是不是存在它在数据库是不是重复的等等 这些校验他不是简单的判断一下你传上来的值就可以了,它还需要其他的校验逻辑,这是我们必须要自己去写一些校验的逻辑,那么自己去写这些校验逻辑就需要自己写注解了。

   在这之前我们先创建一个 MyConstraintValidator.kt 这个类,它就是我们的校验逻辑类,并且让它继承 ConstraintValidator 注意这个 Any 在 java 中就是 object 这个类,kotlin中有一个长得很像的一个类叫 Objects,注意就是多了个s。默认继承的时候一般选中String,今天从java代码中照搬的时候不小心就写成了Objects,这个如果错写成Objects 控制台会报一个 No validator could be found for constraint 并抛出一个UnexpectedTypeException 异常。但如果你写成Object 它是不会抛出异常的而且顺利执行,但是idea会有警告。就因为多出来的这个s,花了我一下午的时间去排错。但也是因为这个s,我才发现我泛型写错了。

MyConstraintValidator.kt 类
package com.fara.security.demo.validator

import com.fara.security.demo.service.HelloService
import org.springframework.beans.factory.annotation.Autowired
import javax.validation.ConstraintValidator
import javax.validation.ConstraintValidatorContext

/**
 * Created by 黄德辉 on 2018/04/11 15:56
 **/
class MyConstraintValidator : ConstraintValidator {

    @Autowired
    private lateinit var helloService: HelloService

    /**
     * 初始化时调用
     */
    override fun initialize(constraintAnnotation: MyConstraint?) {
        super.initialize(constraintAnnotation)

        println("my validator init 初始化方法")
    }
    override fun isValid(value: Any?, context: ConstraintValidatorContext?): Boolean {

        helloService.greeting("huangdehui") //此处没有在控制台输出日志
        println(value)

        //返回 false 会显示出 message = "这是一个测试方法的值“
        return false
    }
}

写完这个类以后,我们添加这个类里面未实现的方法,isValid,再重写一个 initialize。它一共有两个方法,一个是初始化,一个是校验器,那么iniitialize这个方法我们简单的打一句话就可以了 println("my validator init 初始化方法"), 这个就是初始化要完成的事情,另一个就是校验逻辑 isValid 方法 一共有两个参数,一个是你校验时传进来的值 value 然后另一个是你的上下文 context 这里面真正包含了你注解类的一些值,我们直接吧这个值打出来,要注意的是这里面你可以使用 spring 里的注解,比如 @Autowired 我举个例子创建一个 helloService ,注意这个类不需要在上面加 @Component spring 看到 MyConstraintValidator 实现了ConstraintValidator这个接口后,它会自动的把它做为springBean。

那么我们接下去在User类中为username字段添加的@MyConstraint注解类,就是通过@Constraint(validatedBy = [MyConstraintValidator::class])注解的方式指定了约束的这个类。那么当我们这个 Controller update这个方法它在校验User的时候,它发现User上面写的 @MyConstraint(message = "这是一个测试") 时候 它就会调用 MyConstraintValidator 里面的,isValid方法,然后它会把请求体中 json 格式对应username的值,当做参数传进来然后我会根据这个值去校验,返回true 或者 false ,返回失败的时候它就会把 @MyConstraint(message="这是一个测试"),注解中的massage存放到我们最终的 BindingResult errors 中的这个对象里去最后通过

if (errors.hasErrors()) {         
          errors.allErrors.forEach { error -> println(error.defaultMessage)
 }    

这种方式循环出来 打到控制台上,通过这个循环你还可以将信息打印到对应的日志上。

那么先来观察一下这个User类

User类

/**
 * Created by 黄德辉 on 2018/04/11 04:38
 **/
class User {

    interface UserSimpleView

    //User详细视图 要继承 User简单视图 在显示详细视图的时候会把简单视图里全部显示出来
    interface UserDetailView : UserSimpleView

    @JsonView(UserSimpleView::class)
    var id: String? = ""

    @MyConstraint(message = "这是一个测试")
    @JsonView(UserSimpleView::class)
    var username: String? = ""

    @NotBlank(message = "密码不能为空")
    @JsonView(UserDetailView::class)
    var password: String? = ""
    /**
     * 日期在前后分离的情况下最好存时间戳,格式的显示交给前端来处理
     * 有些场景需要 网页上的时间格式与手机上的不一样.不要去传带格式的时间
     * 前端要展现成什么让前台来决定 后台就传时间就可以了
     */
    @Past(message = "生日必须是过去时间") //必须是一个过去的时间 因为人的生日肯定是一个过去的时间,不可能是未来的时间
    @JsonView(UserSimpleView::class)
    var birthday: Date? = Date()

    @Pattern(regexp = "[1-7]{1}", message = "reason的类型值为1-7中的一个类型")
    val reson:String? = null
}

这里有一个注解类叫@MyConstraint(message = "这是一个测试"),这就是我们要实现的注解那么该怎么写呢?我们先按住 Ctrl 这个键然后点击鼠标右键查看@NotBlank 注解类,参考一下这个java注解类。

@NotBlank 注解类
@Documented
@Constraint(validatedBy = { })
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(List.class)
public @interface NotBlank {

    String message() default "{javax.validation.constraints.NotBlank.message}";

    Class[] groups() default { };

    Class[] payload() default { };

    /**
     * Defines several {@code @NotBlank} constraints on the same element.
     *
     * @see NotBlank
     */
    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
    @Retention(RUNTIME)
    @Documented
    public @interface List {
        NotBlank[] value();
    }
}

在这里有 validation 注解类需要的非常重要的三个成员变量分别是

  • String message() default "{javax.validation.constraints.NotBlank.message}";
  • Class[] groups() default { };
  • Class[] payload() default { };

任何你自己写的 validator 注解,都必须包含 message groups payload 这三个成员变量。
message 想必大家都知道了, groups 与 payload 都是 hibernate validator 里的概念,关系不大就不深入的去介绍,有兴趣的同学可以自己去看一下相关的文档。

那么这里我们依葫芦画瓢创建一个注解类 MyConstraint.kt

MyConstraint.kt 注解类
package com.fara.security.demo.validator

import javax.validation.Constraint
import javax.validation.Payload
import kotlin.reflect.KClass

/**
 * Created by 黄德辉 on 2018/04/11 20:18
 **/
@Target(AnnotationTarget.FIELD, AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
@Constraint(validatedBy = [MyConstraintValidator::class])
annotation class MyConstraint(
        val message: String,
        val groups: Array> = [],
        val payload: Array> = []
)

可以看到的是kotlin的语法真的很简单哈,我们照搬 @NotBlank 这个注解后,这样就有了一个注解,以及它相应的校验器了,这时候就可以将这个注解类写到任何一个类字段或方法上面了。

@MyConstraint(message = "这是一个测试")
    @JsonView(UserSimpleView::class)
    var username: String? = ""

当调用到 Controller 中的这个update 方法的时候


    @PutMapping(value = ["/{id:\\d+}"])
    fun update(@Valid @RequestBody user: User, errors: BindingResult): User {

        if (errors.hasErrors()) {         
          errors.allErrors.forEach { error -> println(error.defaultMessage)
         }     
        println(user.id)
        user.id = "1"
        return user            
    }

它就会自动去校验 User ,并将传入username的字段值,以参数的形式传入到MyConstraintValidator.kt中的isValid方法中进行校验,方法返回 false 的时候它既能抛出异常也可以记录用户是否传入了什么样的非法值。

其实这个@MyConstraint自定义注解类最花费我时间的地方就是就是将message groups payload 这三个成员变量这三个成员变量如何用 kotlin 来表达,对于我这个刚入门kotlin第二天的新手而言我翻烂了官方文档有关于这三个成员变量的书写,由于在 Kotlin 中 没有成员变量声明是用 [ ] 这种方式来实现的,还有对java注解类的不了解给我带来了非常多的困惑与麻烦。如果对 java 注解类不熟悉的同学们可以参考我这篇文章 java 注解类 。

该学习笔记的源代码 https://gitee.com/huangdehui/fara-security-demo 来源于spring security demo 根据慕课网上spring security学习书写的笔记。

你可能感兴趣的:(kotlin validation 自定义校验注解)