springMVC请求参数校验

2021-03-21  本文已影响0人  水煮鱼又失败了

下下下周,争取做只水煮鱼~~~
算了吧,买现成的调料吧~~~


fish.png

1 场景

JavaWeb后台应用程序,具体的执行方法,收到请求,需要对请求的数据进行基础校验,如字符串长度限制、正则校验、数字区间校验等。

推荐在springMVC中对前台的请求参数进行统一校验,校验方式建议采用JSR30标准进行校验。

1.1 普通校验方式

最简单的校验方式是,对请求的参数手动一个个进行校验,如下代码:

@GetMapping("saveWithOld")
public JSONObject saveWithOld(User user) {
    JSONObject result = new JSONObject();
    if (user.getUserCode() == null || user.getUserCode() == "") {
        result.put("success", true);
        result.put("message", "用户代码不可为空");
        return result;
    }
    if (user.getUserName() == null || user.getUserName() == "") {
        result.put("success", true);
        result.put("message", "用户名称不可为空");
        return result;
    }
    // do something ......

    result.put("success", true);
    return result;
}

这种方式,代码量非常大代码非常不友好

1.2 springMVC校验方式

springMVC,在执行后台方法之前,可以对请求的数据通过注解进行校验。此校验方式基于JSR303规范

如下代码所示:

@Data
public class User {
    @NotNull(message = "用户代码不可为空")
    private String userCode;
}
@GetMapping("saveWithNormal")
    public JSONObject saveWithNormal(@Valid User user) {
        JSONObject result = new JSONObject();
        result.put("success", true);
        result.put("message", user.toString());
        return result;
    }
@PostMapping("saveWithRequestParam")
public JSONObject saveWithRequestParam(@NotNull(message = "用户代码不可为空") String userCode) {
    JSONObject result = new JSONObject();
    result.put("success", true);
    result.put("message", userCode);
    return result;
}

此种方式,可以使用注解,已更简单的方式对请求参数进行校验。

3 版本说明

本文中代码涉及到的相关版本如下:

3.1 JDK

JDK1.8

3.2 maven依赖

spring-boot-starter-web中已包含了我们需要的依赖。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>2.2.9.RELEASE</version>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.7 </version>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.18</version>
    <scope>provided</scope>
</dependency>

2 名词关系说明

这里讲下springMVC中使用JSR303进行参数校验,相关的名词含义及名词之间的关系说明

2.1 基本说明

springMVC基于JSR303规范进行校验。

官网说明:https://jcp.org/en/jsr/detail?id=303

规范的相关说明如下:

JSR是Java Specification Requests的缩写,意思是Java 规范提案 。
JSR-303 是JAVA EE 6 中的一项子规范,叫做Bean Validation
Hibernate Validator是 Bean Validation的参考实现
Hibernate Validator提供了JSR 303 规范中所有内置 constrain(约束)的实现,除此之外还有一些附加的constraint(约束)

2.2 详细说明

关于springMVC请求参数校验,涉及几个对应的名词,如下是:

名词 说明
constraint(约束) 对参数的校验约束注解,如@NotNull表示参数不可以为Null
校验注解 为元素加上约束后,有时候需要在参数前加上校验注解来开启验证。
相关注解有@Valid@Validated,如没有用到注解独有的特性(分组、嵌套)等,用哪个注解都一样。

需注意不是所有的校验都需要开启校验,如下不需要加上校验注解:
saveWithRequestParam(@NotNull(message = "用户代码不可为空") String userCode)
但是需要在Controller类上加上注解@Valid或@Validated
JSR303规范 行业规范标准,包括校验的constraint(约束,如@NotNull)开启校验注解@Valid
体现:代码中体现为注解、接口无具体实现代码
jar包:jakarta.validation-api-2.0.2.jar
约束注解:javax.validation.constraints包下注解+hibernate增强注解org.hibernate.validator.constraints
校验注解:javax.validation.Valid
Hibernate Validator Hibernate对JSR303规范中的约束constraint具体代码实现
jar包:hibernate-validator-6.0.20.Final.jar
增强:在原有JSR303的constraint(约束)增加了约束(如@Range)
spring JSR303 spring对JSR303的包装,对原有的校验进行了增强
增强:分组校验顺序校验
缺点:不支持嵌套校验
约束注解:javax.validation.constraints包下注解+hibernate增强注解org.hibernate.validator.constraints
校验注解:org.springframework.validation.annotation.Validated
增强说明:所谓的包装和增强,只是将@Valid注解扩展为@Validated注解。约束注解和JSR303一样。

