在应用程序中,为了确保程序运行正确,通常需要对用户输入进行校验。Java中为了对Bean进行校验先后发布了JSR 303、JSR 349、JSR 380,帮助Java开发者快速校验Java Bean。作为Java应用开发中的主流框架Spring,也大量使用了Java Bean Validation。本文先简单介绍Java Bean Validation的发展历史,然后介绍在Spring中如何应用,最后介绍Spring中Bean Validation的实现原理。
Java Bean Validation发展历史 Emmanuel Bernard在2009年发布了JSR 303(Bean Validation 1.0),在该标准中定义了如何使用注解对JavaBean进行校验;Emmanuel于2013年又发布了JSR 349(Bean Validation 1.1),在该版本中引入了对方法参数校验、校验器依赖注入等特性的支持;在Java8发布后,Gunnar Morling于201年又发布了JSR 380(Bean Validation 2.0),该版本中大量使用了Java8的新特性,如Optional、Lambda表达式、Type Annotation等。
Hibernate Validator是JSR 380的一个参考实现,被业界广泛使用,能够在展示层、业务层、数据层对输入进行校验。
Spring中应用Java Bean Validation 在Spring也有对Java Bean Validation的封装,maven组件为:
1
2
3
4
<dependency>
<groupId> org.springframework.boot</groupId>
<artifactId> spring-boot-starter-validation</artifactId>
</dependency>
Spring Controller参数校验 在使用Spring进行Web开发时,接收参数的方式有以下两种:
对于HTTP GET等请求,通常使用@RequestParam、@PathVariable注解接收请求参数及URL中的参数; 对于HTTP POST请求,通常是用@RequestBody注解接受请求体。 下面看下如何对GET、POST请求进行参数校验。
首先定义接受请求参数的类,并对类中的参数增加校验项:
1
2
3
4
5
6
7
8
9
10
11
12
@Data
@Builder
public class UserVO {
private Long id ;
@NotBlank ( message = "名称不能为空" )
private String name ;
@Email ( message = "邮箱格式错误" )
private String email ;
}
GET请求中,如果参数数量较少,可以使用简单类型并使用@RequestParam注解接收参数,然后给需要校验的参数加校验注解;如果参数数量过多,可以将参数聚合到一个类中,然后给该类的参数添加@Valid注解。无论是使用简单类型还是类来接收参数,都必须 在Contoller类上加@Validated注解。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Slf4j
@Validated
@RestController
public class UserQueryController {
@GetMapping ( "/user/query/id" )
public UserVO queryUserById ( @RequestParam @Min ( 5 ) int id ) {
log . info ( "[queryUserById] {}" , id );
return null ;
}
@GetMapping ( "/user/query/full" )
public UserVO queryUser ( @Valid UserVO userVo ) throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper ();
log . info ( "[queryUser] {}" , objectMapper . writeValueAsString ( userVo ));
return null ;
}
}
对于POST请求,后端通常用@RequestBody并使用一个类来接受请求体的内容,在类中添加校验项后,只需要在参数上加@Valid或@Validated注解即可。
1
2
3
4
5
6
7
8
9
10
@Slf4j
@RestController
public class UserOperateController {
@PostMapping ( "/user/add" )
public UserVO addUser ( @Valid @RequestBody UserVO userVO ) {
log . info ( "add user" );
return null ;
}
}
统一处理校验错误 当参数校验失败时,Spring会抛出异常,在不同场景下抛出的异常有所不同,具体区别如下:
HTTP请求 参数注解 抛出异常类型 HTTP 返回状态码 GET 无注解 BindException 400 GET RequestParam MethodArgumentNotValidException 500 GET PathVariable MethodArgumentNotValidException 500 POST RequestBody ConstraintViolationException 400
如果想要返回统一的结构或针对校验失败返回统一的状态码,有两种方法。
如果只针对某一个Controller设置,则可以在该Controller中新增一个方法,处理抛出的异常,并返回400:
1
2
3
4
5
@ExceptionHandler ( ConstraintViolationException . class )
@ResponseStatus ( HttpStatus . BAD_REQUEST )
ResponseEntity < String > handleConstraintViolationException ( ConstraintViolationException e ) {
return new ResponseEntity <> ( "参数校验失败:" + e . getMessage (), HttpStatus . BAD_REQUEST );
}
如果想针对全局配置,则可以添加一个全局处理类,用@ControllerAdvice注解。下面的两个方法分别处理ConstraintViolationException和MethodArgumentNotValidException,并返回了一个统一的结构HttpResponse。
1
2
3
4
5
6
public class HttpResponse < T > implements Serializable {
private static final long serialVersionUID = - 5831412260861490028L ;
private int status ;
private String msg ;
private T data ;
}
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
@ControllerAdvice
public class ValidateControllerAdvice {
@ExceptionHandler ( ConstraintViolationException . class )
@ResponseStatus ( HttpStatus . BAD_REQUEST )
@ResponseBody
HttpResponse <?> onConstraintValidationException ( ConstraintViolationException e ) {
// 获取错误
String errorMsg = e . getConstraintViolations (). stream (). map ( ConstraintViolation :: getMessage ). collect ( Collectors . joining ( " " ));
// 构造统一返回结果
HttpResponse <?> response = new HttpResponse <> ();
response . setStatus ( 500 );
response . setData ( null );
response . setMsg ( errorMsg );
return response ;
}
@ResponseBody
@ResponseStatus ( HttpStatus . BAD_REQUEST )
@ExceptionHandler ( MethodArgumentNotValidException . class )
HttpResponse <?> onConstraintViolationException ( MethodArgumentNotValidException e ) {
// 获取错误
String errorMsg = e . getBindingResult (). getFieldErrors (). stream ()
. map ( DefaultMessageSourceResolvable :: getDefaultMessage ). collect ( Collectors . joining ( " " ));
// 构造统一返回结果
HttpResponse <?> response = new HttpResponse <> ();
response . setStatus ( 500 );
response . setData ( null );
response . setMsg ( errorMsg );
return response ;
}
}
Controller中处理方法优先级比全局的处理类高。
自定义校验器 如果javax.validation.constraints中的校验器不满足校验要求,还可以自定义一个校验注解和校验器。
例如我们给UserVO增加一个身份证属性并对其进行校验。首先定义校验注解,定义的注解中必须包含以下内容:
message,当校验不通过时的提示信息,默认的信息从ValidationMessages.properties指定 groups,分组校验的组别 payload,用处较少 Constraint注解,指定校验器 1
2
3
4
5
6
7
8
9
@Target ({ ElementType . FIELD })
@Retention ( RetentionPolicy . RUNTIME )
@Constraint ( validatedBy = IdCardNoValidator . class )
@Documented
public @interface IdCardNo {
String message () default "{IdCardNo.invalid}" ;
Class <?>[] groups () default {};
Class <? extends Payload >[] payload () default {};
}
对应的校验类必须实现ConstraintValidator接口,接口的第一个范型为实际要校验的注解,第二个为校验的类型。
在isValid方法中进行实际的校验,第一个方法参数是要校验的对象,第二个是校验的上下文信息,可以使用该对象自定义错误信息(详见:Validator校验器中重新定义默认的错误信息模板 )。
1
2
3
4
5
6
7
8
9
10
public class IdCardNoValidator implements ConstraintValidator < IdCardNo , String > {
@Override
public boolean isValid ( String value , ConstraintValidatorContext context ) {
return validateCard ( value );
}
private boolean validateCard ( String value ) {
// 略
}
}
具体使用:
1
2
3
4
5
6
7
8
@Data
@Builder
public class UserVO {
// ...
@IdCardNo ( message = "身份证号格式错误" )
private String idCardNo ;
}
Service层进行校验 除了在Controller层对用户输入校验,还可以在Serivce层进行校验。校验方法是在接口方法及实现方法中给需校验的参数加@Valid注解,在实现类上加@Validated注解。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public interface UserOperateService {
void addUser ( @Valid UserVO userVO );
}
@Slf4j
@Service
@Validated
public class UserOperateServiceImpl implements UserOperateService {
@Override
public void addUser ( @Valid UserVO userVO ) {
log . info ( "[addUser] userVO: {}" , JsonUtil . object2Json ( userVO ));
}
}
显式校验 在没有Spring框架的情况下,如果也需要对参数进行校验,可以直接调用Hiberate的方法对参数进行校验。
1
2
3
4
5
6
7
8
9
10
class ProgrammaticallyValidatingService {
void validateInput ( Input input ) {
ValidatorFactory factory = Validation . buildDefaultValidatorFactory ();
Validator validator = factory . getValidator ();
Set < ConstraintViolation < Input >> violations = validator . validate ( input );
if ( ! violations . isEmpty ()) {
throw new ConstraintViolationException ( violations );
}
}
}
这种方式会在每次校验的时候创建一个ValidatorFactory和Validator,而参数校验的时候会获取校验类的所有远数据并缓存起来,提高下次校验时的校验速度。但是每次重新创建,该缓存会失效,所以对于频繁调用的方法,不建议使用这种方式进行校验。
而ValidatorFactory和Validator都是线程安全的,因此一个Application中只用一个ValidatorFactory单例即可。
Spring参数校验源码分析 校验类加载 Spring Boot中加载校验类的配置类为ValidationAutoConfiguration,该类为容器中提供了两个类:
LocalValidatorFactoryBean,当容器中不存在Validator时,作为默认的校验器。 MethodValidationPostProcessor,是一个BeanPostProcessor,在应用中Bean初始化完成后,会判断Bean是否使用了@Validated注解,如果使用了则给该Bean的方法加上校验的切面。 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
@AutoConfiguration
@ConditionalOnClass ( ExecutableValidator . class )
@ConditionalOnResource ( resources = "classpath:META-INF/services/javax.validation.spi.ValidationProvider" )
@Import ( PrimaryDefaultValidatorPostProcessor . class )
public class ValidationAutoConfiguration {
@Bean
@Role ( BeanDefinition . ROLE_INFRASTRUCTURE )
@ConditionalOnMissingBean ( Validator . class )
public static LocalValidatorFactoryBean defaultValidator ( ApplicationContext applicationContext ) {
LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean ();
MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory ( applicationContext );
factoryBean . setMessageInterpolator ( interpolatorFactory . getObject ());
return factoryBean ;
}
@Bean
@ConditionalOnMissingBean ( search = SearchStrategy . CURRENT )
public static MethodValidationPostProcessor methodValidationPostProcessor ( Environment environment ,
@Lazy Validator validator , ObjectProvider < MethodValidationExcludeFilter > excludeFilters ) {
FilteredMethodValidationPostProcessor processor = new FilteredMethodValidationPostProcessor (
excludeFilters . orderedStream ());
boolean proxyTargetClass = environment . getProperty ( "spring.aop.proxy-target-class" , Boolean . class , true );
processor . setProxyTargetClass ( proxyTargetClass );
processor . setValidator ( validator );
return processor ;
}
}
校验切面 在ValidationAutoConfiguration中提供的MethodValidationPostProcessor的继承关系为:
在MethodValidationPostProcessor中加载了一个DefaultPointcutAdvisor,包含的pointcut为AnnotationMatchingPointcut,关注的是有Validated注解的类;包含的advise为MethodValidationInterceptor,进行实际的参数校验。
MethodValidationInterceptor的校验部分:
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
public Object invoke ( MethodInvocation invocation ) throws Throwable {
// Avoid Validator invocation on FactoryBean.getObjectType/isSingleton
if ( isFactoryBeanMetadataMethod ( invocation . getMethod ())) {
return invocation . proceed ();
}
Class <?>[] groups = determineValidationGroups ( invocation );
// Standard Bean Validation 1.1 API
ExecutableValidator execVal = this . validator . forExecutables ();
Method methodToValidate = invocation . getMethod ();
Set < ConstraintViolation < Object >> result ;
Object target = invocation . getThis ();
Assert . state ( target != null , "Target must not be null" );
// 执行方法前检验参数
try {
result = execVal . validateParameters ( target , methodToValidate , invocation . getArguments (), groups );
}
catch ( IllegalArgumentException ex ) {
// Probably a generic type mismatch between interface and impl as reported in SPR-12237 / HV-1011
// Let's try to find the bridged method on the implementation class...
methodToValidate = BridgeMethodResolver . findBridgedMethod (
ClassUtils . getMostSpecificMethod ( invocation . getMethod (), target . getClass ()));
result = execVal . validateParameters ( target , methodToValidate , invocation . getArguments (), groups );
}
if ( ! result . isEmpty ()) {
// 如果有校验不通过,则抛出ConstraintViolationException
throw new ConstraintViolationException ( result );
}
Object returnValue = invocation . proceed ();
// 调用结束后校验返回值
result = execVal . validateReturnValue ( target , methodToValidate , returnValue , groups );
if ( ! result . isEmpty ()) {
// 如果有校验不通过,则抛出ConstraintViolationException
throw new ConstraintViolationException ( result );
}
return returnValue ;
}
POST参数校验 Spring处理Http请求时首先根据请求URL定位到要实际调用的方法,然后根据要调用的方法参数确定请求参数的解析类。
对于用RequestBody注解的参数,使用解析类为RequestResponseBodyMethodProcessor,在对参数解析完成后,会对参数进行校验。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Override
public Object resolveArgument ( MethodParameter parameter , @Nullable ModelAndViewContainer mavContainer ,
NativeWebRequest webRequest , @Nullable WebDataBinderFactory binderFactory ) throws Exception {
parameter = parameter . nestedIfOptional ();
Object arg = readWithMessageConverters ( webRequest , parameter , parameter . getNestedGenericParameterType ());
String name = Conventions . getVariableNameForParameter ( parameter );
if ( binderFactory != null ) {
WebDataBinder binder = binderFactory . createBinder ( webRequest , arg , name );
if ( arg != null ) {
// 校验参数
validateIfApplicable ( binder , parameter );
if ( binder . getBindingResult (). hasErrors () && isBindExceptionRequired ( binder , parameter )) {
throw new MethodArgumentNotValidException ( parameter , binder . getBindingResult ());
}
}
if ( mavContainer != null ) {
mavContainer . addAttribute ( BindingResult . MODEL_KEY_PREFIX + name , binder . getBindingResult ());
}
}
return adaptArgumentIfNecessary ( arg , parameter );
}
GET参数校验 对于GET参数的校验则不是在校验类中实现的,而是利用了Spring的AOP机制。所以对于GET请求的方法,其Controller类必须加@Validated注解。
具体代码见校验切面,这里不再赘述。
总结 Spring中可以使用Hibernate Bean Validation对POST、GET等请求方法的入参做校验,也可以对业务逻辑层做校验。
Spring中的校验可以是JSR 380定义的校验器,也可以自定义校验器。
对于POST方法,只需对参数加@Valid注解即可,GET方法则须对Controller类加@Validated注解。
Spring中,POST方法进行校验是在参数解析时直接调用Hibernate的校验器校验;对GET方法的校验则是使用AOP实现。
参考文献 The Java Community Process(SM) Program - JSRs: Java Specification Requests - summary Jakarta Bean Validation - News The Bean Validation reference implementation. - Hibernate Validator Java Bean Validation Basics java - Spring Validation最佳实践及其实现原理,参数校验没那么简单! - 个人文章 - SegmentFault 思否 Spring Method Parameter Hibernate Validator 8.0.0.Final - Jakarta Bean Validation Reference Implementation: Reference Guide Author
Gum
LastMod
2023-01-30
License
CC BY-NC-ND 4.0