我曾多次被 JSR 380 搞得焦頭爛額,因此很欣賞一個不需要拖入整個依賴生態的驗證框架。Solon 的驗證系統位於 solon-security-validation — 單一外掛提供 20 多個註解、實體驗證及自訂驗證器,且完全不引入 javax.validation 或 jakarta.validation。
以下是我覺得實用的功能介紹。
兩階段驗證模型
Solon 將驗證分為兩個階段:
- 上下文驗證(注入前)— 在方法參數解析前執行。方法上的註解可檢查標頭、Cookie、IP 或請求層級的限制條件。
- 參數驗證(注入後)— 在參數解析後執行。個別參數或實體欄位上的註解。
這對應兩種不同的實作機制:
- 上下文驗證在內部使用
@Addition(Filter.class) - 參數驗證使用
@Around(Interceptor.class)
大多數時候你不需要在意這些細節,但這解釋了為什麼有些註解放在方法上,有些放在參數上。
入門:控制器上的 @valid
在控制器類別(或其基底類別)上加入 @Valid,然後在參數上加上註解:
@Valid
@Controller
public class UserController {
@Mapping("/user/add")
public void addUser(
@NotNull String name,
@Email String email,
@Pattern("^https?://") String avatarUrl,
@Length(min = 6, max = 20) String password) {
// All parameters validated before this runs
}
}
Enter fullscreen mode Exit fullscreen mode
就是這樣。不需要設定 ValidatorFactory 或宣告 bean。只要在類別上加上 @Valid 就能啟用所有處理方法的驗證。
用於實體(巢狀)驗證的 @Validated
當參數是帶有自身驗證規則的 DTO 時,請使用 @Validated:
@Valid
@Controller
public class UserController {
@Mapping("/user/register")
public void register(@Validated RegisterRequest req) {
// req fields are validated
}
}
@Data
public class RegisterRequest {
@NotNull
@Length(min = 2, max = 50)
private String name;
@Email
@NotNull
private String email;
@Validated // Nested entity validation
@NotNull
@Size(min = 1)
private List<Order> orderList;
}
Enter fullscreen mode Exit fullscreen mode
分組驗證
針對更新與建立的不同情境,可使用分組:
public interface UpdateGroup {}
@Data
public class User {
@NotNull(groups = UpdateGroup.class) // Only required on update
private Long id;
@NotNull
private String name;
}
// Controller
@Valid
@Controller
public class UserController {
@Mapping("/user/update")
public void update(@Validated(UpdateGroup.class) User user) {
// Only validates fields in UpdateGroup
}
}
Enter fullscreen mode Exit fullscreen mode
20 多個註解一覽表
Solon 的驗證外掛內建豐富的註解集合。完整清單如下:
| 註解 | 作用範圍 | 用途 |
|---|---|---|
@Valid |
控制器類別 | 啟用驗證 |
@Validated |
參數/欄位 | 驗證實體欄位 |
@NotNull |
方法/參數/欄位 | 不可為 null |
@Null |
方法/參數/欄位 | 必須為 null |
@NotBlank |
方法/參數/欄位 | 非空白字串 |
@NotEmpty |
方法/參數/欄位 | 非空字串 |
@NotZero |
方法/參數/欄位 | 不可為零 |
@Min(value) |
參數/欄位 | >= 指定值 |
@Max(value) |
參數/欄位 | <= 指定值 |
@DecimalMin(value) |
參數/欄位 | >= 小數值 |
@DecimalMax(value) |
參數/欄位 | <= 小數值 |
@Length(min, max) |
參數/欄位 | 字串長度範圍 |
@Size |
參數/欄位 | 集合大小範圍 |
@Email |
參數/欄位 | 電子郵件格式 |
@Pattern(value) |
參數/欄位 | 正規表示式比對 |
@Date |
參數/欄位 | 日期格式 |
@Numeric |
參數/欄位 | 數值格式 |
@Logined |
控制器/方法 | 使用者已登入 |
@NoRepeatSubmit |
控制器/方法 | 防止重複提交 |
@Whitelist |
控制器/方法 | IP 在白名單中 |
@NotBlacklist |
控制器/方法 | IP 不在黑名單中 |
其中幾個(@Logined、@NoRepeatSubmit、@Whitelist、@NotBlacklist)屬於「業務感知」型註解 — 需要你實作檢查器介面。詳情見下文。
非 Web 元件上的驗證
這點讓我驚訝:Solon 的驗證也能在普通的 @Component 類別上運作,而不僅限於控制器:
@Valid
@Component
public class UserService {
public void addUser(@NotNull String name, @Email String email) {
// Validation works here too
}
}
Enter fullscreen mode Exit fullscreen mode
使用 ValidUtils 進行手動驗證
你也可以在任何地方以程式方式驗證:
User user = new User();
user.setName(null);
ValidUtils.validateEntity(user); // throws ValidatorException if invalid
Enter fullscreen mode Exit fullscreen mode
處理驗證錯誤
在過濾器中捕捉 ValidatorException:
@Component
public class ValidationFilter implements Filter {
@Override
public void doFilter(Context ctx, FilterChain chain) throws Throwable {
try {
chain.doFilter(ctx);
} catch (ValidatorException e) {
ctx.render(Result.failure(e.getCode(), e.getMessage()));
}
}
}
Enter fullscreen mode Exit fullscreen mode
預設情況下,驗證遇到第一個錯誤就會停止。若要收集所有錯誤,請設定:
solon.validation.validateAll: true
Enter fullscreen mode Exit fullscreen mode
業務感知驗證器(四個檢查器)
四個註解需要你提供檢查器實作。這正是 Solon 設計的亮點 — 框架處理攔截,你只需插入業務邏輯。
@NoRepeatSubmit
@Component
public class NoRepeatSubmitCheckerImpl implements NoRepeatSubmitChecker {
@Override
public boolean check(NoRepeatSubmit anno, Context ctx,
String submitHash, int limitSeconds) {
return LockUtils.tryLock(Solon.cfg().appName(), submitHash, limitSeconds);
}
}
Enter fullscreen mode Exit fullscreen mode
@Whitelist
@Component
public class WhitelistCheckerImpl implements WhitelistChecker {
@Override
public boolean check(Whitelist anno, Context ctx) {
String ip = ctx.realIp();
return CloudClient.list().inListOfIp("whitelist", ip);
}
Enter fullscreen mode Exit fullscreen mode
@NotBlacklist
@Component
public class NotBlacklistCheckerImpl implements NotBlacklistChecker {
@Override
public boolean check(NotBlacklist anno, Context ctx) {
String ip = ctx.realIp();
return !CloudClient.list().inListOfIp("blacklist", ip);
}
Enter fullscreen mode Exit fullscreen mode
@Logined
@Component
public class LoginedCheckerImpl implements LoginedChecker {
@Override
public boolean check(Logined anno, Context ctx, String userKeyName) {
return ctx.sessionAsLong("userId") > 0;
}
Enter fullscreen mode Exit fullscreen mode
自訂驗證器:建立自己的註解
此框架設計為可擴充。以下是建立自訂 @Phone 驗證器的完整範例:
步驟 1:定義註解
@Target({ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Phone {
String message() default "Invalid phone number";
Class<?>[] groups() default {};
}
Enter fullscreen mode Exit fullscreen mode
步驟 2:實作驗證器
public class PhoneValidator implements Validator<Phone> {
private static final Pattern PHONE_PATTERN =
Pattern.compile("^1[3-9]\\d{9}$");
@Override
public String message(Phone anno) {
return anno.message();
}
@Override
public Class<?>[] groups(Phone anno) {
return anno.groups();
}
@Override
public Result validateOfValue(Phone anno, Object val, StringBuilder tmp) {
if (val == null) return Result.succeed();
if (val instanceof String == false) return Result.failure();
if (PHONE_PATTERN.matcher((String) val).matches()) {
return Result.succeed();
}
return Result.failure();
}
@Override
public Result validateOfContext(Context ctx, Phone anno,
String name, StringBuilder tmp) {
String val = ctx.param(name);
if (val == null) return Result.succeed();
if (PHONE_PATTERN.matcher(val).matches()) {
return Result.succeed();
}
return Result.failure(name);
}
}
Enter fullscreen mode Exit fullscreen mode
步驟 3:註冊
@Configuration
public class ValidationConfig {
@Bean
public void registerValidators() {
ValidatorManager.register(Phone.class, new PhoneValidator());
}
}
Enter fullscreen mode Exit fullscreen mode
步驟 4:使用
@Valid
@Controller
public class UserController {
@Mapping("/user/phone")
public void setPhone(@Phone String phone) {
// Validated
}
}
Enter fullscreen mode Exit fullscreen mode
我想改進的地方
兩件事:
-
沒有內建
@Phone或@URL— 這些常見的驗證你會期待有,但必須自己撰寫。雖然容易,但若能內建就更好了。 -
@Validated註解名稱 — 其功能與 JSR 380 的@Valid及 Spring 的@Validated不同。在 Solon 中,@Validated專門用於觸發參數上的實體欄位驗證。了解後沒問題,但與 JSR 380 的命名重疊曾讓我困惑。
結論
Solon 的驗證是一個完整且獨立的系統。你可直接取得 20 多個註解、實體驗證、分組驗證,以及乾淨的自訂驗證器擴充點 — 完全不需要任何 javax.validation 或 jakarta.validation 依賴。
業務感知註解(@Whitelist、@NoRepeatSubmit、@Logined)是其亮點。它們讓你能在驗證層級而非在處理常式中散落檢查,即可強制執行安全性與冪等性限制。
對於希望保持依賴圖精簡的專案,Solon 的驗證是一個穩健的選擇。
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.