2.3 关系图

一图胜千言。参数校验的相关说明,关系图如下:

spring参数校验.jpg

3 校验流程

3.1 对象参数

3.1.1 说明

对象参数中进行约束校验。需满足以下条件:

(1)在mapping方法中通过注解@Valid或@Validated指定要校验的参数对象

如下:

@GetMapping("saveWithNormal")
public JSONObject saveWithNormal(@Valid User user) {......}

(2)在对象参数对应的类中,对需要校验的参数加上约束注解

如下:

@Data
public class User {
    /**
     * 用户代码
     */
    @NotNull(message = "用户代码不可为空")
    private String userCode;
}
3.1.2 校验流程

校验失败后,需要对失败的异常信息进行处理,处理方式有两种:

1、在mapping方法上加上参数BindingResult bindingResult

此种方式,校验失败后,会将异常信息封装到参数对象bindingResult中,可以自行对其中的异常信息进行处理,封装错位信息,返回请求结果

这种情况,需要每个请求,都对参数BindingResult进行处理,较为繁琐,不建议此种方式。

如下:

@GetMapping("saveWithBind")
public JSONObject saveWithBind(@Valid User user, BindingResult bindingResult) {
    // --------------------[手动检测验证是否通过]--------------------
    if (bindingResult.hasErrors()) {
        for (FieldError fieldError : bindingResult.getFieldErrors()) {
            JSONObject result = new JSONObject();
            result.put("success", false);
            result.put("message", fieldError.getDefaultMessage());
            return result;
        }
    }
    // --------------------[验证检测通过后执行其他操作]--------------------
    // ......
    JSONObject result = new JSONObject();
    result.put("success", true);
    result.put("message", user.toString());
    return result;
}

2、定义spring全局异常处理,捕捉对应的异常信息,进行统一处理

mapping方法上不加参数BindingResult bindingResult,校验失败后,会抛出异常,异常信息,通过spring全局异常管理,统一对抛出的异常信息进行处理,处理后统一封装错位信息。这种方式,代码量较少,且处理错误信息集中,推荐此种方式。

如下代码:

@Data
public class User {
    /**
     * 用户代码
     */
    @NotNull(message = "用户代码不可为空")
    private String userCode;
}
// 参数校验失败,抛出异常:org.springframework.validation.BindException
@GetMapping("saveWithNormal")
public JSONObject saveWithNormal(@Valid User user) {
    JSONObject result = new JSONObject();
    result.put("success", true);
    result.put("message", user.toString());
    return result;
}
/**
   * 捕捉全局异常:org.springframework.validation.BindException
   * <div>普通请求的参数,校验失败,抛出此异常</div>
   * <div>如:(@Valid User user)</div>
   *
   * @param exception
   * @return
   */
@ExceptionHandler(BindException.class)
public JSONObject handlerBindException(BindException exception) {
    log.info("全局异常[BindException]:" + exception.getMessage());
    JSONObject result = new JSONObject();
    result.put("success", false);
    if (exception != null) {
        String message = exception.getBindingResult().getFieldErrors().stream().filter(e -> e != null).map(FieldError::getDefaultMessage).collect(Collectors.joining(","));
        result.put("message", message);
    }
    return result;
}

需注意:参数上@Valid和@Validated使用方式的不同,校验失败后,会抛出不同的异常

如下:

/**
  * 捕捉全局异常:org.springframework.validation.BindException
  * <div>普通请求的参数,校验失败,抛出此异常</div>
  * <div>如:(@Valid User user)</div>
  * @param exception
  * @return
  */
@ExceptionHandler(BindException.class)
public JSONObject handlerBindException(BindException exception) {......}
/**
  * 捕捉全局异常:org.springframework.web.bind.MethodArgumentNotValidException
  * <div>@RequestBody修饰的参数,校验失败,抛出此异常</div>
  * <div>如:(@RequestBody @Valid User user)</div>
  * @param exception
  * @return
  */
@ExceptionHandler({MethodArgumentNotValidException.class})
public JSONObject handleMethodArgumentNotValidException(MethodArgumentNotValidException exception) {......}

总结校验流程图如下:

对象参数校验流程.jpg

3.2 普通类型参数

3.2.1 说明

普通类型参数中进行约束校验。需满足以下条件:

(1)在Controller类上通过注解@Validated开启校验

注意:需为@Validated注解,而不是@Valid注解

