1. 概述
本文檔介紹 Spring Data REST 驗證器的基本介紹。如果您需要先回顧 Spring Data REST 的基本知識,請務必訪問本文檔以鞏固基礎知識。
簡單來説,通過 Spring Data REST,我們可以通過 REST API 簡單地向數據庫添加新的條目,但我們當然也需要確保數據在實際持久化之前是有效的。
本文檔延續了之前的文章,我們將重用我們在那裏設置的現有項目。
如果您正在尋找如何首先 開始使用 Spring Data REST – 這裏提供了一種快速上手的方法:
2. Using Validators
從 Spring 3 開始,框架包含 Validator 接口 – 可以用於驗證對象。
2.1. 動機
在上一篇文章中,我們定義了實體有兩個屬性 – name 和 email。
因此,為了創建一個新的資源,我們只需要運行:
curl -i -X POST -H "Content-Type:application/json" -d
'{ "name" : "Test", "email" : "[email protected]" }'
http://localhost:8080/users
這個 POST 請求會將提供的 JSON 對象保存到我們的數據庫,並且操作將返回:
{
"name" : "Test",
"email" : "[email protected]",
"_links" : {
"self" : {
"href" : "http://localhost:8080/users/1"
},
"websiteUser" : {
"href" : "http://localhost:8080/users/1"
}
}
}
一個積極的結果被期望,因為我們提供了有效數據。但是,如果我們將屬性 name 刪除,或者將其值設置為空字符串 String,會發生什麼?
為了測試第一個場景,我們將運行之前修改後的命令,其中空字符串設置為屬性 name 的值:
curl -i -X POST -H "Content-Type:application/json" -d
'{ "name" : "", "email" : "Baggins" }' http://localhost:8080/users
使用這個命令,我們會得到以下響應:
{
"name" : "",
"email" : "Baggins",
"_links" : {
"self" : {
"href" : "http://localhost:8080/users/1"
},
"websiteUser" : {
"href" : "http://localhost:8080/users/1"
}
}
}
對於第二個場景,我們將從請求中刪除屬性 name:
curl -i -X POST -H "Content-Type:application/json" -d
'{ "email" : "Baggins" }' http://localhost:8080/users
對於這個命令,我們會得到這個響應:
{
"name" : null,
"email" : "Baggins",
"_links" : {
"self" : {
"href" : "http://localhost:8080/users/2"
},
"websiteUser" : {
"href" : "http://localhost:8080/users/2"
}
}
}
正如我們所見,這兩個請求都是有效的,並且我們可以確認,通過 201 狀態碼和 API 鏈接到我們的對象。
這種行為不可接受,因為我們想要避免將部分數據插入到數據庫中。
2.2. Spring Data REST 事件
在對 Spring Data REST API 的每次調用期間,Spring Data REST 導出器會生成各種事件,列出如下:
- BeforeCreateEvent
- AfterCreateEvent
- BeforeSaveEvent
- AfterSaveEvent
- BeforeLinkSaveEvent
- AfterLinkSaveEvent
- BeforeDeleteEvent
- AfterDeleteEvent
由於所有事件都以類似的方式處理,我們將只展示如何處理 beforeCreateEvent,該事件在新的對象保存到數據庫之前生成。
2.3. 定義 Validator
為了創建我們自己的驗證器,我們需要實現 org.springframework.validation.Validator 接口,並使用 supports 和 validate 方法。
Supports 檢查驗證器是否支持提供的請求,而 validate 方法驗證請求中的數據。
讓我們定義一個 WebsiteUserValidator 類:
public class WebsiteUserValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return WebsiteUser.class.equals(clazz);
}
@Override
public void validate(Object obj, Errors errors) {
WebsiteUser user = (WebsiteUser) obj;
if (checkInputString(user.getName())) {
errors.rejectValue("name", "name.empty");
}
if (checkInputString(user.getEmail())) {
errors.rejectValue("email", "email.empty");
}
}
private boolean checkInputString(String input) {
return (input == null || input.trim().length() == 0);
}
}
Errors 對象是一個特殊類,旨在包含 validate 方法中提供的所有錯誤。稍後,在本文中,我們將展示如何使用 Errors 對象中包含的提供的消息。
為了向錯誤消息添加,我們必須調用 errors.rejectValue(nameOfField, errorMessage)。
在定義驗證器之後,我們需要將其映射到特定的事件,該事件在請求被接受後生成。
例如,在我們的案例中,beforeCreateEvent 在新的對象保存到數據庫之前生成。但是,由於我們想要驗證請求中的對象,因此首先需要定義我們的驗證器。
這可以通過三種方式實現:
- 添加 Component 註解,名稱為“beforeCreateWebsiteUserValidator”。 Spring Boot 將識別前綴 beforeCreate,該前綴確定我們要捕獲的事件,並且還將識別 WebsiteUser 類來自 Component 名稱。
@Component("beforeCreateWebsiteUserValidator") public class WebsiteUserValidator implements Validator { ... } - 創建一個 Bean 在 Application Context 中,帶有 @Bean 註解:
@Bean public WebsiteUserValidator beforeCreateWebsiteUserValidator() { return new WebsiteUserValidator(); } - 手動註冊:
@SpringBootApplication public class SpringDataRestApplication implements RepositoryRestConfigurer { public static void main(String[] args) { SpringApplication.run(SpringDataRestApplication.class, args); } @Override public void configureValidatingRepositoryEventListener( ValidatingRepositoryEventListener v) { v.addValidator(p -> p.getKey().startsWith("beforeCreate"), new WebsiteUserValidator()); } }- 對於此案例,您不需要在 WebsiteUserValidator 類上添加任何註釋。
2.4. 事件發現 Bug
目前,Spring Data REST 中存在一個 bug – 影響事件發現。
如果調用生成 beforeCreate 事件的 POST 請求,我們的應用程序將不會調用驗證器,因為事件將不會被發現,由於此 bug。
對於此問題的簡單解決方法是在 Spring Data REST 的 ValidatingRepositoryEventListener 類中插入所有事件。
@Configuration
public class ValidatorEventRegister implements InitializingBean {
@Autowired
ValidatingRepositoryEventListener validatingRepositoryEventListener;
@Autowired
private Map<String, Validator> validators;
@Override
public void afterPropertiesSet() throws Exception {
List<String> events = Arrays.asList("beforeCreate");
for (Map.Entry<String, Validator> entry : validators.entrySet()) {
events.stream()
.filter(p -> entry.getKey().startsWith(p))
.findFirst()
.ifPresent(
p -> validatingRepositoryEventListener
.addValidator(p, entry.getValue()));
}
}
}
3. 測試
在 第 2.1 節 中,我們展示了,如果沒有驗證器,我們可以將沒有名稱屬性的對象添加到我們的數據庫中,這並不是我們期望的行為,因為我們不檢查數據完整性。
如果我們想添加相同對象,但沒有 name 屬性,並且提供了驗證器,我們會得到以下錯誤:
curl -i -X POST -H "Content-Type:application/json" -d
'{ "email" : "[email protected]" }' http://localhost:8080/users
{
"timestamp":1472510818701,
"status":406,
"error":"Not Acceptable",
"exception":"org.springframework.data.rest.core.
RepositoryConstraintViolationException",
"message":"Validation failed",
"path":"/users"
}
正如我們所見,請求中缺少的數據被檢測到,對象沒有被保存到數據庫中。我們的請求被返回了 500 HTTP 狀態碼和內部錯誤的提示信息。
錯誤信息並沒有説明請求中的問題。如果我們想讓它更具信息量,我們需要修改響應對象。
在 Spring 框架中的異常處理文章中,我們展示瞭如何處理由框架生成的異常,所以這絕對是一個值得一讀的地方。
由於我們的應用程序生成了 RepositoryConstraintViolationException 異常,我們將創建一個處理程序來修改響應消息。
這是我們的 RestResponseEntityExceptionHandle 類的代碼:
@ControllerAdvice
public class RestResponseEntityExceptionHandler extends
ResponseEntityExceptionHandler {
@ExceptionHandler({ RepositoryConstraintViolationException.class })
public ResponseEntity<Object> handleAccessDeniedException(
Exception ex, WebRequest request) {
RepositoryConstraintViolationException nevEx =
(RepositoryConstraintViolationException) ex;
String errors = nevEx.getErrors().getAllErrors().stream()
.map(p -> p.toString()).collect(Collectors.joining("\n"));
return new ResponseEntity<Object>(errors, new HttpHeaders(),
HttpStatus.PARTIAL_CONTENT);
}
}
有了這個自定義處理程序,我們的返回對象將包含所有檢測到的錯誤信息。
4. 結論
在本文中,我們展示了驗證器對於每個 Spring Data REST API 的重要性,它為數據插入提供了一層額外的安全保障。
我們還説明瞭如何輕鬆創建新的驗證器,使用註解。