SpringBoot 參數校驗的方法


Introduction

有參數傳遞的地方都少不了參數校驗。在web開發中,前端的參數校驗是為了用戶體驗,后端的參數校驗是為了安全。試想一下,如果在controller層中沒有經過任何校驗的參數通過service層、dao層一路來到了數據庫就可能導致嚴重的后果,最好的結果是查不出數據,嚴重一點就是報錯,如果這些沒有被校驗的參數中包含了惡意代碼,那就可能導致更嚴重的后果。

這里我們主要介紹在springboot中的幾種參數校驗方式。常用的用於參數校驗的注解如下:

  • @AssertFalse 所注解的元素必須是Boolean類型,且值為false
  • @AssertTrue 所注解的元素必須是Boolean類型,且值為true
  • @DecimalMax 所注解的元素必須是數字,且值小於等於給定的值
  • @DecimalMin 所注解的元素必須是數字,且值大於等於給定的值
  • @Digits 所注解的元素必須是數字,且值必須是指定的位數
  • @Future 所注解的元素必須是將來某個日期
  • @Max 所注解的元素必須是數字,且值小於等於給定的值
  • @Min 所注解的元素必須是數字,且值小於等於給定的值
  • @Range 所注解的元素需在指定范圍區間內
  • @NotNull 所注解的元素值不能為null
  • @NotBlank 所注解的元素值有內容
  • @Null 所注解的元素值為null
  • @Past 所注解的元素必須是某個過去的日期
  • @PastOrPresent 所注解的元素必須是過去某個或現在日期
  • @Pattern 所注解的元素必須滿足給定的正則表達式
  • @Size 所注解的元素必須是String、集合或數組,且長度大小需保證在給定范圍之內
  • @Email 所注解的元素需滿足Email格式

controller層參數校驗

在controller層的參數校驗可以分為兩種場景:

  1. 單個參數校驗
  2. 實體類參數校驗

單個參數校驗

@RestController
@Validated
public class PingController {

    @GetMapping("/getUser")
    public String getUserStr(@NotNull(message = "name 不能為空") String name,
                             @Max(value = 99, message = "不能大於99歲") Integer age) {
        return "name: " + name + " ,age:" + age;
    }
}

當處理GET請求時或只傳入少量參數的時候,我們可能不會建一個bean來接收這些參數,就可以像上面這樣直接在controller方法的參數中進行校驗。

注意:這里一定要在方法所在的controller類上加入@Validated注解,不然沒有任何效果。

這時候在postman輸入請求:

http://localhost:8080/getUser?name=Allan&age=101

調用方會收到springboot默認的格式報錯:

{
    "timestamp": "2019-06-01T04:30:26.882+0000",
    "status": 500,
    "error": "Internal Server Error",
    "message": "getUserStr.age: 不能大於99歲",
    "path": "/getUser"
}

后台會打印如下錯誤:

javax.validation.ConstraintViolationException: getUserStr.age: 不能大於99歲
   at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:116)
   at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185)
   at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:688)
   at io.shopee.bigdata.penalty.server.controller.PingController$$EnhancerBySpringCGLIB$$232cfd51.getUserStr(<generated>)
   ...

如果有很多使用這種參數驗證的controller方法,我們希望在一個地方對ConstraintViolationException異常進行統一處理,可以使用統一異常捕獲,這需要借助@ControllerAdvice注解來實現,當然在springboot中我們就用@RestControllerAdvice(內部包含@ControllerAdvice和@ResponseBody的特性)

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.ValidationException;
import java.util.Set;

/**
 * @author pengchengbai
 * @date 2019-06-01 14:09
 */
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ValidationException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public String handle(ValidationException exception) {
        if(exception instanceof ConstraintViolationException){
            ConstraintViolationException exs = (ConstraintViolationException) exception;

            Set<ConstraintViolation<?>> violations = exs.getConstraintViolations();
            for (ConstraintViolation<?> item : violations) {
                //打印驗證不通過的信息
                System.out.println(item.getMessage());
            }
        }
        return "bad request" ;
    }
}

當參數校驗異常的時候,該統一異常處理類在控制台打印信息的同時把bad request的字符串和HttpStatus.BAD_REQUEST所表示的狀態碼400返回給調用方(用@ResponseBody注解實現,表示該方法的返回結果直接寫入HTTP response body 中)。其中:

  • @ControllerAdvice:控制器增強,使@ExceptionHandler、@InitBinder、@ModelAttribute注解的方法應用到所有的 @RequestMapping注解的方法。
  • @ExceptionHandler:異常處理器,此注解的作用是當出現其定義的異常時進行處理的方法,此例中處理ValidationException異常。

實體類參數校驗

當處理post請求或者請求參數較多的時候我們一般會選擇使用一個bean來接收參數,然后在每個需要校驗的屬性上使用參數校驗注解:

