版本概要
springboot版本2.1.6.RELEASE
kotlin版本1.2.71
gradle版本5.2.1
idea版本2019.1.2 ultimate edition
新建项目
- 点击file -> new project ->选择spring initializrd点击下一步
- 选择语言,选择项目管理工具
- 此篇我们讨论只涉及校验,只引入springWebStarter即可
- 选择gradle路径(或者使用默认的),这里我选择本地路径
- 输入项目路径,点击finish,查看项目结构,我们可以看到生成的gradle依赖文件变成了kts后缀的文件,和之前比起来,配置会略有不同
- 增加国内镜像地址
追加根节点1
2
3
4repositories {
maven (url = "http://maven.aliyun.com/nexus/content/groups/public/")
jcenter()
} - 重新导入等待编译完成
重现无法校验场景
- 增加测试控制器和测试类,这里我直接加在了启动类上
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class DemoApplication {
fun demo(User) user: : User {
return user
}
fun demoList(List<User>) user: : List<User> {
return user
}
}
class User {
val username: String? = null
val password: String? = null
}
fun main(args: Array<String>) {
runApplication<DemoApplication>(*args)
} - 增加测试类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class WebMvcTest {
lateinit var mockMvc: MockMvc
fun testDemo() {
val example = "{\"username\":\"\", \"password\":\"111\"}"
mockMvc.perform(MockMvcRequestBuilders.post("/demo")
.contentType(MediaType.APPLICATION_JSON_VALUE).content(example))
.andExpect(status().isOk).andDo { println(it.response.contentAsString) }.andReturn()
}
fun testDemoList() {
val example = "[{\"username\":\"\", \"password\":\"111\"}]"
mockMvc.perform(MockMvcRequestBuilders.post("/demoList")
.contentType(MediaType.APPLICATION_JSON_VALUE).content(example))
.andExpect(status().isOk).andDo { println(it.response.contentAsString) }.andReturn()
}
} - 我们先执行能够正常执行校验的测试方法testDemo,执行后发现控制台报错,说明参数被校验了
- 但执行List时参数却未被校验
处理方案
方案1:
- 新建类包装List,在list上加上@Valid(javax包中)(由于@Validated不支持放在字段上,所以无法使用)注解
- 控制器方法
1
2
3
4
fun demoValidList(ValidList<User>) user: : List<User>? {
return user.list
} - 包装类
1
2
3
4class ValidList<T> {
val list: List<T>? = null
} - 测试方法
1
2
3
4
5
6
7
fun testDemoValidList() {
val example = "{\"list\":[{\"username\":\"\", \"password\":\"111\"}]}"
mockMvc.perform(MockMvcRequestBuilders.post("/demoValidList")
.contentType(MediaType.APPLICATION_JSON_VALUE).content(example))
.andExpect(status().isOk).andDo { println(it.response.contentAsString) }.andReturn()
}
方案2:
- 控制器方法
- 我们可以采用实现list接口并转接方法的方式,去掉这层无用的嵌套
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87class ValidList1<T> : MutableList<T> {
override fun iterator(): MutableIterator<T> {
return list.iterator()
}
override fun listIterator(): MutableListIterator<T> {
return list.listIterator()
}
override fun listIterator(index: Int): MutableListIterator<T> {
return list.listIterator(index)
}
override fun subList(fromIndex: Int, toIndex:Int): MutableList<T> {
return list.subList(fromIndex, toIndex)
}
override fun add(element: T): Boolean {
return list.add(element)
}
override fun add(index: Int, element: T) {
return list.add(index, element)
}
override fun addAll(index: Int, elements: Collection<T>): Boolean {
return list.addAll(index, elements)
}
override fun addAll(elements: Collection<T>): Boolean {
return list.addAll(elements)
}
override fun clear() {
list.clear()
}
override fun remove(element: T): Boolean {
return list.remove(element)
}
override fun removeAll(elements: Collection<T>): Boolean {
return list.removeAll(elements)
}
override fun removeAt(index: Int): T {
return list.removeAt(index)
}
override fun retainAll(elements: Collection<T>): Boolean {
return list.retainAll(elements)
}
override fun set(index: Int, element: T): T {
return list.set(index, element)
}
override val size: Int
get() = list.size
override fun contains(element: T): Boolean {
return list.contains(element)
}
override fun containsAll(elements: Collection<T>): Boolean {
return list.containsAll(elements)
}
override fun get(index: Int): T {
return list[index]
}
override fun indexOf(element: T): Int {
return list.indexOf(element)
}
override fun isEmpty(): Boolean {
return list.isEmpty()
}
override fun lastIndexOf(element: T): Int {
return list.lastIndexOf(element)
}
val list: MutableList<T> = mutableListOf()
}- 测试控制器
1
2
3
4
fun demoValidList(ValidList1<User>) user: : List<User>? {
return user.list
} - 测试方法
1
2
3
4
5
6
7
fun testDemoValidList1() {
val example = "[{\"username\":\"\", \"password\":\"111\"}]"
mockMvc.perform(MockMvcRequestBuilders.post("/demoValidList1")
.contentType(MediaType.APPLICATION_JSON_VALUE).content(example))
.andExpect(status().isOk).andDo { println(it.response.contentAsString) }.andReturn()
}
- 测试控制器
- 使用kotlin委托我们可以节省部分代码(注意直接在list上增加@Valid是无效的)
- 委托器
1
2
3
4
5class ValidList2<T>(val list: MutableList<T>) : MutableList<T> by list {
var mlist: MutableList<T> = list
constructor() : this(mutableListOf())
} - 控制器
1
2
3
4
fun demoValidList2(ValidList2<User>) user: : List<User>? {
return user.list
} - 测试类
1
2
3
4
5
6
7
fun testDemoValidList2() {
val example = "[{\"username\":\"\", \"password\":\"111\"}]"
mockMvc.perform(MockMvcRequestBuilders.post("/demoValidList2")
.contentType(MediaType.APPLICATION_JSON_VALUE).content(example))
.andExpect(status().isOk).andDo { println(it.response.contentAsString) }.andReturn()
}
- 委托器
- 如果使用的是java我们也可以利用lombok替我们节省部分代码
1
2
3
4
5
public class ValidList<T> implements List<T>{
List<T> list = new ArrayList<>();
}
由于未做异常拦截,以上方案 正常校验,验证器将抛出异常
方案1和方案2本质上是一致的,缺点在于对于控制器代码的侵入性较大(意味着所有需要校验list的控制器方法都需要修改类为新的包装类)
方案3
- 我们可以自定义验证器并配置@ControllerAdvice统一为集合增加验证器
- 自定义验证器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23class CollectionValidator(private val validatorFactory: LocalValidatorFactoryBean) : Validator {
override fun supports(clazz: Class<*>): Boolean {
return Collection::class.java.isAssignableFrom(clazz)
}
/**
* 校验集合 遇到失败即退出
*
* @param target 受校验对象
* @param errors 错误结果
*/
override fun validate(target: Any, errors: Errors) {
val collection = target as Collection<*>
for (`object` in collection) {
`object`?.let { ValidationUtils.invokeValidator(validatorFactory, `object`, errors) }
// 存在错误即退出校验
if (errors.hasErrors()) {
break
}
}
}
} - 控制器拦截
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class CollectionValidatorAdvice(private val collectionValidator: CollectionValidator) {
/**
* 在initBinder阶段修改集合类型的校验器
*/
fun initBinder(binder: WebDataBinder) {
// 这里必须判断 否则会影响非集合类型校验
if (binder.target !is Collection<*>) {
return
}
binder.addValidators(collectionValidator)
}
} - 配置类
1
2
3
4
5
6
7
8
9
10
class CollectionValidatorConfig {
lateinit var localValidatorFactoryBean: LocalValidatorFactoryBean
fun collectionValidator(): CollectionValidator {
return CollectionValidator(localValidatorFactoryBean)
}
} - 修改测试类,导入配置文件
1
2
3
4
class WebMvcTest - 再次运行testDemoList此时此方法将受校验
- 自定义验证器
同样由于未做异常拦截,以上方案,验证器将抛出异常
方案3允许使用分组,对参数不需要做变更,而其缺陷在于如果想要知道是集合的哪条数据出现问题相对而言不太容易