如下:

@Validated
@RestController
@RequestMapping("user")
public class UserController {......}

(2)在mapping方法普通类型参数前面加上约束注解

如下:

// 参数校验失败,抛出异常:javax.validation.ConstraintViolationException
@PostMapping("saveWithRequestParam")
public JSONObject saveWithRequestParam(@NotNull(message="用户代码不可为空") String userCode){......}
// 参数校验失败,抛出异常:org.springframework.web.bind.ConstraintViolationException
@GetMapping("saveWithRestful/{userCode}")
public JSONObject saveWithRest(@PathVariable("userCode") @Length(max = 10,message="用户代码不可超过10位") String userCode) {......}
3.2.1 校验流程

校验结果的处理流程同《3.1对象参数》

需注意:如使用全局异常捕捉,校验失败后抛出的异常如下:

 /**
   * 捕捉全局异常:javax.validation.ConstraintViolationException
   * <div>直接在参数上加的校验,校验失败,抛出此异常</div>
   * <div>如:(@NotNull(message = "用户代码不可为空") String userCode)</div>
   *
   * @param exception
   * @return
   */
@ExceptionHandler(ConstraintViolationException.class)
public JSONObject handlerConstraintViolationException(ConstraintViolationException exception) {......}

总结校验流程图如下:

普通参数校验流程.jpg

4 嵌套校验

嵌套校验,准确来说,是对象内约束的嵌套校验。指的是校验A对象,A对象内有个属性B是对象,B对象内部属性仍然有约束。需要对A对象的约束+A对象内B对象的约束进行校验,这种就是嵌套约束。

4.1 代码示例

这里既校验参数user中的userCode约束又需要校验user中的属性对象department中的departmentCode的约束

@Data
public class Department {
    @NotNull(message = "部门代码不可为空")
    private String departmentCode;
}
@Data
public class User {
    @NotNull(message = "用户代码不可为空")
    private String userCode;
    
    @Valid
    @NotNull(message = "部门不可为空")
    private Department department;
    
    private String userName;
}
@GetMapping("saveWithLevel")
public JSONObject saveWithLevel(@Valid User user) {
    JSONObject result = new JSONObject();
    result.put("success", true);
    result.put("message", user.toString());
    return result;
}
@ExceptionHandler(BindException.class)
public JSONObject handlerBindException(BindException exception) {
    log.info("全局异常[BindException]:" + exception.getMessage());
    JSONObject result = new JSONObject();
    result.put("success", false);
    if (exception != null) {
        String message = exception.getBindingResult().getFieldErrors().stream().filter(e -> e != null).map(FieldError::getDefaultMessage).collect(Collectors.joining(","));
        result.put("message", message);
    }
    return result;
}

4.2 代码测试

http://localhost:8080/user/saveWithLevel?department.departmentCode=001

{"success":false,"message":"部门代码长度需在5~10之间,用户代码不可为空"}

可见嵌套校验起作用了,对象user的内部普通属性userCode和内部对象department的自己的约束都起作用了。

4.3 总结

5 分组校验

同一个javaBean,我们加上约束注解后,这个javaBean作为请求参数的对象类型,其中的约束注解,会对参数对象的内容进行校验。

有时候,不同的请求我们会使用相同的javaBean作为对象的参数类型,如新增用户更新用户我们都会使用用户这个JavaBean作为请求参数的封装对象。

5.1 代码示例

比如,我们新增用户,需要设置密码;更新用户,不需要设置密码。

代码如下:

分组接口不需要有实现,仅仅作为一个分组类型

public interface Add {
}
public interface Edit {
}

通过约束中的group参数,来指定对应的分组类型,可以指定多个

@Data
public class User {
    @NotNull(message = "用户代码不可为空", groups = {Add.class, Edit.class})
    private String userCode;
    
    @NotNull(message = "密码不可为空", groups = {Add.class})
    private String password;
}

校验方式,只能指定@Validated,其中的value为这个参数的分组类型,和类中约束注解的groups属性相对性可以指定多个

@GetMapping("groupAdd")
public JSONObject groupAdd(@Validated(Add.class) User user) {
    JSONObject result = new JSONObject();
    result.put("success", true);
    result.put("message", user.toString());
    return result;
}

@GetMapping("groupEdit")
public JSONObject groupEdit(@Validated(Edit.class) User user) {
    JSONObject result = new JSONObject();
    result.put("success", true);
    result.put("message", user.toString());
    return result;
}

同4.1

