springboot 參數校驗詳解


目標

  1. 對於幾種常見的入參方式,了解如何進行校驗以及該如何處理錯誤消息;
  2. 了解springboot 內置的參數異常類型,並能利用攔截器實現自定義處理;
  3. 能實現簡單的自定義校驗規則

一、PathVariable 校驗

在定義 Restful 風格的接口時,通常會采用 PathVariable 指定關鍵業務參數,如下:

@GetMapping("/path/{group:[a-zA-Z0-9_]+}/{userid}")
@ResponseBody
public String path(@PathVariable("group") String group, @PathVariable("userid") Integer userid) {
    return group + ":" + userid;
}

{group:[a-zA-Z0-9_]+} 這樣的表達式指定了 group 必須是以大小寫字母、數字或下划線組成的字符串。
我們試着訪問一個錯誤的路徑:

GET /path/testIllegal.get/10000

此時會得到 404的響應,因此對於PathVariable 僅由正則表達式可達到校驗的目的

二、方法參數校驗

類似前面的例子,大多數情況下,我們都會直接將HTTP請求參數映射到方法參數上。

@GetMapping("/param")
@ResponseBody
public String param(@RequestParam("group")@Email String group, 
                    @RequestParam("userid") Integer userid) {
   return group + ":" + userid;
}

上面的代碼中,@RequestParam 聲明了映射,此外我們還為 group 定義了一個規則(復合Email格式)
這段代碼是否能直接使用呢?答案是否定的,為了啟用方法參數的校驗能力,還需要完成以下步驟:

  • 聲明 MethodValidationPostProcessor
@Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
     return new MethodValidationPostProcessor();
}
  • Controller指定@Validated注解
@Controller
@RequestMapping("/validate")
@Validated
public class ValidateController {

如此之后,方法上的@Email規則才能生效。

校驗異常
如果此時我們嘗試通過非法參數進行訪問時,比如提供非Email格式的 group
會得到以下錯誤:

GET /validate/param?group=simple&userid=10000
====>
{
    "timestamp": 1530955093583,
    "status": 500,
    "error": "Internal Server Error",
    "exception": "javax.validation.ConstraintViolationException",
    "message": "No message available",
    "path": "/validate/param"
}

而如果參數類型錯誤,比如提供非整數的 userid,會得到:

GET /validate/param?group=simple&userid=1f
====>
{
    "timestamp": 1530954430720,
    "status": 400,
    "error": "Bad Request",
    "exception": "org.springframework.web.method.annotation.MethodArgumentTypeMismatchException",
    "message": "Failed to convert value of type 'java.lang.String' to required type 'java.lang.Integer'; nested exception is java.lang.NumberFormatException: For input string: \"1f\"",
    "path": "/validate/param"
}

當存在參數缺失時,由於定義的@RequestParam注解中,屬性 required=true,也將會導致失敗:

GET /validate/param?userid=10000
====>
{
    "timestamp": 1530954345877,
    "status": 400,
    "error": "Bad Request",
    "exception": "org.springframework.web.bind.MissingServletRequestParameterException",
    "message": "Required String parameter 'group' is not present",
    "path": "/validate/param"
}

三、表單對象校驗

頁面的表單通常比較復雜,此時可以將請求參數封裝到表單對象中,
並指定一系列對應的規則,參考JSR-303

public static class FormRequest {
<span class="hljs-variable">@NotEmpty</span>
<span class="hljs-variable">@Email</span>
private String email;

<span class="hljs-variable">@Pattern</span>(regexp = <span class="hljs-string">"[a-zA-Z0-9_]{6,30}"</span>)
private String name;

<span class="hljs-variable">@Min</span>(<span class="hljs-number">5</span>)
<span class="hljs-variable">@Max</span>(<span class="hljs-number">199</span>)
private int age;</code></pre>

上面定義的屬性中:

