首頁 > 軟體

如何通過自定義spring invalidator註解校驗資料合法性

2022-07-04 14:01:45

自定義spring invalidator註解校驗資料合法性

在專案中經常會對使用者輸入的資料,或者外部匯入到系統的資料做合法性檢查。在spring boot框架的微服務中可以使用invalidator註解對資料做合法性,安全性校驗。

下面給一個樣例說明如何自定義註解實現校驗邏輯。

1、定義校驗屬性字串長度的註解

package com.elon.springbootdemo.manager.invalidator;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
/**
 * 屬性欄位長度校驗註解定義。
 * 
 * @author elon
 * @version 2018年9月19日
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = FieldLengthInvalidatorImpl.class)
@Documented
public @interface FieldLengthInvalidator {
    // 欄位支援的最大長度(字元數)
    int maxLength() default 50;
    // 校驗失敗後返回的錯誤資訊
    String message() default "";
    // 分組
    Class<?>[] groups() default {};
    // 負載
    Class<? extends Payload>[] payload() default {};
}

在定義註解時可宣告變數用於輔助校驗。上面的註解中定義了maxLength變數用於指定最大長度限制。變數可以設定預設值,使用註解時不傳引數,變數就使用預設值。

2、實現校驗邏輯,校驗失敗後返回錯誤提示

package com.elon.springbootdemo.manager.invalidator;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
/**
 * 欄位長度校驗實現類。
 * 
 * @author elon
 * @version 2018年9月19日
 */
public class FieldLengthInvalidatorImpl implements ConstraintValidator<FieldLengthInvalidator, String> {
    private int maxLength = 0;
    @Override
    public void initialize(FieldLengthInvalidator invalidator) {
        maxLength = invalidator.maxLength();
    }
    @Override
    public boolean isValid(String fieldValue, ConstraintValidatorContext context) {
        if (fieldValue.length() > maxLength) {
            context.disableDefaultConstraintViolation();
            context.buildConstraintViolationWithTemplate("物件屬性長度超過限制。").addConstraintViolation();
            // 校驗失敗返回false。返回true上游收集不到錯誤資訊。
            return false;
        }
        return true;
    }
}

3、在模型欄位屬性上增加校驗的註解

public class User
{
    private int userId = -1;
    @FieldLengthInvalidator(maxLength=10)
    private String name = "";
}

4、提供統一的校驗方法

package com.elon.springbootdemo.manager;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
/**
 * 有效性校驗管理類。對外提供統一的校驗呼叫介面。
 * @author elon
 * @version 2018年9月19日
 */
public class InvalidatorMgr {
    private InvalidatorMgr() {
        
    }
    
    /**
     * 獲取單例物件。
     * 
     * @return 單例物件
     */
    public static InvalidatorMgr instance() {
        return InvalidatorMgrBuilder.instance;
    }
    
    /**
     * 校驗模型所有屬性的有效性。
     * 
     * @param model 待校驗模型
     * @return 錯誤資訊列表
     */
    public <T> List<String> validate(T model) {
        
        ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
        Validator validator = validatorFactory.getValidator();
        Set<ConstraintViolation<T>> resultSet = validator.validate(model);
        
        List<String> messageList = new ArrayList<>();
        resultSet.forEach((r)->messageList.add(r.getMessage()));        
        return messageList;
    }
    
    /**
     * 單例構建器。
     * @author elon
     * @version 2018年9月19日
     */
    private static class InvalidatorMgrBuilder{
        private static InvalidatorMgr instance = new InvalidatorMgr();
    }
}

5、業務層呼叫校驗方法

        User user = new User();
        user.setName("ahskahskhqlwjqlwqlwhqlhwlqjwlqhwlhqwhqlwjjqlwl");
        List<String> messageList = InvalidatorMgr.instance().validate(user);
        System.out.println(messageList);

invalidator註解主要用於實現長度,範圍,非法字元等通用的規則校驗。不適合用於做業務邏輯的校驗,特定的業務校驗寫在業務層。 

springboot 引數驗證 validation

1、綜述

springboot提供了強大的基於註解的、開箱即用的驗證功能,這種基於bean validation的實現和 hibernate validator類似

2、依賴

建立springboot專案,包含以下依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency> 
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency> 
<dependency> 
    <groupId>com.h2database</groupId> 
    <artifactId>h2</artifactId>
    <version>1.4.197</version> 
    <scope>runtime</scope>
</dependency>

3、定義實體類

