網頁

2020/11/8

Spring Boot @Validated 參數分組驗證 bean validation by group

Spring Boot使用@Validated檢核參數的範例如下。

範例環境:

  • Spring Boot 2.3.2.RELEASE
  • Maven
  • Java Bean Validation
  • Lombok

Spring Boot使用Java Bean Validation要在pom.xml加入以下依賴。

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

參考pom.xml


一般在Controller檢核傳入的請求參數時,可使用Java Bean Validation javax.validation.constraints中的各種檢核annotation或自訂檢核annotation來驗證,並在方法的參數前加上@Valid以支持檢核,但有時不同方法傳入的參數是同個bean而需要被檢核的屬性確不同,此時就可利用Spring的@Validated對檢核進行分組(group)依不同情況進行檢核。

@Validated透過設定class來標注constraint annotation要檢核的group對象。下面範例設定了兩個用來group的Group1Group2。因為僅用來標示group檢核的對象,所以不需要實作內容。

Group1

public interface Group1 {
}

Group2

public interface Group2 {
}

下面DemoControllervalid()validGroup1()validGroup2()方法皆負責處理的DemoRequest請求參數。

在方法上使用@Validated的類別名稱前也要加上@Validated才有效果。

valid()沒有@Validated,僅檢核DemoRequest中無設定group的屬性;
validGroup1()@Validated(Group1.class)則檢核DemoRequest中constraint annotation無設定groupgroup = Group1.class的屬性;
validGroup2()@Validated(Group2.class)則檢核DemoRequest中constraint annotation無設定groupgroup = Group2.class的屬性。

DemoController

package com.abc.demo.controller;

import com.abc.demo.controller.request.DemoRequest;
import com.abc.demo.validation.group.Group1;
import com.abc.demo.validation.group.Group2;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import javax.validation.ConstraintViolationException;
import javax.validation.Valid;

@Validated
@RestController
public class DemoController {

    @PostMapping("/valid")
    public void valid(@RequestBody @Valid DemoRequest request) {
    }

    @Validated(Group1.class)
    @PostMapping("/valid/group1")
    public void validGroup1(@RequestBody @Valid DemoRequest request) {
    }

    @Validated(Group2.class)
    @PostMapping("/valid/group2")
    public void validGroup2(@RequestBody @Valid DemoRequest request) {
    }

    @ExceptionHandler(ConstraintViolationException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    ResponseEntity<String> handleConstraintViolationException(ConstraintViolationException e) {
        return ResponseEntity.badRequest().body(e.getMessage());
    }
}


DemoRequest為以上方法傳入的參數bean。
@Positive沒設定group,所以只要方法參數前有@Valid就需要被檢核;
@Emailgroup = Group1.class,方法前有@Validated(Group1.class)才會被檢核;
@Mingroup = Group2.class,方法前有@Validated(Group2.class)才會被檢。

DemoRequest

package com.abc.demo.controller.request;

import com.abc.demo.validation.group.Group1;
import com.abc.demo.validation.group.Group2;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.validation.constraints.Email;
import javax.validation.constraints.Min;
import javax.validation.constraints.Positive;

@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class DemoRequest {

    @Positive(message = "id必須大於0")
    private long id;

    @Email(message = "信箱格式錯誤",
            groups = Group1.class)
    private String email;

    @Min(value = 18, message = "年記不可小於18歲",
            groups = Group2.class)
    private int age;

}

使用DemoControllerTests測試各個方法通過及不通過檢核的情況。

DemoControllerTests

package com.abc.demo.controller;

import com.abc.demo.controller.request.DemoRequest;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;

@WebMvcTest
class DemoControllerTests {

    @Autowired
    private MockMvc mockMvc;

    /**
     * 只檢核@Positive
     *     id大於0。通過
     */
    @Test
    void valid_pass() throws Exception {

        DemoRequest request = DemoRequest.builder()
                .id(1L) // id大於0
                .email("abc.com") // 信箱格式錯誤
                .age(7) // 年齡小於18歲
                .build();

        String json = new ObjectMapper().writeValueAsString(request);

        mockMvc.perform(
                MockMvcRequestBuilders.post("/valid")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(json))
                .andExpect(MockMvcResultMatchers.status().isOk());

    }

    /**
     * 只檢核@Positive
     *     id小於0。不通過
     */
    @Test
    void valid_notPass() throws Exception {

        DemoRequest request = DemoRequest.builder()
                .id(-1L) // id小於0
                .email("abc.com") // 信箱格式錯誤
                .age(7) // 年齡小於18歲
                .build();

        String json = new ObjectMapper().writeValueAsString(request);

        mockMvc.perform(
                MockMvcRequestBuilders.post("/valid")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(json))
                .andExpect(MockMvcResultMatchers.status().isBadRequest());

    }

    /**
     * 檢核@@Positive,@Email
     *     id大於0,信箱格式正確。通過
     */
    @Test
    void validGroup1_pass() throws Exception {

        DemoRequest request = DemoRequest.builder()
                .id(1L) // id大於0
                .email("john@abc.com") // 信箱格式正確
                .age(7) // 年齡小於18歲
                .build();

        String json = new ObjectMapper().writeValueAsString(request);

        mockMvc.perform(
                MockMvcRequestBuilders.post("/valid/group1")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(json))
                .andExpect(MockMvcResultMatchers.status().isOk());

    }

    /**
     * 檢核@@Positive,@Email
     *     id大於0,信箱格式錯誤。通過
     */
    @Test
    void validGroup1_notPass() throws Exception {

        DemoRequest request = DemoRequest.builder()
                .id(1L) // id大於0
                .email("abc.com") // 信箱格式錯誤
                .age(7) // 年齡小於18歲
                .build();

        String json = new ObjectMapper().writeValueAsString(request);

        mockMvc.perform(
                MockMvcRequestBuilders.post("/valid/group1")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(json))
                .andExpect(MockMvcResultMatchers.status().isBadRequest());

    }

    /**
     * 檢核@@Positive,@Min
     *     id大於0,年齡大於18歲。通過
     */
    @Test
    void validGroup2_pass() throws Exception {

        DemoRequest request = DemoRequest.builder()
                .id(1L) // id大於0
                .email("abc.com") // 信箱格式錯誤
                .age(21) // 年齡大於18歲
                .build();

        String json = new ObjectMapper().writeValueAsString(request);

        mockMvc.perform(
                MockMvcRequestBuilders.post("/valid/group2")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(json))
                .andExpect(MockMvcResultMatchers.status().isOk());

    }

    /**
     * 檢核@@Positive,@Min
     *     id大於0,年齡小於18歲。不通過
     */
    @Test
    void validGroup2_notPass() throws Exception {

        DemoRequest request = DemoRequest.builder()
                .id(1L) // id大於0
                .email("abc.com") // 信箱格式錯誤
                .age(7) // 年齡小於18歲
                .build();

        String json = new ObjectMapper().writeValueAsString(request);

        mockMvc.perform(
                MockMvcRequestBuilders.post("/valid/group2")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(json))
                .andExpect(MockMvcResultMatchers.status().isBadRequest());

    }

}

@Validated也可用在service或dao層,也要與@Valid搭配使用,但由於@Valid僅在Controller有檢核效果,所以行為不太一樣。請參考DemoService與測試DemoServiceTests


參考github


沒有留言:

張貼留言