@Data
public class UserInfo {
    @NotNull(message = "username cannot be null")
    private String name;

    @NotNull(message = "sex cannot be null")
    private String sex;

    @Max(value = 99L)
    private Integer age;
}

然后在controller方法中用@RequestBody表示這個參數接收的類:

@RestController
public class PingController {
    @Autowired
    private Validator validator;

    @GetMapping("metrics/ping")
    public Response<String> ping() {
        return new Response<>(ResponseCode.SUCCESS, null,"pang");
    }

    @PostMapping("/getUser")
    public String getUserStr(@RequestBody @Validated({GroupA.class, Default.class}) UserInfo user, BindingResult bindingResult) {
        validData(bindingResult);

        return "name: " + user.getName() + ", age:" + user.getAge();
    }

    private void validData(BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            StringBuffer sb = new StringBuffer();
            for (ObjectError error : bindingResult.getAllErrors()) {
                sb.append(error.getDefaultMessage());
            }
            throw new ValidationException(sb.toString());
        }
    }
}

需要注意的是,如果想讓UserInfo中的參數注解生效,還必須在Controller參數中使用@Validated注解。這種參數校驗方式的校驗結果會被放到BindingResult中,我們這里寫了一個統一的方法來處理這些結果,通過拋出異常的方式得到GlobalExceptionHandler的統一處理。

校驗模式

在上面的例子中,我們使用BindingResult驗證不通過的結果集合,但是通常按順序驗證到第一個字段不符合驗證要求時,就可以直接拒絕請求了。這就涉及到兩種校驗模式的配置:

  1. 普通模式(默認是這個模式): 會校驗完所有的屬性,然后返回所有的驗證失敗信息
  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 ValidatorConf {
    @Bean
    public Validator validator() {
        ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
                .configure()
                .failFast( true )
                .buildValidatorFactory();
        Validator validator = validatorFactory.getValidator();

        return validator;
    }
}

參數校驗分組

在實際開發中經常會遇到這種情況:想要用一個實體類去接收多個controller的參數,但是不同controller所需要的參數又有些許不同,而你又不想為這點不同去建個新的類接收參數。比如有一個/setUser接口不需要id參數,而/getUser接口又需要該參數,這種時候就可以使用參數分組來實現。

  1. 定義表示組別的interface
public interface GroupA {
}
  1. @Validated中指定使用哪個組;
@RestController
public class PingController {
    @PostMapping("/getUser")
    public String getUserStr(@RequestBody @Validated({GroupA.class, Default.class}) UserInfo user, BindingResult bindingResult) {
        validData(bindingResult);
        return "name: " + user.getName() + ", age:" + user.getAge();
    }

    @PostMapping("/setUser")
    public String setUser(@RequestBody @Validated UserInfo user, BindingResult bindingResult) {
        validData(bindingResult);
        return "name: " + user.getName() + ", age:" + user.getAge();
    }

其中Defaultjavax.validation.groups中的類,表示參數類中其他沒有分組的參數,如果沒有,/getUser接口的參數校驗就只會有標記了GroupA的參數校驗生效。

  1. 在實體類的注解中標記這個哪個組所使用的參數;
@Data
public class UserInfo {
    @NotNull( groups = {GroupA.class}, message = "id cannot be null")
    private Integer id;

    @NotNull(message = "username cannot be null")
    private String name;

    @NotNull(message = "sex cannot be null")
    private String sex;

    @Max(value = 99L)
    private Integer age;
}

級聯參數校驗

當參數bean中的屬性又是一個復雜數據類型或者是一個集合的時候,如果需要對其進行進一步的校驗需要考慮哪些情況呢?

@Data
public class UserInfo {
    @NotNull( groups = {GroupA.class}, message = "id cannot be null")
    private Integer id;

    @NotNull(message = "username cannot be null")
    private String name;

    @NotNull(message = "sex cannot be null")
    private String sex;

    @Max(value = 99L)
    private Integer age;
   
    @NotEmpty
    private List<Parent> parents;
}

比如對於parents參數,@NotEmpty只能保證list不為空,但是list中的元素是否為空、User對象中的屬性是否合格,還需要進一步的校驗。這個時候我們可以這樣寫:

    @NotEmpty
    private List<@NotNull @Valid UserInfo> parents;

然后再繼續在UserInfo類中使用注解對每個參數進行校驗。

但是我們再回過頭來看看,在controller中對實體類進行校驗的時候使用的@Validated,在這里只能使用@Valid,否則會報錯。關於這兩個注解的具體區別可以參考@Valid 和@Validated的關系,但是在這里我想說的是使用@Valid就沒辦法對UserInfo進行分組校驗。這種時候我們就會想,如果能夠定義自己的validator就好了,最好能支持分組,像函數一樣調用對目標參數進行校驗,就像下面的validObject方法一樣:

import javax.validation.Validator;

@RestController
public class PingController {
    @Autowired
    private Validator validator;