測試專案為了方便,直接用JPA,使用@NotBlank指定非空欄位,message是驗證觸發後返回的資訊,還有@Null、@NotNull、@NotBlank、@Email、@Max、@Min、@Size、@Negative、@DecimalMax、@DecimalMin、@Positive、@PositiveOrZero、@NegativeOrZero、@AssertTrue、@AssertFalse、@Future、@FutureOrPresent、@Past、@PastOrPresent、@Pattern

@Entity
public class User {
    
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;
     
    @NotBlank(message = "Name is mandatory")
    private String name;
     
    @NotBlank(message = "Email is mandatory")
    private String email;
    
    // standard constructors / setters / getters / toString    
    
}

建立JPA的repository定義增刪改查介面

@Repository
public interface UserRepository extends CrudRepository<User, Long> {}

4、建立rest controller

@RestController
public class UserController {
 
    @PostMapping("/users")
    ResponseEntity<String> addUser(@Valid @RequestBody User user) {
        // persisting the user
        return ResponseEntity.ok("User is valid");
    }
     
    // standard constructors / other methods
     
}

接收到的user物件新增了@Valid,當Spring Boot發現帶有@Valid註解的引數時,會自動引導預設的JSR 380驗證器驗證引數。當目標引數未能通過驗證時,Spring Boot將丟擲一個MethodArgumentNotValidException

5、實現ExceptionHandler

直接丟擲異常顯然是不合理的,大部分情況需要經過處理返回給前端更友好的提示資訊,通過@ExceptionHandler來處理丟擲的異常實現該功能

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MethodArgumentNotValidException.class)
public 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 errors;
}

MethodArgumentNotValidException作為上一步丟擲的異常,當springboot執行validition觸發時會呼叫此實現,該方法將每個無效欄位的名稱和驗證後錯誤訊息儲存在對映中,然後它將對映作為JSON表示形式傳送回使用者端進行進一步處理。

6、寫測試程式碼

使用springboot自帶的外掛進行測試rest controller,

@RunWith(SpringRunner.class) 
@WebMvcTest
@AutoConfigureMockMvc
public class UserControllerIntegrationTest {
 
    @MockBean
    private UserRepository userRepository;
     
    @Autowired
    UserController userController;
 
    @Autowired
    private MockMvc mockMvc;
 
    //...     
}

@WebMvcTest允許我們使用MockMvcRequestBuilders和MockMvcResultMatchers實現的一組靜態方法測試請求和響應。測試addUser()方法,在請求體中傳遞一個有效的User物件和一個無效的User物件。

@Test
public void whenPostRequestToUsersAndValidUser_thenCorrectResponse() throws Exception {
    MediaType textPlainUtf8 = new MediaType(MediaType.TEXT_PLAIN, Charset.forName("UTF-8"));
    String user = "{"name": "bob", "email" : "bob@domain.com"}";
    mockMvc.perform(MockMvcRequestBuilders.post("/users")
      .content(user)
      .contentType(MediaType.APPLICATION_JSON_UTF8))
      .andExpect(MockMvcResultMatchers.status().isOk())
      .andExpect(MockMvcResultMatchers.content()
        .contentType(textPlainUtf8));
}
 
@Test
public void whenPostRequestToUsersAndInValidUser_thenCorrectResponse() throws Exception {
    String user = "{"name": "", "email" : "bob@domain.com"}";
    mockMvc.perform(MockMvcRequestBuilders.post("/users")
      .content(user)
      .contentType(MediaType.APPLICATION_JSON_UTF8))
      .andExpect(MockMvcResultMatchers.status().isBadRequest())
      .andExpect(MockMvcResultMatchers.jsonPath("$.name", Is.is("Name is mandatory")))
      .andExpect(MockMvcResultMatchers.content()
        .contentType(MediaType.APPLICATION_JSON_UTF8));
    }
}

也可以使用postman或fiddler來測試REST controller API。

7、跑測試

@SpringBootApplication
public class Application {
     
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
     
    @Bean
    public CommandLineRunner run(UserRepository userRepository) throws Exception {
        return (String[] args) -> {
            User user1 = new User("Bob", "bob@domain.com");
            User user2 = new User("Jenny", "jenny@domain.com");
            userRepository.save(user1);
            userRepository.save(user2);
            userRepository.findAll().forEach(System.out::println);
        };
    }
}

如果用沒有使用者名稱或郵箱的資料傳送請求會收到返回的提示資訊

{
  "name":"Name is mandatory",
  "email":"Email is mandatory"
}

8、自定義註解

在進行引數驗證的時候,往往存在現有的約束註解不能滿足的情況,此時就需要我們自己定義validation註解了,下次介紹如何自己定義一個驗證註解。 

以上為個人經驗,希望能給大家一個參考,也希望大家多多支援it145.com。


IT145.com E-mail:sddin#qq.com