  • email必須非空、符合Email格式規則;
  • name必須為大小寫字母、數字及下划線組成,長度在6-30個;
  • age必須在5-199范圍內

Controller方法中的定義:

@PostMapping("/form")
@ResponseBody
public FormRequest form(@Validated FormRequest form) {
    return form;
}

@Validated指定了參數對象需要執行一系列校驗。

校驗異常
此時我們嘗試構造一些違反規則的輸入,會得到以下的結果:

{
    "timestamp": 1530955713166,
    "status": 400,
    "error": "Bad Request",
    "exception": "org.springframework.validation.BindException",
    "errors": [
        {
            "codes": [
                "Email.formRequest.email",
                "Email.email",
                "Email.java.lang.String",
                "Email"
            ],
            "arguments": [
                {
                    "codes": [
                        "formRequest.email",
                        "email"
                    ],
                    "arguments": null,
                    "defaultMessage": "email",
                    "code": "email"
                },
                [],
                {
                    "arguments": null,
                    "codes": [
                        ".*"
                    ],
                    "defaultMessage": ".*"
                }
            ],
            "defaultMessage": "不是一個合法的電子郵件地址",
            "objectName": "formRequest",
            "field": "email",
            "rejectedValue": "tecom",
            "bindingFailure": false,
            "code": "Email"
        },
        {
            "codes": [
                "Pattern.formRequest.name",
                "Pattern.name",
                "Pattern.java.lang.String",
                "Pattern"
            ],
            "arguments": [
                {
                    "codes": [
                        "formRequest.name",
                        "name"
                    ],
                    "arguments": null,
                    "defaultMessage": "name",
                    "code": "name"
                },
                [],
                {
                    "arguments": null,
                    "codes": [
                        "[a-zA-Z0-9_]{6,30}"
                    ],
                    "defaultMessage": "[a-zA-Z0-9_]{6,30}"
                }
            ],
            "defaultMessage": "需要匹配正則表達式\"[a-zA-Z0-9_]{6,30}\"",
            "objectName": "formRequest",
            "field": "name",
            "rejectedValue": "fefe",
            "bindingFailure": false,
            "code": "Pattern"
        },
        {
            "codes": [
                "Min.formRequest.age",
                "Min.age",
                "Min.int",
                "Min"
            ],
            "arguments": [
                {
                    "codes": [
                        "formRequest.age",
                        "age"
                    ],
                    "arguments": null,
                    "defaultMessage": "age",
                    "code": "age"
                },
                5
            ],
            "defaultMessage": "最小不能小於5",
            "objectName": "formRequest",
            "field": "age",
            "rejectedValue": 2,
            "bindingFailure": false,
            "code": "Min"
        }
    ],
    "message": "Validation failed for object='formRequest'. Error count: 3",
    "path": "/validate/form"
}

如果是參數類型不匹配,會得到:

{
    "timestamp": 1530955359265,
    "status": 400,
    "error": "Bad Request",
    "exception": "org.springframework.validation.BindException",
    "errors": [
        {
            "codes": [
                "typeMismatch.formRequest.age",
                "typeMismatch.age",
                "typeMismatch.int",
                "typeMismatch"
            ],
            "arguments": [
                {
                    "codes": [
                        "formRequest.age",
                        "age"
                    ],
                    "arguments": null,
                    "defaultMessage": "age",
                    "code": "age"
                }
            ],
            "defaultMessage": "Failed to convert property value of type 'java.lang.String' to required type 'int' for property 'age'; nested exception is java.lang.NumberFormatException: For input string: \"\"",
            "objectName": "formRequest",
            "field": "age",
            "rejectedValue": "",
            "bindingFailure": true,
            "code": "typeMismatch"
        }
    ],
    "message": "Validation failed for object='formRequest'. Error count: 1",
    "path": "/validate/form"
}

Form表單參數上,使用@Valid注解可達到同樣目的,而關於兩者的區別則是:

@Valid 基於JSR303,即 Bean Validation 1.0,由Hibernate Validator實現;
@Validated 基於JSR349,是Bean Validation 1.1,由Spring框架擴展實現;

后者做了一些增強擴展,如支持分組校驗,有興趣可參考這里

四、RequestBody 校驗

對於直接Json消息體輸入,同樣可以定義校驗規則:

@PostMapping("/json") @ResponseBody public JsonRequest json(@Validated @RequestBody JsonRequest request) { 
<span class="hljs-selector-tag">return</span> <span class="hljs-selector-tag">request</span>;

}

...
public static class JsonRequest {

<span class="hljs-variable">@NotEmpty</span>
<span class="hljs-variable">@Email</span>
private String email;

<span class="hljs-variable">@Pattern</span>(regexp = <span class="hljs-string">"[a-zA-Z0-9_]{6,30}"</span>)
private String name;

<span class="hljs-variable">@Min</span>(<span class="hljs-number">5</span>)
<span class="hljs-variable">@Max</span>(<span class="hljs-number">199</span>)
private int age;</code></pre>

校驗異常
構造一個違反規則的Json請求體進行輸入,會得到:

{
    "timestamp": 1530956161314,
    "status": 400,
    "error": "Bad Request",
    "exception": "org.springframework.web.bind.MethodArgumentNotValidException",
    "errors": [
        {
            "codes": [
                "Min.jsonRequest.age",
                "Min.age",
                "Min.int",
                "Min"
            ],
            "arguments": [
                {
                    "codes": [
                        "jsonRequest.age",
                        "age"
                    ],
                    "arguments": null,
                    "defaultMessage": "age",
                    "code": "age"
                },
                5
            ],
            "defaultMessage": "最小不能小於5",
            "objectName": "jsonRequest",
            "field": "age",
            "rejectedValue": 1,
            "bindingFailure": false,
            "code": "Min"
        }
    ],
    "message": "Validation failed for object='jsonRequest'. Error count: 1",
    "path": "/validate/json"
}

此時與FormBinding的情況不同,我們得到了一個MethodArgumentNotValidException異常。
而如果發生參數類型不匹配,比如輸入age=1f,會產生以下結果:

{
    "timestamp": 1530956206264,
    "status": 400,
    "error": "Bad Request",
    "exception": "org.springframework.http.converter.HttpMessageNotReadableException",
    "message": "Could not read document: Can not deserialize value of type int from String \"ff\": not a valid Integer value\n at [Source: java.io.PushbackInputStream@68dc9800; line: 2, column: 8] (through reference chain: org.zales.dmo.boot.controllers.ValidateController$JsonRequest[\"age\"]); nested exception is com.fasterxml.jackson.databind.exc.InvalidFormatException: Can not deserialize value of type int from String \"ff\": not a valid Integer value\n at [Source: java.io.PushbackInputStream@68dc9800; line: 2, column: 8] (through reference chain: org.zales.dmo.boot.controllers.ValidateController$JsonRequest[\"age\"])",
    "path": "/validate/json"
}

這表明在JSON轉換過程中已經失敗!

五、自定義校驗規則

框架內預置的校驗規則可以滿足大多數場景使用,
但某些特殊情況下,你需要制作自己的校驗規則,這需要用到ContraintValidator接口。

我們以一個密碼校驗的場景作為示例,比如一個注冊表單上,
我們需要檢查 密碼輸入密碼確認 是一致的。

**首先定義 PasswordEquals 注解

@Documented @Constraint(validatedBy = { PasswordEqualsValidator.class }) @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) public @interface PasswordEquals { 
<span class="hljs-selector-tag">String</span> <span class="hljs-selector-tag">message</span>() <span class="hljs-selector-tag">default</span> "<span class="hljs-selector-tag">Password</span> <span class="hljs-selector-tag">is</span> <span class="hljs-selector-tag">not</span> <span class="hljs-selector-tag">the</span> <span class="hljs-selector-tag">same</span>";

<span class="hljs-selector-tag">Class</span>&lt;?&gt;<span class="hljs-selector-attr">[]</span> <span class="hljs-selector-tag">groups</span>() <span class="hljs-selector-tag">default</span> {};

<span class="hljs-selector-tag">Class</span>&lt;? <span class="hljs-selector-tag">extends</span> <span class="hljs-selector-tag">Payload</span>&gt;<span class="hljs-selector-attr">[]</span> <span class="hljs-selector-tag">payload</span>() <span class="hljs-selector-tag">default</span> {};

}

在表單上聲明@PasswordEquals 注解

@PasswordEquals
public class RegisterForm {
<span class="hljs-variable">@NotEmpty</span>
<span class="hljs-variable">@Length</span>(min=<span class="hljs-number">5</span>,max=<span class="hljs-number">30</span>)
private String username;

<span class="hljs-variable">@NotEmpty</span>
private String password;

<span class="hljs-variable">@NotEmpty</span>
private String passwordConfirm;</code></pre>

針對@PasswordEquals實現校驗邏輯

public class PasswordEqualsValidator implements ConstraintValidator<PasswordEquals, RegisterForm> { 
<span class="hljs-meta">@Override</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">initialize</span><span class="hljs-params">(PasswordEquals anno)</span> </span>{
}

<span class="hljs-meta">@Override</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">isValid</span><span class="hljs-params">(RegisterForm form, ConstraintValidatorContext context)</span> </span>{
    String passwordConfirm = form.getPasswordConfirm();
    String password = form.getPassword();