    @PostMapping("/setUser")
    public String setUser(@RequestBody @Validated UserInfo user, BindingResult bindingResult) {
        validData(bindingResult);
        Parent parent = user.getParent();
        validObject(parent, validator, GroupB.class, Default.class);
        return "name: " + user.getName() + ", age:" + user.getAge();
    }

    private void validData(BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            StringBuffer sb = new StringBuffer();
            for (ObjectError error : bindingResult.getAllErrors()) {
                sb.append(error.getDefaultMessage());
            }
            throw new ValidationException(sb.toString());
        }
    }

    /**
     * 實體類參數有效性驗證
     * @param bean 驗證的實體對象
     * @param groups 驗證組
     * @return 驗證成功:返回true;驗證失敗:將錯誤信息添加到message中
     */
    public void validObject(Object bean, Validator validator, Class<?> ...groups) {
        Set<ConstraintViolation<Object>> constraintViolationSet = validator.validate(bean, groups);
        if (!constraintViolationSet.isEmpty()) {
            StringBuilder sb = new StringBuilder();
            for (ConstraintViolation violation: constraintViolationSet) {
                sb.append(violation.getMessage());
            }

            throw new ValidationException(sb.toString());
        }
    }
}


@Data
public class Parent {
    @NotEmpty(message = "parent name cannot be empty", groups = {GroupB.class})
    private String name;

    @Email(message = "should be email format")
    private String email;
}

自定義參數校驗

雖然JSR303和Hibernate Validtor 已經提供了很多校驗注解,但是當面對復雜參數校驗時,還是不能滿足我們的要求,這時候我們就需要自定義校驗注解。這里我們再回到上面的例子介紹一下自定義參數校驗的步驟。private List<@NotNull @Valid UserInfo> parents這種在容器中進行參數校驗是Bean Validation2.0的新特性,假如沒有這個特性,我們來試着自定義一個List數組中不能含有null元素的注解。這個過程大概可以分為兩步:

  1. 自定義一個用於參數校驗的注解,並為該注解指定校驗規則的實現類
  2. 實現校驗規則的實現類

自定義注解

定義@ListNotHasNull注解, 用於校驗 List 集合中是否有null 元素

@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
//此處指定了注解的實現類為ListNotHasNullValidatorImpl
@Constraint(validatedBy = ListNotHasNullValidatorImpl.class)
public @interface ListNotHasNull {

    /**
     * 添加value屬性,可以作為校驗時的條件,若不需要,可去掉此處定義
     */
    int value() default 0;

    String message() default "List集合中不能含有null元素";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    /**
     * 定義List,為了讓Bean的一個屬性上可以添加多套規則
     */
    @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
    @Retention(RUNTIME)
    @Documented
    @interface List {
        ListNotHasNull[] value();
    }
}

注意:message、groups、payload屬性都需要定義在參數校驗注解中不能缺省

注解實現類

該類需要實現ConstraintValidator

import org.springframework.stereotype.Service;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.List;

public class ListNotHasNullValidatorImpl implements ConstraintValidator<ListNotHasNull, List> {

    private int value;

    @Override
    public void initialize(ListNotHasNull constraintAnnotation) {
        //傳入value 值,可以在校驗中使用
        this.value = constraintAnnotation.value();
    }

    public boolean isValid(List list, ConstraintValidatorContext constraintValidatorContext) {
        for (Object object : list) {
            if (object == null) {
                //如果List集合中含有Null元素,校驗失敗
                return false;
            }
        }
        return true;
    }
}

然后我們就能在之前的例子中使用該注解了:

@NotEmpty
@ListNotHasNull
private List<@Valid UserInfo> parents;

其他

Difference Between @NotNull, @NotEmpty, and @NotBlank

@NotNull

不能為null,但是可以為空字符串""

@NotEmpty

不能為null,不能為空字符串"",其本質是CharSequence, Collection, Map, or Array的size或者length不能為0

@NotBlank

a constrained String is valid as long as it’s not null and the trimmed length is greater than zero

@NonNull

@NotNull 是 JSR303(Bean的校驗框架)的注解,用於運行時檢查一個屬性是否為空,如果為空則不合法。
@NonNull 是JSR 305(缺陷檢查框架)的注解,是告訴編譯器這個域不可能為空,當代碼檢查有空值時會給出一個風險警告,目前這個注解只有IDEA支持。

@Valid 注解和描述

參考資料

  1. spring5.0 中的@NonNull
  2. Difference Between @NotNull, @NotEmpty, and @NotBlank
  3. https://my.oschina.net/u/3773384/blog/1795869
  4. @Valid 和@Validated的關系
  5. 校驗bean中的bean
  6. springboot使用hibernate validator校驗
  7. 使用hibernate validation完成數據后端校驗


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM