Laravel 的驗證邏輯通常寫在獨立的 FormRequest 類別裡:
class CreateLinkRequest extends FormRequest
{
public function rules(): array
{
return [
'url' => ['required', 'url'],
];
}
}
// Controller
public function store(CreateLinkRequest $request)
{
$validated = $request->validated();
// 驗證已經跑完,這裡拿到的一定是合法資料
}
關鍵在於:光是把參數型別寫成 CreateLinkRequest,驗證就會自動觸發——Laravel 的 Service Container 解析這個型別時,會先跑 rules() 檢查,驗證失敗直接短路,Controller 方法本體根本不會被執行,還會自動回傳 422 跟結構化的錯誤訊息,整個過程不用寫任何一行額外的程式碼。
Spring Boot 沒有獨立的「Form Request」類別,驗證規則直接標在 DTO 的欄位上:
record CreateLinkRequest(
@NotBlank(message = "url 不能為空")
@URL(message = "url 格式不正確")
String url
) {}
Controller 這邊要明確加上 @Valid,驗證才會真的被觸發:
@PostMapping
ResponseEntity<LinkResponse> store(@Valid @RequestBody CreateLinkRequest request) {
// 驗證通過才會執行到這裡
...
}
這裡有兩個容易踩的雷:
@NotBlank、@URL 這些驗證註解不是 Spring 內建的,要在 pom.xml 額外加 spring-boot-starter-validation 依賴才能用(回顧 Day03:Maven 的依賴要自己宣告,不像 PHP 擴充套件很多是隨語言內建)@Valid 是最常見的雷——欄位上的驗證註解寫了,但 Controller 參數沒加 @Valid,驗證直接被跳過,程式碼看起來完全沒問題,卻悄悄失去保護另外,@URL 嚴格來說不是 Jakarta Bean Validation 規範本身的註解,是 Hibernate Validator(Spring Boot 預設用的 Bean Validation 實作)額外提供的擴充驗證,這也是為什麼查官方 Jakarta 規範文件會找不到它,要查 Hibernate Validator 的文件才有。
Laravel 驗證失敗,框架自動回一個整理好的 422:
{
"message": "The url field is required.",
"errors": {
"url": ["The url field is required."]
}
}
Spring Boot 目前(還沒處理過)丟出的是 MethodArgumentNotValidException,預設的錯誤回應長這樣,明顯比較原始、內部細節外露:
{
"timestamp": "2026-09-20T10:00:00.000+00:00",
"status": 400,
"errors": [
{
"field": "url",
"defaultMessage": "url 不能為空"
}
]
}
這個「原始版本」先讓大家看一次,是刻意的——Spring Boot 預設不會像 Laravel 一樣自動幫你把錯誤回應包得漂漂亮亮,要透過 @ControllerAdvice + @ExceptionHandler 自己攔截、自己整理格式,這部分完整會在 Day10(例外處理與統一錯誤回應) 展開,這裡先知道「預設長這樣、之後會被整理」就好。
如果內建的驗證規則不夠用(例如要驗證「這個短碼有沒有被使用過」),Laravel 用一個 Rule 類別就能搞定:
class UniqueLinkCode implements ValidationRule
{
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if (Link::where('code', $value)->exists()) {
$fail('這個短碼已經被使用過了');
}
}
}
Bean Validation 要寫兩個東西才能做到同樣的事:一個自訂註解 + 一個實作驗證邏輯的 Validator 類別:
@Constraint(validatedBy = UniqueLinkCodeValidator.class)
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface UniqueLinkCode {
String message() default "這個短碼已經被使用過了";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
class UniqueLinkCodeValidator implements ConstraintValidator<UniqueLinkCode, String> {
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
// 查資料庫邏輯,Day07 接上 Repository 後才能真的實作
return true;
}
}
明顯比 Laravel 囉唆不少——但換來的好處是這個自訂驗證規則變成一個「型別」,可以像 @NotBlank 一樣直接標在任何欄位上重複使用,而且編譯期就能檢查有沒有標錯地方,這是 Laravel 的字串 Rule 名稱做不到的。