天天看點

Spring中的參數校驗前言基礎知識和依賴

文章目錄

  • 前言
  • 基礎知識和依賴
    • 相關依賴
    • 實體類
    • JSR提供的校驗注解
    • Hibernate Validator提供的校驗注解
    • 驗證請求體(RequestBody)
    • 驗證請求參數(Path Variables 和 Request Parameters)
    • 驗證 Service 中的方法
    • Validator 程式設計方式手動進行參數驗證
    • 自定義 Validator
    • 使用驗證組

前言

剛開始的時候除了Controller層接受的對象我是直接通過一些 Spring 提供好的注解來實作校驗比如@Valid、@NotNull 等等,在一些需要對參數做校驗的其他地方我都是通過手動程式設計if

else判斷的方式來實作。後面重構代碼發現有更好的方式來滿足我的需求,然後花了半天時間對這部分内容做了一個簡單的總結,希望可以對不了解這部分知識的朋友有幫助。

基礎知識和依賴

相關依賴

如果開發普通 Java 程式的的話,你需要可能需要像下面這樣依賴:

<dependency>
            <groupId>org.hibernate.validator</groupId>
            <artifactId>hibernate-validator</artifactId>
            <version>6.0.9.Final</version>
   </dependency>
   <dependency>
             <groupId>javax.el</groupId>
             <artifactId>javax.el-api</artifactId>
             <version>3.0.0</version>
     </dependency>
     <dependency>
            <groupId>org.glassfish.web</groupId>
            <artifactId>javax.el</artifactId>
            <version>2.2.6</version>
     </dependency>
           

使用 Spring Boot 程式的話隻需要spring-boot-starter-web 就夠了,它的子依賴包含了我們所需要的東西。除了這個依賴,下面的示範還用到了 lombok ,是以不要忘記添加上相關依賴。

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

實體類

下面這個是示例用到的實體類。

@Data
@AllArgsConstructor
@NoArgsConstructor
//hibernate對象懶加載,json序列化失敗,因為懶加載這個對象屬性隻是一個代理對象,如果json直接當作一個存在的屬性去序列化就會出現錯誤需要在實體類上加上,在controller 直接傳回對象會出現這個問題
@JsonIgnoreProperties(value={"hibernateLazyInitializer","handler","fieldHandler"})
public class Person {

    @NotNull(message = "classId 不能為空")
    private String classId;

    @Size(max = 33)
    @NotNull(message = "name 不能為空")
    private String name;

    @Pattern(regexp = "((^Man$|^Woman$|^UGM$))", message = "sex 值不在可選範圍")
    @NotNull(message = "sex 不能為空")
    private String sex;
    
    @Pattern(regexp = "^1(3|4|5|7|8)\\d{9}$",message = "手機号碼格式錯誤")
    @NotBlank(message = "手機号碼不能為空")
    private String phone;

    @Email(message = "email 格式不正确")
    @NotNull(message = "email 不能為空")
    private String email;

}
           

JSR提供的校驗注解

@Null   被注釋的元素必須為 null    
@NotNull    被注釋的元素必須不為 null    
@AssertTrue     被注釋的元素必須為 true    
@AssertFalse    被注釋的元素必須為 false    
@Min(value)     被注釋的元素必須是一個數字,其值必須大于等于指定的最小值    
@Max(value)     被注釋的元素必須是一個數字,其值必須小于等于指定的最大值    
@DecimalMin(value)  被注釋的元素必須是一個數字,其值必須大于等于指定的最小值    
@DecimalMax(value)  被注釋的元素必須是一個數字,其值必須小于等于指定的最大值    
@Size(max=, min=)   被注釋的元素的大小必須在指定的範圍内    
@Digits (integer, fraction)     被注釋的元素必須是一個數字,其值必須在可接受的範圍内    
@Past   被注釋的元素必須是一個過去的日期    
@Future     被注釋的元素必須是一個将來的日期    
@Pattern(regex=,flag=)  被注釋的元素必須符合指定的正規表達式
           

Hibernate Validator提供的校驗注解

@NotBlank(message =)   驗證字元串非null,且長度必須大于0    
@Email  被注釋的元素必須是電子郵箱位址    
@Length(min=,max=)  被注釋的字元串的大小必須在指定的範圍内    
@NotEmpty   被注釋的字元串的必須非空    
@Range(min=,max=,message=)  被注釋的元素必須在合适的範圍内
           

驗證請求體(RequestBody)

Controller:

我們在需要驗證的參數上加上了@Valid注解,如果驗證失敗,它将抛出MethodArgumentNotValidException。預設情況下,Spring會将此異常轉換為HTTP Status 400(錯誤請求)

@RestController
public class PersonController {

    @Autowired
    private PersonRepository repository;

    @PostMapping("save")
    public void savePerson(@RequestBody @Valid Person person) {
        repository.save(person);
    }
}
           

此時進行測試通路的結果

{
    	"id":1,
    	"name":"william",
    	"sex":"1", //sex沒有填寫Man
    	"phone":"17521080146",
    	"email":"[email protected]"
    }
           

輸出結果

{
    "timestamp": "2019-09-18T03:12:03.254+0000",
    "status": 400,
    "error": "Bad Request",
    "errors": [
        {
            "codes": [
                "Pattern.person.sex",
                "Pattern.sex",
                "Pattern.java.lang.String",
                "Pattern"
            ],
            "arguments": [
                {
                    "codes": [
                        "person.sex",
                        "sex"
                    ],
                    "arguments": null,
                    "defaultMessage": "sex",
                    "code": "sex"
                },
                [],
                {
                    "arguments": null,
                    "defaultMessage": "((^Man$|^Woman$|^UGM$))",
                    "codes": [
                        "((^Man$|^Woman$|^UGM$))"
                    ]
                }
            ],
            "defaultMessage": "sex 值不在可選範圍",
            "objectName": "person",
            "field": "sex",
            "rejectedValue": "1",
            "bindingFailure": false,
            "code": "Pattern"
        }
    ],
    "message": "Validation failed for object='person'. Error count: 1",
    "path": "/facial-recognition-view-assembler-service/save"
}

           

背景出錯資訊

2019-09-18 11:15:43.187  WARN 9841 --- [nio-8080-exec-2] .w.s.m.s.DefaultHandlerExceptionResolver : 
Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for 
argument [0] in public void
           

是不是感覺輸出的資訊很亂,這時候我們需要一個全局捕獲異常

ExceptionHandler:

自定義異常處理器可以幫助我們捕獲異常,并進行一些簡單的處理。如果對于下面的處理異常的代碼不太了解的話

@ControllerAdvice(assignableTypes = {PersonController.class})
public class GlobalExceptionHandler {
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, String>> handleValidationExceptions(
            MethodArgumentNotValidException ex) {
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getAllErrors().forEach((error) -> {
            String fieldName = ((FieldError) error).getField();
            String errorMessage = error.getDefaultMessage();
            errors.put(fieldName, errorMessage);
        });
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errors);
    }
}
           

加完後的輸出結果

{
    "phone": "手機号碼格式錯誤"
}
           

通過測試驗證:

下面我通過 MockMvc 模拟請求 Controller 的方式來驗證是否生效,當然你也可以通過 Postman 這種工具來驗證。

我們試一下所有參數輸入正确的情況。

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class PersonControllerTest {
    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @Test
    public void should_get_person_correctly() throws Exception {
        Person person = new Person();
        person.setName("SnailClimb");
        person.setSex("Man");
        person.setClassId("82938390");
        person.setEmail("[email protected]");

        mockMvc.perform(post("/api/person")
                .contentType(MediaType.APPLICATION_JSON_UTF8)
                .content(objectMapper.writeValueAsString(person)))
                .andExpect(MockMvcResultMatchers.jsonPath("name").value("SnailClimb"))
                .andExpect(MockMvcResultMatchers.jsonPath("classId").value("82938390"))
                .andExpect(MockMvcResultMatchers.jsonPath("sex").value("Man"))
                .andExpect(MockMvcResultMatchers.jsonPath("email").value("[email protected]"));
    }
}
           

驗證出現參數不合法的情況抛出異常并且可以正确被捕獲。

@Test
    public void should_check_person_value() throws Exception {
        Person person = new Person();
        person.setSex("Man22");
        person.setClassId("82938390");
        person.setEmail("SnailClimb");

        mockMvc.perform(post("/api/person")
                .contentType(MediaType.APPLICATION_JSON_UTF8)
                .content(objectMapper.writeValueAsString(person)))
                .andExpect(MockMvcResultMatchers.jsonPath("sex").value("sex 值不在可選範圍"))
                .andExpect(MockMvcResultMatchers.jsonPath("name").value("name 不能為空"))
                .andExpect(MockMvcResultMatchers.jsonPath("email").value("email 格式不正确"));
    }
           

使用 Postman 驗證結果如下

Spring中的參數校驗前言基礎知識和依賴

驗證請求參數(Path Variables 和 Request Parameters)

Controller:類似Path Variables和Request Parameters一定一定不要忘記在類上加上 Validated 注解了,這個參數可以告訴 Spring 去校驗方法參數。

@RestController
@Validated
public class PersonController {

    @GetMapping("find/{id}")
    public Person savePerson(@Valid @PathVariable("id") @Max(value = 2, message = "超過範圍啦") Long id) {
        Person one = repository.getOne(id);
        return one;
        
    }

  @PutMapping("/person")
    public String getPersonByName(@Valid @RequestParam("name") @Size(max = 6, message = "超過 name 的範圍了") String name) {
        return null;
    }

           

報錯的輸出資訊

{
    "isSuccess": false,
    "error": {
        "code": null,
        "message": "savePerson.id: 超過範圍啦"
    }
}
           

ExceptionHandler:

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

輸出結果

savePerson.id: 超過範圍啦
           

通過測試驗證:

@Test
    public void should_check_param_value() throws Exception {

        mockMvc.perform(get("/api/person/6")
                .contentType(MediaType.APPLICATION_JSON_UTF8))
                .andExpect(status().isBadRequest())
                .andExpect(content().string("getPersonByID.id: 超過 id 的範圍了"));
    }

    @Test
    public void should_check_param_value2() throws Exception {

        mockMvc.perform(put("/api/person")
                .param("name","snailclimbsnailclimb")
                .contentType(MediaType.APPLICATION_JSON_UTF8))
                .andExpect(status().isBadRequest())
                .andExpect(content().string("getPersonByName.name: 超過 name 的範圍了"));
    }
           

驗證 Service 中的方法

我們還可以驗證任何Spring元件的輸入,而不是驗證控制器級别的輸入,我們可以使用@Validated和@Valid注釋的組合來實作這一需求。

一定一定不要忘記在類上加上 Validated 注解了,這個參數可以告訴 Spring 去校驗方法參數。

@Service
@Validated
public class PersonService {