    <span class="hljs-keyword">boolean</span> match = passwordConfirm != <span class="hljs-keyword">null</span> ? passwordConfirm.equals(password) : <span class="hljs-keyword">false</span>;
    <span class="hljs-keyword">if</span> (match) {
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">true</span>;
    }

    String messageTemplate = context.getDefaultConstraintMessageTemplate();
    
    <span class="hljs-comment">// disable default violation rule</span>
    context.disableDefaultConstraintViolation();

    <span class="hljs-comment">// assign error on password Confirm field</span>
    context.buildConstraintViolationWithTemplate(messageTemplate).addPropertyNode(<span class="hljs-string">"passwordConfirm"</span>)
            .addConstraintViolation();
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;

}

}

如此,我們已經完成了自定義的校驗工作。

六、異常攔截器

SpringBoot 框架中可通過 @ControllerAdvice 實現Controller方法的攔截操作。
可以利用攔截能力實現一些公共的功能,比如權限檢查、頁面數據填充,以及全局的異常處理等等。

在前面的篇幅中,我們提及了各種校驗失敗所產生的異常,整理如下表:

異常類型 描述
ConstraintViolationException 違反約束,javax擴展定義
BindException 綁定失敗,如表單對象參數違反約束
MethodArgumentNotValidException 參數無效,如JSON請求參數違反約束
MissingServletRequestParameterException 參數缺失
TypeMismatchException 參數類型不匹配

如果希望對這些異常實現統一的捕獲,並返回自定義的消息,
可以參考以下的代碼片段:

@ControllerAdvice
public static class CustomExceptionHandler extends ResponseEntityExceptionHandler {
<span class="hljs-meta">@ExceptionHandler</span>(value = { ConstraintViolationException.<span class="hljs-keyword">class</span> })
public ResponseEntity&lt;<span class="hljs-built_in">String</span>&gt; handle(ConstraintViolationException e) {
    <span class="hljs-built_in">Set</span>&lt;ConstraintViolation&lt;?&gt;&gt; violations = e.getConstraintViolations();
    StringBuilder strBuilder = <span class="hljs-keyword">new</span> StringBuilder();
    <span class="hljs-keyword">for</span> (ConstraintViolation&lt;?&gt; violation : violations) {
        strBuilder.append(violation.getInvalidValue() + <span class="hljs-string">" "</span> + violation.getMessage() + <span class="hljs-string">"\n"</span>);
    }
    <span class="hljs-built_in">String</span> result = strBuilder.toString();
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> ResponseEntity&lt;<span class="hljs-built_in">String</span>&gt;(<span class="hljs-string">"ConstraintViolation:"</span> + result, HttpStatus.BAD_REQUEST);
}

<span class="hljs-meta">@Override</span>
protected ResponseEntity&lt;<span class="hljs-built_in">Object</span>&gt; handleBindException(BindException ex, HttpHeaders headers, HttpStatus status,
        WebRequest request) {
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> ResponseEntity&lt;<span class="hljs-built_in">Object</span>&gt;(<span class="hljs-string">"BindException:"</span> + buildMessages(ex.getBindingResult()),
            HttpStatus.BAD_REQUEST);
}

<span class="hljs-meta">@Override</span>
protected ResponseEntity&lt;<span class="hljs-built_in">Object</span>&gt; handleMethodArgumentNotValid(MethodArgumentNotValidException ex,
        HttpHeaders headers, HttpStatus status, WebRequest request) {
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> ResponseEntity&lt;<span class="hljs-built_in">Object</span>&gt;(<span class="hljs-string">"MethodArgumentNotValid:"</span> + buildMessages(ex.getBindingResult()),
            HttpStatus.BAD_REQUEST);
}

<span class="hljs-meta">@Override</span>
public ResponseEntity&lt;<span class="hljs-built_in">Object</span>&gt; handleMissingServletRequestParameter(MissingServletRequestParameterException ex,
        HttpHeaders headers, HttpStatus status, WebRequest request) {
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> ResponseEntity&lt;<span class="hljs-built_in">Object</span>&gt;(<span class="hljs-string">"ParamMissing:"</span> + ex.getMessage(), HttpStatus.BAD_REQUEST);
}

<span class="hljs-meta">@Override</span>
protected ResponseEntity&lt;<span class="hljs-built_in">Object</span>&gt; handleTypeMismatch(TypeMismatchException ex, HttpHeaders headers,
        HttpStatus status, WebRequest request) {
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> ResponseEntity&lt;<span class="hljs-built_in">Object</span>&gt;(<span class="hljs-string">"TypeMissMatch:"</span> + ex.getMessage(), HttpStatus.BAD_REQUEST);
}

private <span class="hljs-built_in">String</span> buildMessages(BindingResult result) {
    StringBuilder resultBuilder = <span class="hljs-keyword">new</span> StringBuilder();

    <span class="hljs-built_in">List</span>&lt;ObjectError&gt; errors = result.getAllErrors();
    <span class="hljs-keyword">if</span> (errors != <span class="hljs-keyword">null</span> &amp;&amp; errors.size() &gt; <span class="hljs-number">0</span>) {
        <span class="hljs-keyword">for</span> (ObjectError error : errors) {
            <span class="hljs-keyword">if</span> (error instanceof FieldError) {
                FieldError fieldError = (FieldError) error;
                <span class="hljs-built_in">String</span> fieldName = fieldError.getField();
                <span class="hljs-built_in">String</span> fieldErrMsg = fieldError.getDefaultMessage();
                resultBuilder.append(fieldName).append(<span class="hljs-string">" "</span>).append(fieldErrMsg).append(<span class="hljs-string">";"</span>);
            }
        }
    }
    <span class="hljs-keyword">return</span> resultBuilder.toString();
}

}

默認情況下,對於非法的參數輸入,框架會產生 HTTP_BAD_REQUEST(status=400) 錯誤碼,
並輸出友好的提示消息,這對於一般情況來說已經足夠。

更多的輸入校驗及提示功能應該通過客戶端去完成(服務端僅做同步檢查),
客戶端校驗的用戶體驗更好,而這也符合富客戶端(rich client)的發展趨勢。

碼雲同步代碼

參考文檔

springmvc-validation樣例
使用validation api進行操作
hibernate-validation官方文檔
Bean-Validation規范

原文地址:https://www.cnblogs.com/littleatp/p/9391856.html


免責聲明!

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



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