Spring Boot RestController如何无需@Valid注解实现所有参数校验
核心原因
类级别标注@Validated仅能开启方法简单参数的直接校验能力:也就是直接在@PathVariable、@RequestParam参数上标注@Min、@NotBlank这类约束注解时无需额外配置即可生效。但该注解不会自动触发@RequestBody、@ModelAttribute这类复杂封装对象的级联校验,Spring默认要求这类参数必须在定义处标注@Valid或@Validated,才会递归校验对象内部字段的约束,这也是手动在参数上加@Valid后校验正常的原因。
无侵入自动校验实现方案
以下两种方案均基于Spring原生能力实现,无需额外引入第三方依赖,配置完成后所有控制器参数无需逐一枚举@Valid注解即可自动触发校验。
方案1:自定义请求参数解析逻辑(稳定性最高)
直接修改Spring MVC核心请求适配器的参数解析逻辑,给所有复杂类型参数自动追加校验流程,和原生@Valid的校验逻辑完全一致。
首先编写自动配置类,替换默认的参数解析器:
import org.springframework.context.annotation.Configuration import org.springframework.validation.Validator import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter import javax.annotation.PostConstruct @Configuration class AutoValidationConfig( private val requestMappingHandlerAdapter: RequestMappingHandlerAdapter, private val validator: Validator ) { @PostConstruct fun init() { val argumentResolvers = requestMappingHandlerAdapter.argumentResolvers val wrappedResolvers = argumentResolvers.map { resolver -> when (resolver) { is RequestResponseBodyMethodProcessor -> AutoValidatingRequestResponseBodyMethodProcessor(resolver, validator) is ModelAttributeMethodProcessor -> AutoValidatingModelAttributeMethodProcessor(resolver, validator) else -> resolver } } requestMappingHandlerAdapter.argumentResolvers = wrappedResolvers } }
实现@RequestBody参数的自动校验包装类:
import org.springframework.core.MethodParameter import org.springframework.http.converter.HttpMessageConverter import org.springframework.validation.Validator import org.springframework.web.bind.MethodArgumentNotValidException import org.springframework.web.bind.WebDataBinder import org.springframework.web.bind.support.WebDataBinderFactory import org.springframework.web.context.request.NativeWebRequest import org.springframework.web.method.support.ModelAndViewContainer import org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor class AutoValidatingRequestResponseBodyMethodProcessor( private val delegate: RequestResponseBodyMethodProcessor, private val validator: Validator ) : RequestResponseBodyMethodProcessor(emptyList<HttpMessageConverter<*>>()) { override fun supportsParameter(parameter: MethodParameter): Boolean { return delegate.supportsParameter(parameter) } override fun supportsReturnType(returnType: MethodParameter): Boolean { return delegate.supportsReturnType(returnType) } override fun handleReturnValue( returnValue: Any?, returnType: MethodParameter, mavContainer: ModelAndViewContainer, webRequest: NativeWebRequest ) { delegate.handleReturnValue(returnValue, returnType, mavContainer, webRequest) } override fun resolveArgument( parameter: MethodParameter, mavContainer: ModelAndViewContainer?, webRequest: NativeWebRequest, binderFactory: WebDataBinderFactory? ): Any? { val argument = delegate.resolveArgument(parameter, mavContainer, webRequest, binderFactory) argument?.let { val binder = binderFactory!!.createBinder(webRequest, it, parameter.parameterName) binder.validator = validator binder.validate(it) if (binder.bindingResult.hasErrors()) { throw MethodArgumentNotValidException(parameter, binder.bindingResult) } } return argument } }
实现@ModelAttribute参数的自动校验包装类:
import org.springframework.core.MethodParameter import org.springframework.validation.BindException import org.springframework.validation.Validator import org.springframework.web.bind.WebDataBinder import org.springframework.web.bind.support.WebDataBinderFactory import org.springframework.web.context.request.NativeWebRequest import org.springframework.web.method.support.ModelAndViewContainer import org.springframework.web.servlet.mvc.method.annotation.ModelAttributeMethodProcessor class AutoValidatingModelAttributeMethodProcessor( private val delegate: ModelAttributeMethodProcessor, private val validator: Validator ) : ModelAttributeMethodProcessor(false) { override fun supportsParameter(parameter: MethodParameter): Boolean { return delegate.supportsParameter(parameter) } override fun resolveArgument( parameter: MethodParameter, mavContainer: ModelAndViewContainer?, webRequest: NativeWebRequest, binderFactory: WebDataBinderFactory? ): Any? { val argument = delegate.resolveArgument(parameter, mavContainer, webRequest, binderFactory) argument?.let { val binder = binderFactory!!.createBinder(webRequest, it, parameter.parameterName) binder.validator = validator binder.validate(it) if (binder.bindingResult.hasErrors()) { throw BindException(binder.bindingResult) } } return argument } }
方案2:AOP切面拦截校验(实现成本最低)
如果不想修改Spring MVC内置组件逻辑,可引入AOP依赖实现全局拦截校验,仅需一个切面类即可完成配置:
import org.aspectj.lang.JoinPoint import org.aspectj.lang.annotation.Aspect import org.aspectj.lang.annotation.Before import org.aspectj.lang.reflect.MethodSignature import org.springframework.stereotype.Component import org.springframework.validation.BeanPropertyBindingResult import org.springframework.validation.Validator import org.springframework.web.bind.MethodArgumentNotValidException import java.util.* import javax.validation.Valid import javax.validation.Validated @Aspect @Component class AutoValidAspect( private val validator: Validator ) { // 拦截所有@RestController下的接口方法 @Before("@within(org.springframework.web.bind.annotation.RestController)") fun beforeControllerMethod(joinPoint: JoinPoint) { val signature = joinPoint.signature as MethodSignature val method = signature.method val args = joinPoint.args val parameters = method.parameters for (i in args.indices) { val arg = args[i] ?: continue val param = parameters[i] // 跳过简单值类型,避免无意义校验 if (isSimpleValueType(arg.javaClass)) continue // 手动加了@Valid/@Validated的参数跳过,避免重复校验 if (param.isAnnotationPresent(Valid::class.java) || param.isAnnotationPresent(Validated::class.java)) continue val bindingResult = BeanPropertyBindingResult(arg, param.name) validator.validate(arg, bindingResult) if (bindingResult.hasErrors()) { throw MethodArgumentNotValidException(MethodParameter(method, i), bindingResult) } } } private fun isSimpleValueType(clazz: Class<*>): Boolean { return (Void::class.java.isAssignableFrom(clazz) || clazz.isPrimitive || CharSequence::class.java.isAssignableFrom(clazz) || Number::class.java.isAssignableFrom(clazz) || Date::class.java.isAssignableFrom(clazz) || clazz.isEnum || UUID::class.java.isAssignableFrom(clazz) || java.time.temporal.Temporal::class.java.isAssignableFrom(clazz)) } }
配置注意事项
- 上述方案生效后,仅需在控制器类上保留
@Validated注解,即可同时支持简单参数的直接约束校验、复杂对象的内部字段递归校验,无需在参数上重复添加@Valid。 - Spring Boot 2.3+版本需要手动引入
spring-boot-starter-validation依赖,否则校验模块不会自动装配。 - 需要配套全局异常处理器,捕获
MethodArgumentNotValidException、ConstraintViolationException、BindException三类校验异常,返回统一的业务错误格式。 - AOP方案中的简单类型判断列表可根据项目实际使用的类型扩展,比如新增项目自定义的通用值类型到跳过列表中。
内容的提问来源于stack exchange,提问作者Markus Rohlof
相关产品推荐
相关产品推荐