5.2 代码测试

5.3 总结

(1)分组校验中,定义的分组类型接口,不需要有实现内容,仅仅是作为分组的一个类型存在,不同的业务,可以共用相同的类型。

(2)约束中的分组类型,可以定义多个。

(3)@Validated中的分组类型,也可以指定多个。

(4)校验的时候,根据@Validated中指定分组类型,去找校验对象中的对应有此分组类型的约束,进行校验。

(5)指定分组后,不满足分组的约束(不加分组的约束为默认分组,也是一种分组),不会进行校验

6 顺序校验

如不进行顺序校验配置,校验对象内的属性,校验顺序是随机的。

有时候想先校验比较简单的约束,再校验复杂的,因此需要指定约束的校验顺序。可以结合《7 验证将检测到第一个约束违例时停止》一起使用。

6.1 代码示例

// 分组类型:第一个执行
public interface FirstCheck {
}
// 分组类型:第二个执行
public interface SecondCheck {
}
// 待顺序的分组类型组
@GroupSequence({FirstCheck.class, SecondCheck.class})
public interface UserGroupCheck {
}
@Data
public class User {
    @NotNull(message = "用户代码不可为空", groups = {FirstCheck.class})
    private String userCode;
    
    @NotNull(message = "密码不可为空", groups = {SecondCheck.class})
    private String password;
    
    @NotNull(message = "用户名不可为空")
    private String userName;
}
@GetMapping("orderCheck")
public JSONObject orderCheck(@Validated(UserGroupCheck.class) User user) {
    JSONObject result = new JSONObject();
    result.put("success", true);
    result.put("message", user.toString());
    return result;
}

同4.1

6.2 代码测试

{"success":false,"message":"用户代码不可为空,用户名不可为空"}


或

