@valid 校验list_springboot学习(7)处理集合类型校验

本文介绍了在SpringBoot中使用@Valid进行集合类型参数校验的几种解决方案,包括创建包装类、实现接口委托以及自定义验证器。通过案例展示了如何处理无法对List参数进行校验的问题,并探讨了各种方案的优缺点。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本文所用相关版本:

springboot版本2.1.6.RELEASE
kotlin版本1.2.71
gradle版本5.2.1
idea版本2019.1.2 ultimate edition

新建项目

  • 点击file -> new project ->选择spring initializrd点击下一步
  • 选择语言,选择项目管理工具

7f7549f9c7ed36468d30b5edf951168e.png
  • 此篇我们只涉及校验,只引入springWebStarter即可
  • 选择gradle路径(或者使用默认的),这里我选择本地路径

47430c0a26a5a1186b2dba2b2bbede90.png
  • 输入项目路径,点击finish,查看项目结构,我们可以看到生成的gradle依赖文件变成了kts后缀的文件,和之前比起来,配置会略有不同
  • 增加国内镜像地址
    • 追加根节点
repositories {
    maven (url = "http://maven.aliyun.com/nexus/content/groups/public/")
    jcenter()
} 
  • 刷新依赖等待编译完成

重现无法校验场景

  • 增加测试控制器和测试类,这里我直接加在了启动类上
@SpringBootApplication
@RestController
class DemoApplication {
    @PostMapping("/demo")
    fun demo(@RequestBody @Validated user: User): User {
        return user
    }

    @PostMapping("/demoList")
    fun demoList(@RequestBody @Validated user: List<User>): List<User> {
        return user
    }
}

class User {
    @NotBlank(message = "用户名不能为空")
    val username: String? = null
    val password: String? = null
}

fun main(args: Array<String>) {
    runApplication<DemoApplication>(*args)
}
  • 增加测试类
@RunWith(SpringRunner::class)
@WebMvcTest(DemoApplication::class)
class WebMvcTest {
    @Autowired
    lateinit var mockMvc: MockMvc

    @Test
    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()
    }

    @Test
    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,执行后发现抛出异常,说明参数被校验了

61ec59beae3abbcd969134aa34246b57.png
  • 但执行List时参数却未被校验

0d990a97dcf3455dc1ce28f6895fd239.png

处理方案

方案1:

新建类包装List,在list上加上@Valid注解(javax包中)(由于@Validated不支持放在字段上,所以无法使用)

  • 控制器
@PostMapping("/demoValidList")
fun demoValidList(@RequestBody @Validated user: ValidList<User>): List<User>? {
    return user.list
}
  • 包装类
class ValidList<T> {
    @Valid
    val list: List<T>? = null
}
  • 测试方法
@Test
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()
}

可以看到此方案需要我们将传输的参数变为对象,多了一层无用的嵌套,并且由于@Valid注解的缺陷,无法使用分组

方案2:

我们可以采用实现list接口并转接方法的方式,去掉上一方案无用的嵌套

  • 包装类
class 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)
    }

    @Valid
    val list: MutableList<T> = mutableListOf()
控制器
@PostMapping("/demoValidList1")
fun demoValidList(@RequestBody @Validated user: ValidList1<User>): List<User>? {
    return user.list
}
  • 测试方法
@Test
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是无效的)
class ValidList2<T>(val list: MutableList<T>) : MutableList<T> by list {
    @Valid
    var mlist: MutableList<T> = list
    constructor() : this(mutableListOf())
}
  • 控制器
@PostMapping("/demoValidList2")
fun demoValidList2(@RequestBody @Validated user: ValidList2<User>): List<User>? {
    return user.list
}
  • 测试方法
@Test
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替我们节省部分代码

@Data
public class ValidList<T> implements List<T>{
    @Delegate
    List<T> list = new ArrayList<>();
}

由于未做异常拦截,以上方案 正常校验,验证器将抛出异常

2ee7b36491ee6f81fc012e0ac5b46a56.png

方案1和方案2本质上是一致的,缺点在于对于控制器代码的侵入性较大(意味着所有需要校验list的控制器方法都需要修改类为新的包装类)

方案3:

我们可以自定义验证器并配置@ControllerAdvice统一为集合增加验证器

  • 自定义验证器
class 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
            }
        }
    }
}
  • 控制器拦截
@ControllerAdvice
class CollectionValidatorAdvice(private val collectionValidator: CollectionValidator) {
    /**
     * 在initBinder阶段修改集合类型的校验器
     */
    @InitBinder
    fun initBinder(binder: WebDataBinder) {
        // 这里必须判断 否则会影响非集合类型校验
        if (binder.target !is Collection<*>) {
            return
        }
        binder.addValidators(collectionValidator)
    }
}
  • 配置类
@Configuration
class CollectionValidatorConfig {
    @Autowired
    lateinit var localValidatorFactoryBean: LocalValidatorFactoryBean

    @Bean
    fun collectionValidator(): CollectionValidator {
        return CollectionValidator(localValidatorFactoryBean)
    }
}
  • 修改测试类,导入配置文件
@RunWith(SpringRunner::class)
@WebMvcTest(DemoApplication::class)
@Import(CollectionValidatorConfig::class)
class WebMvcTest
  • 再次运行testDemoList此时此方法将受校验

同样由于未做异常拦截,以上方案,验证器将抛出异常

88be7e2e5a600778a27020579ce215f4.png

方案3允许使用分组,对参数不需要做变更,而其缺陷在于如果想要知道是集合的哪条数据出现问题相对而言不太容易


代码链接

  • 以上即是本文全部内容

d229cd8a3f1af3d9f744ca9e759612f3.png
  • 本文代码链接
kotlin-springboot-collection-validation​github.com
eb1b764322eb150ab2348cbe8c794ecf.png
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值