Spring Boot之輸入Bean驗證@Valid應用場景總結

bladestone發表於2019-03-25

Validation應用場景

在日常應用中,存在大量需要針對輸入資料進行驗證的應用場景,例如字串的大小,ip地址驗證,email格式驗證等等各類情況。
值得開心的是Spring Boot將這類場景進行了整合抽象,提供一個完整而且靈活的機制簡化開發者在進行相關操作時的重複工作量。

Maven引用

在pom檔案中新增如下資訊:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

就可以將相關Validation操作引入系統了。

基於@Valid驗證JsonBean的輸入實體Bean

定義Product實體類:

public class Product {
    //限購:1~5
    @Min(value = 1, message = "min Value is above zero")
    @Max(value = 10, message = "最大值不超過10")
    private int count;

    @Pattern(regexp = "^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$", message = "不符合IP規則")
    private String ipAddr;

    @Email(message = "email format error")
    @NotBlank
    private String email;

    @NotBlank(message = "name 不能為空")
    @Length(min=5, max = 10, message = "name長度在5到10個字元之間")
    private String name;
}

Controller測試類:

@RestController
@Slf4j
public class TestController {
    @PostMapping("/test")
    public ResponseEntity<String> validateJsonBean(@Valid @RequestBody Product product) {
        return ResponseEntity.ok("valid id done");
    }
}

輸入測試資料:

{
	"name": "asd",
	"count":100,
	"ipAddr": "12.2.1",
	"email":""
}

其相應結果資訊如下:

{
    "timestamp": "2019-03-25T11:29:43.382+0000",
    "status": 400,
    "error": "Bad Request",
    "errors": [
        {
            "codes": [
                "Min.product.count",
                "Min.count",
                "Min"
            ],
            "arguments": [
                {
                    "codes": [
                        "product.count",
                        "count"
                    ],
                    "arguments": null,
                    "defaultMessage": "count",
                    "code": "count"
                },
                1
            ],
            "defaultMessage": "min Value is above zero",
            "objectName": "product",
            "field": "count",
            "rejectedValue": 0,
            "bindingFailure": false,
            "code": "Min"
        },
        {
            "codes": [
                "NotBlank.product.email",
                "NotBlank.email",
                "NotBlank"
            ],
            "arguments": [
                {
                    "codes": [
                        "product.email",
                        "email"
                    ],
                    "arguments": null,
                    "defaultMessage": "email",
                    "code": "email"
                }
            ],
            "defaultMessage": "不能為空",
            "objectName": "product",
            "field": "email",
            "rejectedValue": null,
            "bindingFailure": false,
            "code": "NotBlank"
        },
        {
            "codes": [
                "NotBlank.product.name",
                "NotBlank.name",
                "NotBlank"
            ],
            "arguments": [
                {
                    "codes": [
                        "product.name",
                        "name"
                    ],
                    "arguments": null,
                    "defaultMessage": "name",
                    "code": "name"
                }
            ],
            "defaultMessage": "name 不能為空",
            "objectName": "product",
            "field": "name",
            "rejectedValue": null,
            "bindingFailure": false,
            "code": "NotBlank"
        }
    ],
    "message": "Validation failed for object='product'. Error count: 3",
    "trace": "org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public org.springframework.http.ResponseEntity<java.lang.String> org.cjf.validationdemo.controller.TestController.validateJsonBean(org.cjf.validationdemo.beans.Product) with 3 errors: [Field error in object 'product' on field 'count': rejected value [0]; codes [Min.product.count,Min.count,Min]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [product.count,count]; arguments []; default message [count],1]; default message [min Value is above zero]] [Field error in object 'product' on field 'email': rejected value [null]; codes [NotBlank.product.email,NotBlank.email,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [product.email,email]; arguments []; default message [email]]; default message [不能為空]] [Field error in object 'product' on field 'name': rejected value [null]; codes [NotBlank.product.name,NotBlank.name,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [product.name,name]; arguments []; default message [name]]; default message [name 不能為空]] \n\tat org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.resolveArgument(RequestResponseBodyMethodProcessor.java:138)\n\tat org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:126)\n\tat org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:166)\n\tat org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:134)\n\tat org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:102)\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895)\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:800)\n\tat org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)\n\tat org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1038)\n\tat org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:942)\n\tat org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1005)\n\tat org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:908)\n\tat javax.servlet.http.HttpServlet.service(HttpServlet.java:660)\n\tat org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:882)\n\tat javax.servlet.http.HttpServlet.service(HttpServlet.java:741)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\n\tat org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\n\tat org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\n\tat org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:92)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\n\tat org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:93)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\n\tat org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:200)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\n\tat org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:200)\n\tat org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96)\n\tat org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:490)\n\tat org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:139)\n\tat org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)\n\tat org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)\n\tat org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)\n\tat org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:408)\n\tat org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66)\n\tat org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:834)\n\tat org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1415)\n\tat org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)\n\tat java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)\n\tat java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)\n\tat org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)\n\tat java.lang.Thread.run(Thread.java:748)\n",
    "path": "/test"
}