```json
{"success":false,"message":"用户名不可为空,用户代码不可为空"}

可以看出,当一个分组内有多个约束,约束的校验顺序仍然是随机的

6.3 总结

(1)根据参数中的分组对应的接口中@GroupSequence指定的分组类型的顺序进行加校验

(2)只有当一个分组内的所有约束都校验通过后,才会进入下一个分组进行校验。

(3)顺序校验,指的是@GroupSequence内配置的分组的顺序,当一个分组内有多个约束这个分组内约束校验顺序仍然随机

7 验证将检测到第一个约束违例时停止

默认,有多个约束的情况下,将会对所有参数进行校验,如果存在校验失败的约束,返回的校验结果(BindingResult或对应Exception)中会有所有的参数校验错误信息。即如果多个不满足约束,则返回结果中会有多个失败信息

有时候,我们只需要返回第一个一个校验失败的约束信息就好,校验到一个约束失败后,没有必要再花费代价进行其他约束校验。

springBoot中,参数校验的实现,基于MethodValidationPostProcessor

@Bean
@ConditionalOnMissingBean
public static MethodValidationPostProcessor methodValidationPostProcessor(Environment environment,
                                                                          @Lazy Validator validator) {
    MethodValidationPostProcessor processor = new MethodValidationPostProcessor();
    boolean proxyTargetClass = environment.getProperty("spring.aop.proxy-target-class", Boolean.class, true);
    processor.setProxyTargetClass(proxyTargetClass);
    processor.setValidator(validator);
    return processor;
}

这个postProcessor的校验配置基于spring中的beanValidator,我们创建自己的Validator的bean,配置failFast,即可实现验证将检测到第一个约束违例时停止这个要求。

实现代码如下:

@Bean
public Validator validator() {
    HibernateValidatorConfiguration configuration = Validation.byProvider(HibernateValidator.class).configure();
    //验证将检测到第一个约束违例时停止
    configuration.failFast(true);
    ValidatorFactory validatorFactory = configuration.buildValidatorFactory();
    return validatorFactory.getValidator();
}

或使用更简洁的写法:

@Bean
public Validator validator() {
    ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
        .configure()
        //验证将检测到第一个约束违例时停止
        .failFast(true)
        .buildValidatorFactory();
    return validatorFactory.getValidator();
}

failFastHibernateValidatorConfiguration中的一个属性配置,配置中还有其他配置属性,可以定制我们的校验器

8 自定义校验器

自定义校验器,注意点比较多,不是本文的重点,暂时不进行记录,后续有时间会有专门的文章进行分析。

9 生产环境配置

前面说的都是原理和使用细节,这里记录下生产环境,需要进行哪些全局配置。

9.1 全局异常处理

建议使用全局异常处理,对请求的异常信息进行统一处理。

代码如下:

import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.util.stream.Collectors;

/**
 * 统一异常处理
 */
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
    
    /**
     * 捕捉全局异常:org.springframework.web.bind.MethodArgumentNotValidException
     * <div>@RequestBody修饰的参数,校验失败,抛出此异常</div>
     * <div>如:xxxAction(@RequestBody @Valid User user)</div>
     * @param exception
     * @return
     */
    @ExceptionHandler({MethodArgumentNotValidException.class})
    public JSONObject handleMethodArgumentNotValidException(MethodArgumentNotValidException exception) {
        log.info("全局异常[MethodArgumentNotValidException]:" + exception.getMessage());
        JSONObject result = new JSONObject();
        result.put("success", false);
        if (exception != null) {
            String message = exception.getBindingResult().getFieldErrors().stream().filter(e -> e != null).map(FieldError::getDefaultMessage).collect(Collectors.joining(","));
            result.put("message", message);
        }
        return result;
    }
    
    /**
     * 捕捉全局异常:org.springframework.validation.BindException
     * <div>普通请求的参数,校验失败,抛出此异常</div>
     * <div>如:xxxAction(@Valid User user)</div>
     * @param exception
     * @return
     */
    @ExceptionHandler(BindException.class)
    public JSONObject handlerBindException(BindException exception) {
        log.info("全局异常[BindException]:" + exception.getMessage());
        JSONObject result = new JSONObject();
        result.put("success", false);
        if (exception != null) {
            String message = exception.getBindingResult().getFieldErrors().stream().filter(e -> e != null).map(FieldError::getDefaultMessage).collect(Collectors.joining(","));
            result.put("message", message);
        }
        return result;
    }
    
    /**
     * 捕捉全局异常:javax.validation.ConstraintViolationException
     * <div>直接在参数上加的校验,校验失败,抛出此异常</div>
     * <div>如:xxxAction(@NotNull(message = "用户代码不可为空") String userCode)</div>
     * <div>如:xxxAction(@PathVariable("userCode") @Length(max = 10,message="用户代码不可超过10位") String userCode)</div>
     * @param exception
     * @return
     */
    @ExceptionHandler(ConstraintViolationException.class)
    public JSONObject handlerConstraintViolationException(ConstraintViolationException exception) {
        log.info("全局异常[ConstraintViolationException]:" + exception.getMessage());
        JSONObject result = new JSONObject();
        result.put("success", false);
        if (exception != null) {
            result.put("message", exception.getConstraintViolations().stream().map(ConstraintViolation::getMessage).collect(Collectors.joining(";")));
        }
        return result;
    }
    
}

参数校验失败,返回的错误json信息如下,可以根据项目的实际情况进行定制:

{"success":false,"message":"用户代码不可超过10位"}

9.2 验证将检测到第一个约束违例时停止

import org.hibernate.validator.HibernateValidator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;

@Configuration
public class ValidConfig {
    @Bean
    public Validator validator() {
        ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
                .configure()
                //验证将检测到第一个约束违例时停止
                .failFast(true)
                .buildValidatorFactory();
        return validatorFactory.getValidator();
    }
}

10 补充

10.1 校验限制

@Valid支持嵌套验证、@Validated支持分组验证排序验证(准确来说,排序验证也是分组验证的一种)。

无法实现:“嵌套验证+分组验证”和“嵌套验证+排序验证”这种组合形式的验证。

10.2 建议

虽然JSR303支持自定义校验器,笔者不建议将太复杂的校验交给JSR303的标准进行校验

如果是参数基本的属性校验(是否为空、长度、大小、枚举、正则格式),可以以这种形式进行校验。

但是如果是太复杂的校验,如需要连接数据库进行业务判断的校验,笔者仍然建议在具体的业务代码中进行校验。

10.2 校验顺序的随机性

如不使用@Validated指定约束的校验顺序,所有约束的校验顺序是随机的,即相同的情况,返回的校验结果的顺序可能不一样。

10.3 一个字段多个约束

同一个字段可以加多个约束注解,并不是只能有一个约束注解。如下:

@NotNull(message = "用户代码不可为空")
@Length(min = 5, max = 10, message = "用户代码长度需在5~10之间")
private String userCode;

如userCode为空,则抛出异常:用户代码不可为空

如userCode不为空,则校验约束:用户代码长度需在5~10之间

上一篇 下一篇

猜你喜欢

热点阅读