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<MyConstraint, Any> 注意这个 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<MyConstraint, Any> {
@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<? extends Payload>[] 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<? extends Payload>[] 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<KClass<*>> = [],
val payload: Array<KClass<out Payload>> = []
)
可以看到的是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学习书写的笔记。