    public void validatePerson(@Valid Person person){
        // do something
    }
}
           

通過測試驗證

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class PersonServiceTest {
    @Autowired
    private PersonService service;

    @Test(expected = ConstraintViolationException.class)
    public void should_throw_exception_when_person_is_not_valid() {
        Person person = new Person();
        person.setSex("Man22");
        person.setClassId("82938390");
        person.setEmail("SnailClimb");
        service.validatePerson(person);
    }

}
           

Validator 程式設計方式手動進行參數驗證

某些場景下可能會需要我們手動校驗并獲得校驗結果。

@Test
    public void check_person_manually() {
        StringBuffer stringBuffer = new StringBuffer();
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        Validator validator = factory.getValidator();
        Person person = new Person();
        person.setSex("Man22");
        person.setClassId("82938390");
        person.setEmail("SnailClimb");
        //傳回值是不符合注解規範的資訊
        //比如:ConstraintViolationImpl{interpolatedMessage='不能為空', propertyPath=visualId, rootBeanClass=class com.wbie.wdpro.frvas.model.checkQuest, messageTemplate='{javax.validation.constraints.NotEmpty.message}'}
        Set<ConstraintViolation<Person>> violations = validator.validate(person);
   constraintViolations.forEach(item -> {
            //這行代碼可以參考下面的圖檔
            String annotationType = item.getConstraintDescriptor().getAnnotation().annotationType().getName();
            if (stringBuffer.length() > 0) {
                stringBuffer.append(", ");
            }
            if (annotationType.equals(ANNOTATION_NOTNULL)) {
                stringBuffer.append(item.getPropertyPath().toString() + ": should be not empty");
            }
        });
        return stringBuffer.toString();
    }
           
Spring中的參數校驗前言基礎知識和依賴

上面我們是通過 Validator 工廠類獲得的 Validator 示例,當然你也可以通過 @Autowired 直接注入的方式。但是在非 Spring Component 類中使用這種方式的話,隻能通過工廠類來獲得 Validator。

@Autowired
Validator validate
           

自定義 Validator

如果自帶的校驗注解無法滿足你的需求的話,你還可以自定義實作注解。比如我們的Person類多了一個 region 字段,region 字段隻能是China、China-Taiwan、China-HongKong這三個中的一個。

第一步你需要建立一個注解:

@Target({FIELD})
@Retention(RUNTIME)
@Constraint(validatedBy = RegionValidator.class)
@Documented
public @interface Region {

    String message() default "Region 值不在可選範圍内";

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

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

第二步你需要實作 ConstraintValidator接口,并重寫isValid 方法

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

public class RegionValidator implements ConstraintValidator<Region, String> {

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        HashSet<Object> regions = new HashSet<>();
        regions.add("China");
        regions.add("China-Taiwan");
        regions.add("China-HongKong");
        return regions.contains(value);
    }
}

           

現在你就可以使用這個注解:

@Region
    private String region;
           

使用驗證組

很多時候我們需要使用到驗證組,這樣說可能不太清楚,說簡單點就是對對象操作的不同方法有不同的驗證規則,示例如下。

先建立兩個接口:

public interface AddPersonGroup {
    }
    public interface DeletePersonGroup {
    }
           

我們可以這樣去使用驗證組

@NotNull(groups = DeletePersonGroup.class)
@Null(groups = AddPersonGroup.class)
private String group;
           
@Service
@Validated
public class PersonService {

    public void validatePerson(@Valid Person person) {
        // do something
    }

    @Validated(AddPersonGroup.class)
    public void validatePersonGroupForAdd(@Valid Person person) {
        // do something
    }

    @Validated(DeletePersonGroup.class)
    public void validatePersonGroupForDelete(@Valid Person person) {
        // do something
    }

}
           

通過測試驗證:

@Test(expected = ConstraintViolationException.class)
    public void should_check_person_with_groups() {
        Person person = new Person();
        person.setSex("Man22");
        person.setClassId("82938390");
        person.setEmail("SnailClimb");
        person.setGroup("group1");
        service.validatePersonGroupForAdd(person);
    }
           
@Test(expected = ConstraintViolationException.class)
    public void should_check_person_with_groups2() {
        Person person = new Person();
        person.setSex("Man22");
        person.setClassId("82938390");
        person.setEmail("SnailClimb");
        service.validatePersonGroupForDelete(person);
    }
           

使用驗證組這種方式的時候一定要小心,這是一種反模式,還會造成代碼邏輯性變差。