從上述資訊中可以發現,其status code為400,表示特定錯誤資訊。在程式碼實現層面,其將丟擲MethodArgumentNotValidException的異常,由Spring框架進行捕獲,並轉化為400的異常資訊提示出來。
在errors中展示了定製的錯誤提示資訊. 這裡沒有定製錯誤的提示結果,使用了預設的error提示資訊結果格式。

如果定製化錯誤資訊的展示結果,會更新人性化一些,可讀性會更好。具體示例如下:

@Data
public class Product {
    //限購:1~5
    @Min(value = 1, message = "min Value is above zero")
    @Max(value = 10, message = "最大值不超過10")
    private int count;

    @Pattern(regexp = "^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$", message = "不符合IP規則")
    private String ipAddr;

    @Email(message = "email format error")
    @NotBlank
    private String email;

    @NotBlank(message = "name 不能為空")
    @Length(min=5, max = 10, message = "name長度在5到10個字元之間")
    private String name;
}

基於@RequestParam和@PathVariable增加Valid

在Spring MVC應用中,允許使用上述各類的valid註解去驗證@RequestParam和@PathVariable中定義的各類params。
具體示例如下:

@Controller
@Validated
public class PathVariableController {
    @GetMapping("/valid/test/{id}")
    public ResponseEntity<String> handleTest(@PathVariable("id") @Max(10) int id,
                                             @RequestParam("count") @Min(5) int count) {
        return ResponseEntity.ok("validation is done");
    }
}

注意這裡使用@Validated來修飾整個類,表示其中方法需要在method這個級別使用validation邏輯進行驗證。
針對@PathVariable/@RequestParam直接掛接註解的方式來修飾特定變數。
注意一下,這裡@Validated驗證丟擲的異常是javax.validation.ConstraintViolationException,在瀏覽器訪問的過程中,其體會提示500的內部錯誤資訊。
在這裡插入圖片描述
如果希望定製這個錯誤資訊,可以將這個異常進行捕獲,然後自行定義響應資訊,具體做法如下:

@ControllerAdvice
public class GlobalExceptionHandler {
    /**
     * Exception Handler.
     *
     * @param e
     * @return
     */
    @ExceptionHandler(ConstraintViolationException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    ResponseEntity<String> handleConstraintViolationException(ConstraintViolationException e) {
        return new ResponseEntity<>("be valid due to validation error: " + e.getMessage(), HttpStatus.BAD_REQUEST);
    }
}

這裡ControllerAdvice是表示其為advice,用來接收controller的相關處理邏輯。
@ExceptionHandler主要用來定義系統內部發生的特定異常的處理邏輯。
在這裡是將異常轉化為了400的status code資訊。

單元測試程式碼如下:

@RunWith(SpringRunner.class)
@WebMvcTest(controllers={PathVariableController.class})
@Slf4j
public class PathVariableTestTest {
    @Autowired
    private MockMvc mockMvc;

    @Before
    public void before() throws Exception {
    }

    @After
    public void after() throws Exception {
    }

    @Test
    public void testValidParams() throws Exception {
        mockMvc.perform(get("/valid/test/{id}?count={count}", 9, 6))
                .andExpect(status().isOk());
    }

    @Test
    public void testInValidParams() throws Exception {
        mockMvc.perform(get("/valid/test/{id}?count={count}", 123, 4))
                .andExpect(status().isBadRequest());
    }
}

在這個測試用例中,主要是基於Http Status Code來進行判斷計算結果的正確性。

小結

在本節中,主要測試了基於@Valid和@Validated兩種模式下的驗證邏輯,它們應用在不同場合,會丟擲不同的異常資訊,分別為:MethodArgumentNotValidException和javax.validation.ConstraintViolationException,前一個有Spring框架將其轉化為400的Bad Request,而後者則直接定位為500的內部錯誤。

對於後者的異常資訊,這裡定義了異常的捕獲邏輯,自行將其轉為一個400的Bad Request。也希望大家可以從中學習如何來使用自定義的異常處理邏輯。

相關文章